Back to roadmaps langgraph Course

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: