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.
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.
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.
- 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.
- 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.
- 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.
- 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.
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?
Should I use ask_human or pushary_interrupt?
What happens if nobody answers?
Can my own end-users approve on their own phones?
Does this work with plain LangChain agents?
Why is my LangGraph interrupt not working?
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.