Edges define the order in which nodes run. Every node needs an outgoing edge. Every flow needs an edge out of START and must be able to reach END.
Sentinels
Two reserved node names control flow boundaries:
START: the entry point. Exactly one edge must originate from it.
END: the terminal marker. Any node pointing to END finishes the flow when reached.
import { createFlow, END, START } from "@waniwani/sdk/mcp";
Direct edges
Use .addEdge(from, to) for unconditional transitions. Each node can have at most one outgoing edge.
flow
.addEdge(START, "ask_email")
.addEdge("ask_email", "ask_use_case")
.addEdge("ask_use_case", "record")
.addEdge("record", END);
Calling addEdge twice from the same node throws at build time. If you need a second outgoing path, replace the first call with addConditionalEdge.
Conditional edges
Use .addConditionalEdge(from, condition) when the next node depends on state. The condition function receives Partial<TState> and must return a node name (or END). It can be async.
flow
.addNode("ask_email", ({ interrupt }) =>
interrupt({ email: { question: "What's your email?" } }),
)
.addNode("analyze_email", ({ state }) => {
const domain = state.email?.split("@")[1] ?? "";
const generic = new Set(["gmail.com", "yahoo.com", "outlook.com"]);
return { isCompanyEmail: !generic.has(domain) };
})
.addNode("ask_company", ({ interrupt }) =>
interrupt({ companyName: { question: "What company are you with?" } }),
)
.addNode("done", () => ({ ready: true }))
.addEdge(START, "ask_email")
.addEdge("ask_email", "analyze_email")
.addConditionalEdge("analyze_email", (state) =>
state.isCompanyEmail ? "done" : "ask_company",
)
.addEdge("ask_company", "done")
.addEdge("done", END);
Async conditions work for branches that depend on external lookups:
.addConditionalEdge("check_inventory", async (state) => {
const inStock = await inventoryService.check(state.sku!);
return inStock ? "confirm_order" : "offer_alternative";
})
The return type is checked against registered node names, so a typo in a branch target fails type-checking.
Reaching END
When execution reaches END, the engine returns status: "complete" to the model and deletes the session’s flow state so a stale continue call cannot resume it. Any node can point to END, directly or conditionally.
.addConditionalEdge("review", (state) =>
state.approved ? END : "ask_corrections",
)
Loops
A conditional edge that points back to an earlier node creates a loop. Useful for retries or collection gathering.
.addNode("ask_item", ({ interrupt }) =>
interrupt({ item: { question: "Add an item (or say 'done')" } }),
)
.addNode("append", ({ state }) => ({
items: [...(state.items ?? []), state.item].filter(Boolean) as string[],
}))
.addEdge("ask_item", "append")
.addConditionalEdge("append", (state) =>
state.item === "done" ? "finalize" : "ask_item",
)
The engine enforces a hard cap of 50 iterations per tool call to prevent runaway loops between action nodes. You will still need END reachable from every branch or the flow can stall.
Compile-time validation
.compile() validates the graph before returning. It throws if:
- No edge exists from
START.
- A
START edge or direct edge points at an undeclared node.
- A node has no outgoing edge.
- An edge is declared
from a non-existent node.
Errors surface immediately at startup, not at the first user interaction.
Visualizing the graph
Both the builder and the compiled flow expose graph(), which returns a Mermaid flowchart TD string. Conditional edges render as dashed arrows to a placeholder because branch targets are decided at runtime.
console.log(flow.graph());