LangGraph Tutorial (TypeScript): adding human-in-the-loop for beginners
This tutorial shows you how to add a human approval step to a LangGraph workflow in TypeScript. You need this when an agent is about to do something risky, like send an email, approve a payment, or update a customer record.
What You'll Need
- •Node.js 18+
- •TypeScript 5+
- •A LangGraph-compatible project setup
- •
@langchain/langgraph - •
@langchain/core - •
zod - •An API key only if you later connect the graph to an LLM or external tools
- •A terminal and a TypeScript runner like
tsxorts-node
Install the packages:
npm install @langchain/langgraph @langchain/core zod
npm install -D typescript tsx @types/node
Step-by-Step
- •Start with a small state object that tracks the user request, the draft action, and whether a human has approved it. Keep the state explicit; that makes the interrupt/resume flow easy to reason about.
import { Annotation } from "@langchain/langgraph";
export const AgentState = Annotation.Root({
request: Annotation<string>(),
draftAction: Annotation<string>(),
approved: Annotation<boolean>(),
result: Annotation<string>(),
});
- •Build two nodes: one that prepares the action and one that asks for human approval. The approval node uses
interrupt()so execution pauses before the graph continues.
import { interrupt } from "@langchain/langgraph";
import type { NodeInterrupt } from "@langchain/langgraph";
export async function draftNode(state: typeof AgentState.State) {
return {
draftAction: `Send refund email for request: ${state.request}`,
approved: false,
};
}
export async function approvalNode(state: typeof AgentState.State) {
const decision = interrupt({
prompt: "Approve this action?",
draftAction: state.draftAction,
options: ["approve", "reject"],
}) as NodeInterrupt<{ prompt: string; draftAction: string; options: string[] }>;
return {
approved: decision.value === "approve",
};
}
- •Add a final node that only runs after approval. In real systems, this is where you call your tool, write to your database, or send the message.
export async function executeNode(state: typeof AgentState.State) {
if (!state.approved) {
return {
result: "Action rejected by human reviewer.",
};
}
return {
result: `Executed safely: ${state.draftAction}`,
};
}
- •Wire the graph together with an entry point, a pause point, and a finish node. The key detail is that
approvalNodesits in the middle of the flow, so you can stop and resume without losing state.
import { StateGraph, START, END } from "@langchain/langgraph";
import { AgentState } from "./state";
import { draftNode, approvalNode, executeNode } from "./nodes";
const graph = new StateGraph(AgentState)
.addNode("draft", draftNode)
.addNode("approval", approvalNode)
.addNode("execute", executeNode)
.addEdge(START, "draft")
.addEdge("draft", "approval")
.addEdge("approval", "execute")
.addEdge("execute", END);
export const app = graph.compile();
- •Run the graph with a thread ID so LangGraph can pause and resume the same conversation. When
interrupt()fires, you capture the pending checkpoint, inspect it, then resume with your human decision.
import { app } from "./graph";
async function main() {
const config = { configurable: { thread_id: "ticket-123" } };
const firstRun = await app.invoke(
{ request: "Refund $25 for order #8842" },
config
);
console.log("First run:", firstRun);
const resumed = await app.invoke(
{
approved: true,
},
config
);
console.log("Resumed run:", resumed);
}
main().catch(console.error);
Testing It
Run the file once and confirm that execution pauses at the approval node instead of immediately reaching execute. In practice, your first output should show either an interrupted state or partial progress, depending on how your runner surfaces checkpoints.
Then resume with an approval decision using the same thread_id. If everything is wired correctly, the second run should continue from the paused point and produce the final result.
To test rejection paths, change the resumed input to reject and verify that executeNode returns the rejection message instead of performing the action. That’s the core behavior you want in banking and insurance flows: no irreversible side effects until a human signs off.
Next Steps
- •Add persistent checkpoint storage so pauses survive process restarts.
- •Replace the hardcoded approval prompt with structured review metadata like customer ID, amount, and risk score.
- •Connect this pattern to an LLM planner so only specific branches require human-in-the-loop review.
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