Examples

LangChain

A create_agent support agent whose email is approved, edited or rejected - after it asks a human what to offer.

A support agent built with create_agent and HumanInTheLoopMiddleware. There is no graph code: the middleware pauses the run before any gated tool, and the Vigilator bridge speaks its payload natively. One run raises two interrupts - a question (respond) and an email (approve / edit / reject). Source: Vigilator/examples/langchain.

What you'll see

  • Inbox - first an interrupt whose action request is the question with a single Respond box; then an interrupt carrying the drafted email with Approve, Edit (a typed form, from the tool's schema) and Reject.
  • Live View - a support-agent session per stretch of work: the lookup, then the email draft after your answer, then the send.

Run it

Install

git clone https://github.com/Vigilator/examples
cd examples/langchain
uv sync

Configure

cp .env.example .env

Fill in VIGILATOR_API_KEY and ANTHROPIC_API_KEY. MODEL accepts any init_chat_model string.

Run with polling

uv run main.py

The agent looks the customer up and asks what compensation it may offer. Answer in the inbox; it drafts an email and asks again. Edit the subject line and approve; the script resumes with your version and prints the agent's reply.

Or resume by webhook

uv run fastapi dev webhook.py
svix listen http://localhost:8000/webhooks/vigilator

Register the relay's URL under Integrations → Webhooks for interrupt.answered, put the signing secret in .env, and POST /runs. Each decision is delivered and resumes the run - twice for the default prompt.

How it works

The whole configuration is the interrupt_on map: which tools pause, and what a reviewer may do about each.

agent.py
INTERRUPT_ON = {
    "lookup_customer": False,                                          # runs unreviewed
    "ask_human": {"allowed_decisions": ["respond"]},                   # a question
    "send_email": {"allowed_decisions": ["approve", "edit", "reject"]},
}

agent = create_agent(
    model=MODEL, tools=TOOLS, system_prompt=SYSTEM_PROMPT,
    middleware=[HumanInTheLoopMiddleware(interrupt_on=INTERRUPT_ON)],
    checkpointer=checkpointer,
)

Before a listed tool runs, the middleware raises an interrupt with action_requests and review_configs. The bridge maps each request onto a Vigilator action request; a request whose only decision is respond becomes a plain question, named after the question text, which is how the inbox presents it:

vigilator_bridge.py
name = tool_name
if allowed == ["respond"] and isinstance(args.get("question"), str):
    name = args["question"]  # a question to a human is an action request named after the question

The reviewer's decision comes back as Command(resume={"decisions": [...]}), one entry per action request in order - exactly what the middleware expects. approve runs the tool, edit runs it with the new arguments, reject returns the reason to the model, and respond returns the human's text as the tool's result without running it. The webhook receiver does the same from the event:

webhook.py
if isinstance(event, InterruptAnsweredEvent):
    thread_id = thread_id_from(event.data.external_id)
    if thread_id and pending.get(thread_id) == event.data.id:
        del pending[thread_id]  # a redelivery is now a no-op
        background.add_task(drive, thread_id, to_langgraph_resume(decisions_from_event(event)))

Adapting it

  • Add your tools and list the risky ones in INTERRUPT_ON. True allows every decision; {"allowed_decisions": [...]} narrows them.
  • Any middleware stack works - the bridge only reads the interrupt payload.
  • Deep Agents use the same middleware - see DeepAgents.

On this page