Run two nodes.
See the contract in action.
Collect a destination, save it as an accepted fact, and pass control to a second node that creates a trip card. Then inspect the saved session and run the validation tests.
Node.js 22.5 or later. No model credentials or EZGraph account needed for this scripted run.
Extract the project and run it.
The download contains the package configuration, TypeScript settings, graph, entrypoint, tests, and README. Extract it, open a terminal in its parent directory, and run:
cd ezgraph-starter
npm install
npm startThe entrypoint uses scriptedGateway() to provide predictable model replies. The real compiled graph, tool validation, handlers, histories, and session engine execute.
You: Hi
EZGraph: Which city would you like to visit?
You: Lisbon
EZGraph: Trip to Lisbon: choose your dates, then book transport.
Completed: true
Session document:
{ … }The graph continues a session across two turns, saves accepted city data, enters the next node, and finishes with exact content from code. Scripted replies establish contract behavior; live model quality needs a separate evaluation. The memory session lasts for this process.
Read the workflow from top to bottom.
CityNode publishes capture_city and saves valid input in its own channel. go(SummaryNode) enters the next stage in the same turn. That node reads the saved city, creates a trip card in code, and calls finish().
The graph declares the model and named histories. Its topology registers the two conversational nodes and the built-in termination path.
import { END } from "@langchain/langgraph";
import { z } from "zod";
import {
BaseGraph, LlmNode, Tool, ModelCatalog, TerminateSessionNode,
GRAPH_END_NODE, createGraphStateAnnotation, go, stay, finish,
type GraphDefinition, type LlmGateway, type NodeStateValue,
type ToolDefinition, type ToolResponse,
} from "@picoflow/ezgraph";
type StarterNodes = {
CityNode?: NodeStateValue<{ city?: string }>;
SummaryNode?: NodeStateValue<{ plan?: string }>;
};
export const StarterState = createGraphStateAnnotation(
"CityNode", () => ({} as StarterNodes),
);
export type StarterStateType = typeof StarterState.State;
export class CityNode extends LlmNode<StarterStateType, { city?: string }> {
getPrompt() {
return "Ask for a destination city, then call capture_city. To stop, call terminate_session.";
}
defineTool(): readonly ToolDefinition[] {
return [{
name: "capture_city",
description: "Save a valid destination 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.length < 2 || !/^[\p{L} .'-]+$/u.test(value)) {
return stay("Ask for a city name using letters, rather than a number.");
}
this.saveState({ city: value });
return go(SummaryNode);
}
}
export class SummaryNode extends LlmNode<StarterStateType, { plan?: string }> {
getPrompt(state: StarterStateType) {
return `The saved city is ${state.nodes.CityNode?.city}. Call make_plan.`;
}
defineTool(): readonly ToolDefinition[] {
return [{
name: "make_plan",
description: "Create the trip card for the saved city.",
schema: z.object({}),
}];
}
@Tool("make_plan")
async makePlan(): Promise<ToolResponse> {
const city = this.graph.graphState().nodes.CityNode?.city;
if (!city) return stay("A valid destination must be saved first.");
const plan = `Trip to ${city}: choose your dates, then book transport.`;
this.saveState({ plan });
return finish(plan);
}
}
export class StarterGraph extends BaseGraph<StarterStateType> {
static getGraphDefinition(): GraphDefinition {
return {
llmConfig: ModelCatalog.model("scripted:scripted-model", { retries: 0 }),
endNode: GRAPH_END_NODE,
historySpaces: [[CityNode, "intake"], [SummaryNode, "summary"]],
};
}
constructor(gateway: LlmGateway) {
super(gateway, StarterGraph.getGraphDefinition());
}
protected buildGraph() {
const graph = this.createStateGraph(StarterState);
graph.registerTurnNodes(CityNode, SummaryNode, TerminateSessionNode);
graph.addEdge(TerminateSessionNode, END);
return graph.compile();
}
}
Download this file · Explore all response builders · Read the lifecycle contract
The entrypoint drives two real turns.
import "reflect-metadata";
import { createTurnHarness, scriptedGateway } from "@picoflow/ezgraph/testing";
import { StarterGraph, type StarterStateType } from "./workflow.js";
// Script the model's responses; the real graph, tools and session engine run.
const gateway = scriptedGateway()
.text("Which city would you like to visit?")
.callsTool("capture_city", { city: "Lisbon" })
.callsTool("make_plan");
const harness = createTurnHarness<StarterStateType>({
graph: StarterGraph, gateway, sessionId: "first-workflow",
});
try {
console.log("You: Hi");
console.log("EZGraph:", (await harness.send("Hi")).response);
console.log("You: Lisbon");
const result = await harness.send("Lisbon");
console.log("EZGraph:", result.response);
console.log("Completed:", result.completed);
console.log("\nSession document:");
console.log(JSON.stringify(result.document, null, 2));
} finally {
await harness.close();
}
The first turn returns a question and keeps CityNode active. The second captures Lisbon, enters SummaryNode, and completes. The printed document contains both node channels and the two history spaces.
Test the application boundary.
Run npm test. The included tests exercise complete turns through createTurnHarness(), with an in-memory session store.
- A second turn restores the active node and saves both channels.
- A schema-invalid argument and a domain-invalid city are repaired before advancing.
- The city stage offers
capture_city; the summary stage offersmake_plan.
Change the city, the validation rule, or the code-owned trip card, then update the expected result. Inspect gateway.calls to see the actual tools offered by each stage.
Connect a model and your application.
Keep the node and tool contracts. Replace the scripted graph model with a catalog model for your provider, and replace the testing harness with GraphEngine.create() and its provider adapters. The developer guide shows the supported initialization contract.
Choose model policy.
Set graph defaults, node overrides, and recovery behavior. Configure your own provider credentials.
Models and recovery →Choose session storage.
Compose selected database client factories in your application entrypoint. Configure ownership and shutdown with the engine.
Application database initialization →Then try a complete application.
QuoteGraph adds staged validation, catalog disambiguation, revisions, rating, and explicit acceptance. The workflow examples show extraction and reviewed presentations using the same application layer.
Give your next LangGraph application a consistent foundation.
Run the starter, inspect the session, and decide whether the node contract fits your team.