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.