Human-in-the-loop for LangChain

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

Put one call inside your risky LangChain tool, and it waits until your user approves on their phone.

What LangChain gives you

Middleware that interrupts before a tool

LangChain 1.0 handles approvals in middleware. You list the tools that need approval, and the agent interrupts before those calls with a pending request your code approves, edits or rejects. It runs on LangGraph's interrupt underneath.

native
agent = create_agent(
    model=model,
    tools=[issue_refund],
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={"issue_refund": True},
        ),
    ],
    checkpointer=checkpointer,
)

Where it stops

  • It needs a checkpointer, so you run the graph runtime whether you wanted it or not.
  • The interrupt reaches your code, not a person. Getting it to someone is on you.
  • Nothing is written down. There is no record of what the middleware stopped.

The fix

Pushary gets your user's answer.

The middleware stops the tool and hands your code a pending request. decisions.ask() puts the question on your user's phone, waits for Approve or Deny, and saves the answer.

The ask_human tool calls decisions.ask(), which sends the question to your user's phone and waits up to 55 seconds for the answer. It returns approved only on an explicit yes. Use pushary in Python or @pushary/server in TypeScript; there is no wrapper to install.

bash
pip install pushary
two calls
import os
from langchain_core.tools import tool
from pushary import PusharyServer

px = PusharyServer(api_key=os.environ["PUSHARY_API_KEY"])

# Once per user: show them this link to connect their phone.
link = px.enroll("user_123")["universalLink"]

@tool
def ask_human(question: str) -> str:
    """Ask a real person to approve before acting."""
    d = px.decisions.ask(question=question, external_id="user_123", type="confirm")
    return "approved" if d["approved"] else f"not approved ({d['status']})"

# agent = create_agent(model, tools=[ask_human])

How it works

From install to the first answer

Connect your user once. Put the approval on the risky step. Pushary sends the question, waits for the answer and saves it.

  1. 01

    Install the SDK

    Run pip install pushary for Python, or npm i @pushary/server for TypeScript. Put your API key in PUSHARY_API_KEY.

  2. 02

    Connect your user once

    One SDK call per user returns a connect link. Your user opens it once on their phone with the free Pushary app. No account, no payment.

  3. 03

    Wrap decisions.ask in an @tool

    Define an @tool whose body calls px.decisions.ask() and add it to your agent's tools. The docstring tells the model when to use it.

  4. 04

    Act on the answer

    The tool returns approved only on an explicit yes. A no, an expired request or no answer means the agent does not take the action.

A question can be a yes or no, a choice from a list, or a typed answer. Every answer is saved.

Before you ship

Things that bite on LangChain

A tool the model can skip is not a gate
ask_human is a tool the model chooses to call, so it is optional. A prompt that says “ask first” does not stop a refund. Call decisions.ask() inside the refund tool itself, or gate it with the middleware's interrupt_on.
No checkpointer means no interrupt
Without a checkpointer the middleware does nothing useful, because there is nowhere to park the state. Tools run straight through and it looks as if the config was ignored.
Edit lets a person rewrite tool arguments
Approving is simple. Editing means a person changes the arguments before the tool runs, which is a direct route into your tool with typed input. Check edited arguments the same way you check model output.
Tool name keys are exact strings
interrupt_on is keyed by tool name. Rename the tool and the gate silently stops applying, with no error and no warning. Write a test that checks the guarded tool really interrupts.

The Partner plan

Your agent asks your users, not just you

Ship your agent to your users. Each of them approves its risky actions from their phone, under your brand.

$99/ month, every framework included
Start free

Your users never make a Pushary account or pay us.

Your users answer on their own phones

Connect each user once with a link. They install the free Pushary app and answer from the lock screen, with no account of their own.

No answer means no

Run the action only after an explicit Approve. A request that is still waiting, expired or cancelled never counts as a yes. Pushary keeps every request, so your code can pick it up again after a restart.

Reaches them on their phone

The app comes first, because only the app puts Approve and Deny on the lock screen. Browser notifications come next. You build no delivery system.

Every answer saved

Every answer is saved with who answered and when. You can search and export it, and with a callback URL, each answer is also sent to your server as a signed webhook.

FAQ

Questions, answered

Do I need a special package for LangChain?
No. A LangChain tool is a plain function, so calling decisions.ask() from pip install pushary, or @pushary/server in TypeScript, is the whole integration. If you also build LangGraph graphs, pushary-langgraph adds a ready-made ask_human and an interrupt that parks the graph until your user answers.
Does this work with create_agent and older agent runtimes?
Yes. ask_human is a standard LangChain tool, so it works anywhere LangChain tools work, including create_agent, LangGraph prebuilt agents and custom executors. The call inside it does not depend on the runtime.
What happens when nobody responds?
The action does not run. decisions.ask() reports anything short of an explicit yes as not approved. Pushary keeps the request after the wait ends, so a late answer is still saved, a restart does not lose it, and your code can read it later.
Can my end-users answer instead of me?
Yes. Pass your own user id for the person on each call. Each user connects their phone once with a link and the free Pushary app, then answers for their own actions.
Is there a TypeScript path?
Yes. Use @pushary/server and call pushary.decisions.ask() inside a LangChain.js tool the same way. The call shape matches the Python SDK. For LangGraph.js, @pushary/langgraph adds createAskHumanTool and pusharyInterrupt.
How does this compare to the LangChain human-in-the-loop middleware?
Use both. The middleware decides which tools stop, which LangChain does well, then hands the pending request to your code. Pushary does the next part: it reaches your user on their phone, waits for the answer, counts no answer as no, and keeps the record.

Let your LangChain agent ask before it acts.

Connect a user once, ask with one call, and run the action only on a yes. Every answer is saved.

Start free

Partner plan, $99 a month. Your users never make a Pushary account or pay us.