Human-in-the-loop for LangGraph and LangChain

Human-in-the-loop for LangGraph agents, answered on a phone.

A LangGraph node asks a real person and blocks, or parks on a durable interrupt until they answer.

The integration

Add it to your LangGraph agent

Install pushary-langgraph, or @pushary/langgraph in TypeScript. It gives a LangGraph node two ways to ask a real person. ask_human blocks right there until they answer. pushary_interrupt parks the graph with LangGraph's own interrupt() and resumes when a signed webhook comes back, so a long wait holds no compute. Both send the question to a phone, fail closed if nobody answers, and record every decision in a durable ledger. The external_id is your own id for the end-user, so each person approves their own action.

pip install pushary-langgraphtwo calls
from pushary_langgraph import connect, ask_human

# Connect the user's phone once, keyed to your own id.
connect("user_123")

# Ask a human from inside a LangGraph node. Blocks until they answer, fail-closed.
def approve_transfer(state):
    d = ask_human("Approve this transfer?", external_id=state["user_id"], node="approval")
    return {"approved": d["approved"]}

Why you need it

LangGraph pauses the agent. Pushary reaches the human.

LangGraph's interrupt() pauses a node and saves its state, and it does not notify anyone or carry the decision to a phone. pushary-langgraph fills that gap. pushary_interrupt opens the decision, delivers it to the person, parks the graph, and resumes on a signed webhook, while ask_human covers the simpler blocking case. Both are fail-closed and both write to a durable ledger, so a restart never loses the request. The external_id is your own end-user, so each person approves their own action.

What LangGraph already gives you

interrupt() and the checkpointer

LangGraph has the best native pause of any framework here. Call interrupt() inside a node and the graph stops, writes its state to the checkpointer, and returns. Later you resume with Command({ resume: value }) and the graph picks up where it left off. State handling is genuinely solved. What is not solved is everything on the outside of that pause.

native
def approval_node(state):
    answer = interrupt({"question": "Approve the refund?"})
    return {"approved": answer == "yes"}

# somewhere else, eventually:
graph.invoke(Command(resume="yes"), config)

Where it stops

  • interrupt() does not tell anybody. The graph is parked and no notification goes anywhere.
  • Something has to call Command(resume=...). That resume trigger is yours to build, along with whatever surface the human answers on.
  • MemorySaver loses the pause when the process dies. A durable checkpointer is required for any real wait.
  • The checkpoint records that the graph stopped, not who decided or what they saw.

Before you ship it

Things that bite on LangGraph

The whole node re-runs on resume
This is the one that catches people. When the graph resumes, LangGraph re-executes the node from the top, not from the interrupt call. Any side effect before the interrupt happens twice. If you charge a card and then ask for approval in the same node, you charge twice. Keep everything before the interrupt idempotent, or move it to its own node.
Multiple interrupts in one node resolve positionally
Two interrupt() calls in the same node are matched to resume values by order, not by name. Add a conditional branch that skips the first one and the second silently receives the wrong answer.
Pattern A and Pattern B are different tradeoffs
Blocking and polling holds the run open with zero extra infrastructure, which is fine for a wait measured in seconds. Passing a callbackUrl parks the graph via interrupt() and resumes on a signed webhook, which survives worker death and burns no idle compute. Pick the second for anything a person might answer tomorrow.

How it works

From ask to resume

Connect the phone once, then ask a human from the tool or step that guards the action. The waiting, the delivery, and the record are handled for you.

  1. 01

    Install the adapter

    Add the package: pip install pushary-langgraph for Python, or npm i @pushary/langgraph for TypeScript. Set your Pushary API key in the environment.

  2. 02

    Connect the phone

    Call connect(external_id) once per end-user. It returns a link the person taps once to turn on approvals. It opens the Pushary app, which is what carries Approve and Deny on the lock screen, and they need no account of ours.

  3. 03

    Ask a human in a node

    For a quick wait, call ask_human() inside the node that guards the action. For a long wait, call pushary_interrupt() with a callback_url to park the graph and resume it from the webhook.

  4. 04

    Continue on the answer

    The answer is fail-closed: a decline, a timeout, or no reply all count as not approved, so the graph never takes the guarded step on silence. Every decision is kept in the audit log.

A decision is a yes or no confirm, a multiple-choice select, or a free-text input. Every one is stored with its outcome and kept in the audit log.

The Partner plan

Your agent asks your users, not just you

Ship the agent to real users and let each of them approve their own decisions from their phone, under your brand.

$99/ month, every framework included

Your users never see a Pushary login or bill.

Your users answer on their own phones

The externalId is your own id for the end-user. Connect their phone once with a keyless one-tap link that opens the Pushary app, and every approval lands with the right person. No account and no key on their side.

Durable, fail-closed waits

The agent stays open on a durable wait backed by a decisions ledger. If nobody answers in the window you set, the decision comes back denied, so a timeout or a restart never turns into an unapproved action.

Phone-first delivery

Pushary reaches the person on the device they carry: the app first, because it is the only channel that puts Approve and Deny on a lock screen, then PWA push notifications, then Slack if you route it there. You do not build a delivery pipeline.

Signed webhooks and an audit trail

Every decision is written to a durable ledger you can query and export, and resolved decisions post a signed webhook to your callback URL. You have the record of who approved what and when.

FAQ

Questions, answered

Is there a LangGraph package for Pushary?
Yes. pushary-langgraph for Python and @pushary/langgraph for TypeScript. Each gives you connect(), a blocking ask_human, and pushary_interrupt that wraps LangGraph's own interrupt() for the durable path.
Should I use ask_human or pushary_interrupt?
Use ask_human for a straightforward approval that resolves in under a minute. Use pushary_interrupt with a callback_url when the wait can be long: it parks the graph in your checkpointer and resumes from a signed webhook, so it holds no compute.
What happens if nobody answers?
The decision comes back denied. Anything that is not an explicit approval fails closed, so the graph does not take the guarded action, and the durable ledger keeps the request across restarts.
Can my own end-users approve on their own phones?
Yes. The external_id is your own id for the user, connected once with a one-tap link. Each user gets their own approval on their own phone, with no account or app on their side.
Does this work with plain LangChain agents?
Yes. ask_human is a normal function, so you can put it in any LangChain tool or callback the same way you put it in a LangGraph node.
Why is my LangGraph interrupt not working?
Three causes cover almost all of it. There is no checkpointer configured, so there is nowhere to park the state and the graph runs straight through. Or you are resuming with a plain input instead of Command(resume=value), which starts a new run rather than continuing the paused one. Or the interrupt sits inside a tool or a subgraph and you are looking for it on the parent graph's output, where it does not appear. If the pause works but side effects happen twice, that is the node re-running from the top on resume, which is expected behaviour rather than a bug.

Let your LangGraph agent ask a human.

Connect a user with one tap, ask a human with one call, and act on a fail-closed decision. Every decision is saved to a durable ledger you can audit.

Partner plan, $99 a month. Your users never see a Pushary login or bill.