# Using LangGraph interrupt for multi-step wizards with branching — right tool or wrong abstraction?

**URL:** <https://forum.langchain.com/t/using-langgraph-interrupt-for-multi-step-wizards-with-branching-right-tool-or-wrong-abstraction/3530>\
**Category:** LangGraph\
**Tags:** self-hosted, intro-to-langgraph, js-help\
**Created:** [April 20, 2026, 1:46pm UTC](https://forum.langchain.com/t/using-langgraph-interrupt-for-multi-step-wizards-with-branching-right-tool-or-wrong-abstraction/3530 "2026-04-20T13:46:17Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![lofti198](https://yyz1.discourse-cdn.com/flex007/user_avatar/forum.langchain.com/lofti198/32/2534_2.png) [@lofti198](https://forum.langchain.com/u/lofti198)\
**Post date:** [April 20, 2026, 1:46pm UTC](https://forum.langchain.com/t/using-langgraph-interrupt-for-multi-step-wizards-with-branching-right-tool-or-wrong-abstraction/3530/1 "2026-04-20T13:46:17Z")

</div>

I’m building an AI assistant in Next.js + LangGraph JS. My core flow is well-suited for LangGraph: decompose a research question → parallel ReAct sub-agents → synthesize. No complaints there.

But I want to add **guided wizards** alongside this — for example, a scraper configuration flow:

1. Agent analyzes a URL and proposes extraction rules

2. User approves or gives feedback

3. Agent adjusts, then moves to link collection setup

4. …and so on

The wizard has:

- **Branching** based on page type (list page vs. detail page → different next steps)

- **Multiple entry points** (user can start from data collection or from link setup)

- **Mid-flow exits** — user switches to a different intent entirely

- **Resume later** — user comes back to the incomplete wizard

My instinct is that this is a **finite state machine problem** , not a LangGraph problem. Each wizard step is a state, user input is an event, and pause/resume is just serializing a snapshot to JSON in a DB.

**The question:** Is LangGraph’s `interrupt()` / human-in-the-loop the right primitive here, or does it fight against you when you need:

- deterministic branching (not LLM-driven transitions)

- serializable pause/resume across sessions (not just within a single graph run)

- multiple wizard entry points in the same graph

- clean exit when user abandons mid-flow

Has anyone used LangGraph to manage wizard-style flows like this? Did it work well, or did you end up wrapping it with explicit state management (XState, a custom FSM, etc.)? Curious whether the LangGraph checkpoint + interrupt system is expressive enough for this, or whether it’s genuinely the wrong layer of abstraction for deterministic multi-step UX flows.

---

<div class="post-metadata">

**Author:** ![keenborder786](https://yyz1.discourse-cdn.com/flex007/user_avatar/forum.langchain.com/keenborder786/32/1757_2.png) [@keenborder786](https://forum.langchain.com/u/keenborder786)\
**Post date:** [April 20, 2026, 3:07pm UTC](https://forum.langchain.com/t/using-langgraph-interrupt-for-multi-step-wizards-with-branching-right-tool-or-wrong-abstraction/3530/2 "2026-04-20T15:07:27Z")

</div>

Hello @lofti198, welcome to langchain community.

## LangGraph is the right primitive for your wizard flows

### 1. Deterministic branching (not LLM-driven)

LangGraph does **not** require LLM-driven transitions. Any node , even a pure utility function , can return a [`Command`](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph.Command.html) that specifies the next node deterministically:

```typescript
const analyzePageType = async (state) => {
  const type = detectPageType(state.url); // zero LLM, pure logic
  return new Command({
    update: { pageType: type },
    goto: type === "list" ? "configureListStep" : "configureDetailStep",
  });
};

```

`goto` accepts a single node name, an array of names, or `Send` objects for parallel fan-out. The branching is yours to control, not the model’s. Docs: [Command API Reference](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph.Command.html)

* * *

### 2. Serializable pause/resume across sessions

This is the exact problem `interrupt()` + the checkpointer was built for. The mechanics:

- `interrupt()` inside a node suspends execution and serializes full graph state to your checkpointer backend (Postgres in production).
- The `thread_id` in your config is the persistent key — store it in your DB against the user record.
- When the user returns, invoke the same graph with the same `thread_id` and a `Command({ resume: userInput })`. Execution picks up from the exact interrupted line, with all prior `task()` results cached and not re-run.

Docs: [Persistence & Checkpointing](https://langchain-ai.github.io/langgraphjs/concepts/persistence/)

* * *

### 3. Multiple wizard entry points

Use the **Functional API** (`entrypoint` + `task`), not `StateGraph`. Each wizard becomes its own `entrypoint` , a regular async function with no graph topology to declare. Branching is plain `if/else`, loops are plain `for` loops, and each `interrupt()` is just a `await`-able pause:

```typescript
import { entrypoint, task } from "@langchain/langgraph/func";

const scraperWizard = entrypoint(
  { checkpointer, name: "scraperWizard" },
  async (input: { url: string }) => {
    const rules = await analyzeUrl(input.url).then(r => r.result); // task() — cached on resume
    const approval = interrupt({ rules, prompt: "Approve or give feedback" });

    if (approval.type === "feedback") {
      const revised = await adjustRules({ rules, feedback: approval.text }).then(r => r.result);
      interrupt({ rules: revised, prompt: "Confirm revised rules" });
    }

    const linkConfig = interrupt({ prompt: "Configure link collection" });
    return { rules, linkConfig };
  }
);

const linkSetupWizard = entrypoint( // separate entry point
  { checkpointer, name: "linkSetupWizard" },
  async (input) => { /* ... */ }
);

```

Both wizards share one Postgres `checkpointer` instance , no cross-contamination of state, unified persistence. Docs: [Functional API Concepts](https://langchain-ai.github.io/langgraphjs/concepts/functional_api/)

* * *

### 4. Clean mid-flow exit when user abandons or switches intent

An abandoned thread is just a frozen checkpoint, no process is running, no cost incurred. The pattern is simple:

| User action | What you do |
| --- | --- |
| Abandons wizard | Stop invoking. Thread stays frozen in Postgres. |
| Comes back later | Load saved `thread_id`, call `graph.invoke(new Command({ resume: ... }))` |
| Switches to different intent entirely | Start a **new** `thread_id` for the new flow. Old thread stays untouched. |
| Permanently discards a wizard | Delete the checkpoint row from Postgres. |

Docs: [Threads & State Management](https://docs.langchain.com/langgraph-platform/use-threads)

* * *

### Recommended architecture for your app

```auto
Next.js frontend
    │
    │ thread_id per wizard session (stored in your DB per user)
    ↓
LangGraph Functional API (JS)
    ├── scraperWizard entrypoint ─┐
    ├── linkSetupWizard entrypoint ├── shared Postgres checkpointer
    └── researchGraph StateGraph ─┘ (your existing ReAct flow)

```

Your ReAct decompose→parallelize→synthesize flow stays as a `StateGraph` (topology matters there). The wizard flows use `entrypoint` (control flow is sequential and conditional). Both run on the same LangGraph runtime and the same Postgres checkpointer. No XState, no custom FSM layer needed.

* * *

### The one case to genuinely reach for XState

If a wizard has **zero AI steps** , purely routing user input through deterministic transitions with no `task()` calls; then LangGraph’s serialization and checkpoint overhead adds complexity with no payoff. XState’s visual state chart tooling is also better for documenting complex pure-UX flows to non-engineers. But if there’s a single LLM call in the loop, LangGraph’s `task()` result caching on resume alone makes it worth it.
