LangGraph Tutorial (TypeScript): adding human-in-the-loop for intermediate developers
This tutorial shows you how to pause a LangGraph workflow, send a state snapshot to a human, and resume execution with the human’s decision in TypeScript. You need this when an agent reaches an ambiguous or high-risk step, like approving a refund, classifying a claim, or escalating a customer complaint.
What You'll Need
- •Node.js 18+
- •TypeScript 5+
- •
@langchain/langgraph - •
@langchain/core - •
zod - •A package manager like
npm,pnpm, oryarn - •A place to run the code locally
- •Optional: an OpenAI API key if you want to add an LLM later, but this tutorial does not require one
Install the packages:
npm install @langchain/langgraph @langchain/core zod
npm install -D typescript tsx @types/node
Step-by-Step
- •Start by defining the graph state and the shape of the human review payload. Keep it explicit; you want a typed contract for what gets paused and what comes back from the reviewer.
import { Annotation } from "@langchain/langgraph";
import { z } from "zod";
export const GraphState = Annotation.Root({
ticketId: Annotation<string>(),
customerMessage: Annotation<string>(),
draftDecision: Annotation<string>(),
humanDecision: Annotation<string | null>({
default: () => null,
reducer: (_, next) => next,
}),
});
export const HumanReviewSchema = z.object({
approved: z.boolean(),
notes: z.string().min(1),
});
- •Add a node that prepares the decision and then pauses by returning a command with
interrupt. In production, this is where you’d send the state to a queue, database, or review UI before waiting for a person to respond.
import { interrupt } from "@langchain/langgraph";
export function reviewNode(state: typeof GraphState.State) {
const reviewRequest = {
ticketId: state.ticketId,
message: state.customerMessage,
draftDecision: state.draftDecision,
};
const humanInput = interrupt(reviewRequest);
return {
humanDecision: humanInput.approved ? "approved" : "rejected",
draftDecision:
humanInput.approved ? state.draftDecision : "Escalate to supervisor",
};
}
- •Build the graph with a single review step and compile it. This keeps the example small, but the pattern is the same when you insert human approval between multiple agent steps.
import { StateGraph, START, END } from "@langchain/langgraph";
const builder = new StateGraph(GraphState)
.addNode("review", reviewNode)
.addEdge(START, "review")
.addEdge("review", END);
export const app = builder.compile();
- •Run the graph once to get an interrupt, inspect the payload, then resume it with a human response using the same thread id. The important part is that
resumecontinues the exact paused execution path.
async function main() {
const config = { configurable: { thread_id: "ticket-1001" } };
const firstRun = await app.invoke(
{
ticketId: "ticket-1001",
customerMessage: "I was charged twice for my subscription.",
draftDecision: "Issue refund for one charge",
},
config
);
console.log("First run result:", firstRun);
}
main().catch(console.error);
- •Resume from the interrupt with a real decision object. In practice this object would come from your reviewer UI after they approve or reject the action.
async function resumeFlow() {
const config = { configurable: { thread_id: "ticket-1001" } };
const resumed = await app.invoke(
{
humanDecision: {
approved: true,
notes: "Customer verified. Refund one charge only.",
},
},
config
);
console.log("Resumed result:", resumed);
}
resumeFlow().catch(console.error);
Testing It
Run the script once and confirm that execution pauses at interrupt instead of finishing immediately. Then call the same graph again with the same thread_id and a valid humanDecision object; that should complete the workflow and update the final state.
If you are wiring this into an API, check that each request uses stable thread identifiers per case or ticket. If you change the thread id between pause and resume, LangGraph will treat it as a different conversation and you will not get back to the interrupted node.
A good test is to reject once and approve once. That verifies both branches of your downstream logic and proves your review payload is actually controlling execution.
Next Steps
- •Add persistence with a checkpointer so interrupted runs survive process restarts.
- •Put the interrupt payload behind an internal review endpoint or admin UI.
- •Extend the pattern to multi-step approvals, where legal, fraud, or support all need to sign off before execution.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit