Project: E-Commerce Refund Workflow with Manual Approvals
In this project, we will build a shopping refund workflow. If the requested refund amount exceeds $100, the system triggers an interrupt, pausing the graph. Once a manager manually approves it, the graph resumes, calls the banking API, and logs the transaction.
1. Project Workflow
graph TD
A[Start: Refund request] --> B[Node: CheckRefundAmount]
B -->|Conditional Edge| C{Is amount > 100?}
C -->|Yes| D[Interrupt Pause: Wait for manager]
C -->|No| E[Node: ProcessBankRefund]
D -->|Manually Approved| E
E --> F[__end__]2. Implementing the Refund Graph
Configure the state and nodes:
// src/services/refundWorkflow.ts
import { StateGraph, Annotation, MemorySaver } from "@langchain/langgraph";
// 1. Declare State schema properties
const RefundStateAnnotation = Annotation.Root({
customerId: Annotation<string>(),
refundAmount: Annotation<number>(),
approvedByManager: Annotation<boolean>(),
refundStatus: Annotation<string>(),
});
// 2. Define Node checks
async function checkAmountNode(state: typeof RefundStateAnnotation.State) {
console.log("Validating refund for customer:", state.customerId, "Amount:", state.refundAmount);
return {
approvedByManager: false,
refundStatus: "pending-approval",
};
}
async function processBankNode(state: typeof RefundStateAnnotation.State) {
console.log("Calling payment gateway...");
return {
refundStatus: "refund-completed-successfully",
};
}
// 3. Compose the StateGraph
const workflow = new StateGraph(RefundStateAnnotation)
.addNode("checkAmount", checkAmountNode)
.addNode("processBank", processBankNode)
.setEntryPoint("checkAmount")
// Define conditional edge path departing from checkAmount
.addConditionalEdges("checkAmount", (state) => {
if (state.refundAmount > 100) {
return "needs-approval";
}
return "auto-approve";
}, {
"needs-approval": "processBank", // Will be intercepted by compiler interrupt rule
"auto-approve": "processBank", // Runs straight through
})
.addEdge("processBank", "__end__");
// 4. Compile with MemorySaver and Interrupt rules
const checkpointerStore = new MemorySaver();
export const refundApp = workflow.compile({
checkpointer: checkpointerStore,
// Intercept and pause immediately before processBank executes
interruptBefore: ["processBank"],
});3. Integration Script for Routes
When building REST API endpoints (such as Express or Next.js API routes), handle the multi-stage invoke flow:
Route A: Initiate Refund
// POST /api/refund/initiate
export async function initiateRefundHandler(customerId: string, amount: number) {
const threadId = `refund-${customerId}-${Date.now()}`;
const config = { configurable: { thread_id: threadId } };
const finalState = await refundApp.invoke({
customerId,
refundAmount: amount,
}, config);
// Check if execution was suspended
if (finalState.refundStatus === "pending-approval") {
return { status: "paused", threadId, message: "Refund exceeds $100. Awaiting review." };
}
return { status: "completed", threadId, message: "Refund processed automatically." };
}Route B: Approve and Resume Refund
// POST /api/refund/approve
export async function approveRefundHandler(threadId: string) {
const config = { configurable: { thread_id: threadId } };
// 1. Manually update approval state in database checkpoint
await refundApp.updateState(config, {
approvedByManager: true,
});
// 2. Resume execution
const finalState = await refundApp.invoke(null, config);
return { status: "success", finalStatus: finalState.refundStatus };
}Published on Last updated: