EZGraph developer guide
EZGraph is a TypeScript layer for durable, multi-turn LangGraph applications. Each graph node owns its prompt, tools, state writes, and transition decision. LangGraph owns execution; EZGraph supplies the contracts that keep a conversation resumable and auditable.
The current API has one rule worth remembering:
A tool handler writes durable state itself, then returns
go,stay,direct, orfinish.
There is no deferred tool-result builder, nextStep(), createContext(),
turnState(), or automatic outcome routing.
The node contract
Define graph-owned state once. The node registry is the durable schema: every node channel LangGraph replaces lives there.
import { createGraphStateAnnotation, type NodeStateValue } from "@picoflow/ezgraph";
import { DriverNode } from "./nodes/driver.node.js";
export type QuoteGraphNodes = {
DriverNode?: NodeStateValue<{ driver?: DriverProfile }>;
VehicleNode?: NodeStateValue<{ vehicle?: VehicleUse }>;
};
export const QuoteGraphState = createGraphStateAnnotation(
DriverNode.name,
() => ({} as QuoteGraphNodes),
);
export type QuoteGraphStateType = typeof QuoteGraphState.State;
Conversational nodes normally need only the graph state type.
export class DriverNode extends ConversationNode<QuoteGraphStateType> {
getPrompt() {
return "Collect the driver's identity and licence details.";
}
}
Use a local cast from the registry only where TypeScript needs the exact node-channel shape. The class does not carry a redundant second state generic.
State belongs to the node that owns it
Inside an active node invocation, saveState() stages a patch to the current
node channel. EZGraph materializes that channel and LangGraph’s node reducer
replaces it atomically. Use graph.saveNodeState() for another node’s channel.
@Tool("capture_driver")
async captureDriver(input: DriverInput): Promise<ToolResponse> {
const driver = validateDriver(input);
if ("error" in driver) return stay(JSON.stringify({ accepted: false, error: driver.error }));
this.saveState({ driver: driver.value });
return go(VehicleNode);
}
this.saveState({ criteria });
this.graph.saveNodeState(PresentNode, { hotelFound: results });
return go(PresentNode).withMessage(
new HumanMessage("Present the current hotel choices and booking options."),
);
graph.graphState() exposes the invocation’s materialized graph state. Use it
when deterministic policy needs data owned by another node. Do not mutate a
node instance or a session document directly.
Return one direct tool response
| Response | Meaning |
|---|---|
stay(feedback) |
Keep this node active and give the model corrective tool feedback. The agent loop continues. |
go(Target) |
Save the target as the durable resume node and enter it in the same graph invocation. |
direct(content) |
Stop model work and return code-owned content while keeping the graph active. |
finish(content) |
Stop model work and complete the graph with code-owned content. |
Use withState() on go(Target) when the transition itself seeds target state.
const pending: PendingRefund = { request, quote, reasons };
return go(ApprovalNode).withState({ pending });
Use withMessage() only for a genuine target-stage instruction. To preserve a
customer’s input across a distinct history space, append that actual input to
the target history deliberately; never manufacture a user message just to
express internal control flow.
const request = this.graph.input(this.graph.graphState());
this.graph.appendHistory(this.graph.historySpace(ReturnsNode.id()), [
new HumanMessage(request),
]);
return go(ReturnsNode);
Build topology explicitly
Register conversational entry points, then declare only genuine fixed worker
edges. Tool responses select conversational handoffs; there is no
configAutoRoute() call.
protected buildGraph() {
const graph = this.createStateGraph(QuoteGraphState);
graph.registerTurnNodes(
DriverNode,
VehicleNode,
HistoryNode,
CoverageNode,
QuoteNode,
TerminateSessionNode,
);
graph.addEdge(TerminateSessionNode, END);
return graph.compile();
}
ConversationNode inherits terminate_session. Every graph containing one
must register TerminateSessionNode, including one-shot file-extraction
graphs, then connect it to END.
Keep policy deterministic
The model may collect a request, but deterministic code owns eligibility, prices, IDs, durable commits, and irreversible transitions.
const adjudication = PolicyEngine.adjudicate(order, request.lineIds, request.reason);
if (adjudication.decision === "review") {
return go(ApprovalNode).withState({
pending: { request, quote: adjudication.quote!, reasons: adjudication.reasons },
});
}
For an approval gate, generate the first pending-refund presentation from the saved quote in code. The model should not invent a money amount, RMA, ticket identifier, or completion claim.
File attachments
ToolResponse supports model-visible attachment messages and cleanup. A file
tool can remain in the same agent loop without reviving the old result type.
return stay(JSON.stringify({ attached: true, fileName: name, fileId: upload.fileId }))
.withCleanup(upload.cleanup)
.withMessages([
new HumanMessage({ content: [
{ type: "text", text: "Analyze the attached file and submit the extraction." },
upload.contentPart,
] }),
]);
Test in two tiers
Keep deterministic graph tests separate from opt-in provider evaluation.
{
"test:quote-graph": "node --import tsx --test test/quote-graph/*.spec.ts",
"test2:quote-graph": "USE_ENV=1 KEEP_SESSION=1 node --import tsx --test test/quote-graph/quote-graph.e2e.spec.ts"
}
USE_ENV=1 is the single live-provider switch. KEEP_SESSION=1 retains the
session only when a replay needs inspection. Assertions about durable state
belong in deterministic tests; semantic judges and provider calls remain
explicitly opt-in.
Migration checklist
- Replace
ConversationNode<State, LocalState, Context>withConversationNode<State>. - Move channel shapes into the graph’s
*GraphNodesregistry. - Replace
this.toolResult()withstay,go,direct, orfinish. - Replace
turnState()withgetState()andturnGraphState()withgraph.graphState(). - Replace deferred
withStateeffects withsaveState()orgraph.saveNodeState(). - Remove
createContext,nextStep, outcome builders, andconfigAutoRoute(). - Test the entry, correction, transition, restore, and completion paths.
See the QuoteGraph walkthrough for a complete guided application and the tutorial for a small runnable graph.