Build a guided chatbot in four files
This small graph collects a traveller's city, saves it as durable node state, and moves to a weather node. It uses the same contract as the larger hotel, support, invoice, and quote examples: a tool handler saves facts, returns a direct response, or selects the next node.
A conversational tool returns go(), stay(),
direct(), or finish(). There is no separate outcome builder,
routing method, or per-turn context object to maintain.
-
Install
$npm install @picoflow/ezgraph zodEnable decorators and import
reflect-metadataonce at the top of your application entry point before node modules load.main.tsimport "reflect-metadata"; -
Describe the state registry
The registry says which node owns each durable value. The generated annotation is the LangGraph state passed to the compiled graph; the registry itself is not a second runtime state object.
weather-graph.state.tsimport { createGraphStateAnnotation, type NodeStateValue } from "@picoflow/ezgraph"; import { CityNode } from "./nodes/city.node.js"; export type WeatherGraphNodes = { CityNode?: NodeStateValue<{ city?: string }>; WeatherNode?: NodeStateValue<{ forecast?: string }>; }; export const WeatherGraphState = createGraphStateAnnotation( CityNode.name, () => ({} as WeatherGraphNodes), ); export type WeatherGraphStateType = typeof WeatherGraphState.State;Keep this registry beside the graph. It makes ownership visible, and the graph reducer replaces the node's channel whenever that node saves state.
-
Write a node: validate, save, transition
A normal node needs only the graph-state generic. Its class name selects its state channel. A tool handler either keeps the model in this node or transitions directly to another registered node.
nodes/city.node.tsimport { z } from "zod"; import { ConversationNode, Tool, go, stay, type ToolDefinition, type ToolResponse, } from "@picoflow/ezgraph"; import type { WeatherGraphStateType } from "../weather-graph.state.js"; import { WeatherNode } from "./weather.node.js"; export class CityNode extends ConversationNode<WeatherGraphStateType> { getPrompt() { return "Collect the city, then call capture_city."; } defineTool(): readonly ToolDefinition[] { return [{ name: "capture_city", description: "Save a city", schema: z.object({ city: z.string().min(2) }) }]; } @Tool("capture_city") async captureCity({ city }: { city: string }): Promise<ToolResponse> { const value = city.trim(); if (!value) return stay("Ask for a non-empty city."); this.saveState({ city: value }); return go(WeatherNode); } }Why this mattersState is written where the fact becomes valid. The next node reads the committed channel, rather than reconstructing a handoff from a synthetic tool result.
-
Register topology explicitly
Register the nodes that may run in a conversational turn. The transition in
go(WeatherNode)selects the next node; no automatic outcome-router setup is required.weather-graph.tsprotected buildGraph() { const graph = this.createStateGraph(WeatherGraphState); graph.registerTurnNodes(CityNode, WeatherNode, TerminateSessionNode); graph.addEdge(TerminateSessionNode, END); return graph.compile(); } -
Choose the right response
Return Use it when stay("feedback")Validation failed; keep collecting in the current node. go(NextNode)The current tool completed and the next stage should run now. direct("...")Code owns the exact customer-facing response, such as a policy decision or review table. finish("...")The conversation is complete. Use
go(NextNode).withState({ ... })when a target node must receive a state update atomically with the transition. UsewithMessages()to add real messages or attachments, andwithCleanup()for cleanup that belongs to that tool result. -
Test locally, then opt into live evaluation
Scripted tests should cover state, transitions, and deterministic policy without a provider. The project convention is
USE_ENV=1for live/provider tests; useKEEP_SESSION=1when you want to inspect a live session afterward.$yarn test:weather-graph$USE_ENV=1 yarn test2:weather-graph
Apply the same contract to a real graph
Walk the multi-stage quote example, or use the developer guide as the API reference.