Skip to main content

Use LangGraph Interrupts

Use LangGraph interrupts when a graph reaches a decision that needs a human before it can continue. The graph pauses with interrupt(), TryAgent routes the decision to the right reviewer, and your callback resumes the same graph thread with new Command({ resume }). This guide follows LangGraph’s official interrupts pattern:
  • Compile the graph with a checkpointer.
  • Invoke and resume with the same { configurable: { thread_id } }.
  • Call interrupt() inside the node that needs external input.
  • Read the interrupt payload from __interrupt__.
  • Resume with new Command({ resume: ... }).

1. Interrupt with a TryAgent payload

Keep the graph node deterministic up to the interrupt. Build the TryAgent escalation payload inside the node, then let the caller publish it to TryAgent after LangGraph has paused.
import {
  Annotation,
  END,
  MemorySaver,
  START,
  StateGraph,
  interrupt,
} from "@langchain/langgraph";
import type { EscalateInput } from "@tryagent/sdk";

type ApprovalResume = {
  escalationId: string;
  choice: "approve" | "reject";
  answeredBy?: string;
};

type TryAgentInterruptPayload = {
  kind: "tryagent_approval";
  policy: string;
  tryagentInput: EscalateInput;
};

const State = Annotation.Root({
  runId: Annotation<string>(),
  orderId: Annotation<string>(),
  status: Annotation<"approved" | "rejected" | undefined>(),
  escalationId: Annotation<string | undefined>(),
});

const graph = new StateGraph(State)
  .addNode("approval", (state) => {
    const decision = interrupt<TryAgentInterruptPayload, ApprovalResume>({
      kind: "tryagent_approval",
      policy: "orders.auth_doc",
      tryagentInput: {
        agentId: "order-review-agent",
        runId: state.runId,
        subject: {
          type: "order",
          id: state.orderId,
          label: `Order #${state.orderId}`,
        },
        question: "Approve this order authorization document?",
        evidence: ["Signature date is missing.", "Customer identity matched."],
        choices: [
          { id: "approve", label: "Approve" },
          { id: "reject", label: "Reject" },
        ],
        resume: {
          mode: "webhook",
          url: "https://api.example.com/tryagent/resume",
        },
      },
    });

    return {
      status: decision.choice === "approve" ? "approved" : "rejected",
      escalationId: decision.escalationId,
    };
  })
  .addEdge(START, "approval")
  .addEdge("approval", END)
  .compile({ checkpointer: new MemorySaver() });
MemorySaver is useful for local examples. Use a durable checkpointer in production so paused graph state survives process restarts.

2. Publish the interrupt to TryAgent

Use one durable value for both systems:
  • TryAgent runId
  • LangGraph thread_id
When the graph pauses, the payload you passed to interrupt() is returned under __interrupt__. Publish that payload to TryAgent.
import { INTERRUPT, isInterrupted } from "@langchain/langgraph";
import { TryAgent } from "@tryagent/sdk";

const tryagent = new TryAgent({
  apiKey: process.env.TRYAGENT_API_KEY!,
});

const runId = "order-review-ord_4821";
const config = { configurable: { thread_id: runId } };

const result = await graph.invoke(
  { runId, orderId: "ord_4821" },
  config,
);

if (isInterrupted(result)) {
  const activeInterrupt = result[INTERRUPT][0];
  const payload = activeInterrupt.value as TryAgentInterruptPayload;

  const escalation = await tryagent.escalate(
    payload.policy,
    payload.tryagentInput,
  );

  console.log({
    interruptId: activeInterrupt.id,
    escalationId: escalation.id,
    runId,
  });
}

3. Resume from the signed callback

When TryAgent calls your resume.url, verify the callback signature first with webhooks.constructEvent. Then resume the same LangGraph thread with new Command({ resume }).
import { Command } from "@langchain/langgraph";

export async function POST(request: Request) {
  const event = await tryagent.webhooks.constructEvent({
    payload: await request.text(),
    signature: request.headers.get("x-tryagent-signature"),
    secret: process.env.TRYAGENT_WEBHOOK_SECRET!,
  });

  await graph.invoke(
    new Command({
      resume: {
        escalationId: event.escalationId,
        choice: event.choice,
        answeredBy: event.answeredBy,
      },
    }),
    {
      configurable: {
        thread_id: event.runId,
      },
    },
  );

  return Response.json({ ok: true });
}
The object passed as resume becomes the return value of the original interrupt() call. In the first example, that means decision.choice and decision.escalationId are available inside the approval node after resume.

Rules for TryAgent integrations

  • Use runId as the LangGraph thread_id.
  • Keep interrupt payloads JSON-serializable.
  • Do not perform non-idempotent side effects before interrupt() in the same node. If the node is re-entered on resume, that code can run again.
  • Prefer publishing the TryAgent escalation from the caller after LangGraph has returned an interrupt.
  • For sequential approvals, resume each interrupt as it appears. For parallel interrupts, pair resume values with the interrupt IDs returned by LangGraph.
See the runnable examples for complete stock research and refund approval workflows using this pattern.