No EZGraph key required. Free production use. No runtime fee. Bring your own model provider. Run in your own infrastructure. Optional support is available.

Tutorial

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.

The current contract

A conversational tool returns go(), stay(), direct(), or finish(). There is no separate outcome builder, routing method, or per-turn context object to maintain.

  1. Install

    $npm install @picoflow/ezgraph zod

    Enable decorators and import reflect-metadata once at the top of your application entry point before node modules load.

    main.ts
    import "reflect-metadata";
  2. 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.ts
    import { 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.

  3. 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.ts
    import { 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 matters

    State is written where the fact becomes valid. The next node reads the committed channel, rather than reconstructing a handoff from a synthetic tool result.

  4. 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.ts
    protected buildGraph() {
      const graph = this.createStateGraph(WeatherGraphState);
      graph.registerTurnNodes(CityNode, WeatherNode, TerminateSessionNode);
      graph.addEdge(TerminateSessionNode, END);
      return graph.compile();
    }
  5. Choose the right response

    ReturnUse 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. Use withMessages() to add real messages or attachments, and withCleanup() for cleanup that belongs to that tool result.

  6. 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=1 for live/provider tests; use KEEP_SESSION=1 when you want to inspect a live session afterward.

    $yarn test:weather-graph
    $USE_ENV=1 yarn test2:weather-graph
Next

Apply the same contract to a real graph

Walk the multi-stage quote example, or use the developer guide as the API reference.