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

State, context, and history

Configure session persistence, choose the initial node, route history, and manage durable node state and graph context.

Application-owned database initialization

Create database SDK clients in your application’s startup code, alongside graph and model registration. In a Nest application, this is app.module.ts. ezgraph-demo’s AppModule passes inline sessionClients factories to GraphEngine.create(). The application reads its connection settings and chooses authentication; EZGraph adapts the selected client to its session document store.

Install the SDKs your application imports:

npm install mongodb @azure/cosmos @azure/identity

The following Nest example keeps the demo’s database initialization convention, with only QuoteGraph and its OpenAI model registered:

import { Inject, Module, type OnApplicationShutdown } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { CosmosClient } from "@azure/cosmos";
import { ClientSecretCredential, DefaultAzureCredential } from "@azure/identity";
import { MongoClient } from "mongodb";
import { GraphEngine, ModelProvider } from "@picoflow/ezgraph";
import { QuoteGraph } from "./graphs/quote-graph/quote-graph.js";

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true })],
  providers: [{
    provide: GraphEngine,
    inject: [ConfigService],
    useFactory: (config: ConfigService) => GraphEngine.create({
      configManager: config,
      graphs: [QuoteGraph],
      providers: ModelProvider.createBuiltinAdapters({
        openai: { apiKey: config.get<string>("OPENAI_API_KEY") },
      }),
      sessionClients: {
        mongodb: () => {
          const url = config.getOrThrow<string>("MONGODB_URL");
          const tlsCAFile = config.get<string>("MONGODB_TLS_CA_FILE");
          return new MongoClient(url, tlsCAFile ? { tlsCAFile } : {});
        },
        cosmos: () => {
          const endpoint = config.get<string>("COSMO_ENDPOINT")
            || config.get<string>("COSMOS_ENDPOINT")
            || config.get<string>("COSMODB_URL");
          if (!endpoint) throw new Error("Cosmos endpoint is required.");
          const key = config.get<string>("COSMODB_KEY")
            || config.get<string>("COSMOS_KEY");
          if (key) return new CosmosClient({ endpoint, key });

          const tenantId = config.get<string>("AZURE_TENANT_ID");
          const clientId = config.get<string>("COSMO_DB_CLIENT_ID");
          const clientSecret = config.get<string>("COSMO_DB_CLIENT_SECRET");
          if (clientId || clientSecret) {
            if (!tenantId || !clientId || !clientSecret) {
              throw new Error("Cosmos service principal requires tenant, client ID, and secret.");
            }
            return new CosmosClient({
              endpoint,
              aadCredentials: new ClientSecretCredential(tenantId, clientId, clientSecret),
            });
          }
          return new CosmosClient({ endpoint, aadCredentials: new DefaultAzureCredential() });
        },
      },
    }),
  }],
})
export class AppModule implements OnApplicationShutdown {
  constructor(@Inject(GraphEngine) private readonly engine: GraphEngine) {}

  async onApplicationShutdown(): Promise<void> {
    await this.engine.close();
  }
}

Nest’s ConfigModule loads .env. Enable shutdown hooks with app.enableShutdownHooks() in main.ts so process signals run the cleanup hook. For a standalone application, pass a ConfigManager as configManager and put the same factories in the GraphEngine.create() call in your entry point.

SESSION_STORE selects the backend. Only its factory runs: selecting cosmos does not construct a MongoDB client or require MONGODB_URL. Each factory takes no arguments and may return a client directly or asynchronously. Read secrets and SDK options inside that factory so configuration for an unused backend is not required at startup.

Backend Session configuration read by EZGraph
memory (default) No database client; sessions last for the process lifetime.
sqlite SQLITE_DB_PATH, default ./data/sessions.sqlite.
mongodb (alias mongo) Required MONGODB_NAME and MONGODB_COLLECTION. The application factory above reads MONGODB_URL and optional MONGODB_TLS_CA_FILE.
cosmos (alias cosmosdb) COSMO_DB_ID, default langgraph; COSMO_DB_SESSION_CONTAINER_ID, default sessions. The application factory above reads the endpoint and credentials.

For MongoDB, a minimal local configuration is:

SESSION_STORE=mongodb
MONGODB_URL=mongodb://localhost:27017
MONGODB_NAME=ezgraph
MONGODB_COLLECTION=sessions

MONGODB_TLS_CA_FILE is an optional path to a trusted CA certificate file, passed as the MongoDB SDK’s tlsCAFile option. Set it when your deployment requires a custom CA, such as a DocumentDB connection configured that way. Keep connection, TLS, and authentication options in the application factory.

For a pre-provisioned Cosmos database and container using key authentication:

SESSION_STORE=cosmos
COSMO_ENDPOINT=https://your-account.documents.azure.com:443/
COSMODB_KEY=your-account-key
COSMO_DB_ID=ezgraph
COSMO_DB_SESSION_CONTAINER_ID=sessions
COSMOS_CREATE_IF_NOT_EXISTS=false

The example factory uses a key first. For an explicit service principal, omit the key and set AZURE_TENANT_ID, COSMO_DB_CLIENT_ID, and COSMO_DB_CLIENT_SECRET. With neither a key nor explicit client credentials, it uses DefaultAzureCredential. This authentication choice belongs to your application, so you can adapt it to your company’s credential conventions.

EZGraph connects the MongoDB client and ensures a unique session id index. For Cosmos, it validates that the container’s partition key is /id. COSMOS_CREATE_IF_NOT_EXISTS defaults to true; the framework then creates the database and container if needed, using COSMOS_THROUGHPUT (default 400). Set it to false when resources are provisioned separately, and grant the chosen identity the permissions needed for the session operations.

Factory-created clients are owned by the engine by default. engine.close() closes the MongoDB client or disposes the Cosmos client; initialization failures also release owned clients. If a factory returns a client shared elsewhere in your application, set sessionClients.ownsClients: false and close that client through your application’s own lifecycle.

Factories are optional: without one for the selected backend, EZGraph retains its built-in configuration-based client creation. Use application-owned factories when you need explicit authentication or SDK connection options.

Initial node and history routing

The first argument to createGraphStateAnnotation() declares the initial currentNode. In the node contract example, a new session starts at DriverNode. Map that node to a named history in the graph definition:

static getGraphDefinition(): GraphDefinition {
  return {
    llmConfig: ModelCatalog.model("openai:gpt-5.4", { retries: 2 }),
    endNode: GRAPH_END_NODE,
    historySpaces: [
      [DriverNode, "quote-intake"],
      [VehicleNode, "quote-intake"],
    ],
  };
}

Before invoking a node, BaseGraph.prepareInput() resolves the cursor, chooses its history space, and appends the customer’s message there. The START branch created by registerTurnNodes() then enters that same node.

Session Cursor used for input History space
New session The state schema’s initial currentNode, here DriverNode.id() The initial node’s mapping, here "quote-intake"
Restored session The saved currentNode The saved node’s mapping
Session reset by onRestoreSessionDoc() returning null The state schema’s initial currentNode The initial node’s mapping
Any selected node without a mapping The cursor resolved above "default"

Node registration order does not choose the initial cursor. Keep the initial node registered as a turn node, and supply a non-empty cursor default; preparing a first message without that default fails before model work begins.

For complete examples, see QuoteGraph’s history spaces and DecisionHotelGraph’s history spaces.

Graph-wide runtime context

Use context for runtime information shared across stages. New graph states start with {}, and the session document saves it under graph.context. For example, a QuoteGraph session can contain this fragment:

{
  "graph": {
    "id": "QuoteGraph",
    "context": {
      "rating": {
        "calculatedAt": "2027-06-01T10:00:00.000Z",
        "businessDate": "2027-06-01"
      }
    }
  }
}

During an active node invocation:

this.graph.saveContext({
  rating: { calculatedAt: now.toISOString() },
});
const context = this.graph.getContext();

BaseGraph exposes the same methods for graph-owned policies. saveContext() replaces the supplied top-level branches while preserving unrelated branches. Writing rating again replaces that entire subtree; nested objects and arrays are not deep-merged. null remains an ordinary JSON value.

The root is a JSON object; values may be nested objects, arrays, strings, finite numbers, booleans, or null. The framework rejects undefined, functions, dates, circular references, and other non-JSON values. Convert dates to ISO strings. Writes are cloned; reads are detached, deeply frozen snapshots.

A successful node outcome publishes staged context to the next node and the normal session checkpoint. If the node throws, its staged changes are discarded; previously completed checkpoints retain theirs. Concurrent sessions are isolated. A parallel superstep permits one context writer. Internal LlmNode workers may read graph context but cannot write it, even when they execute sequentially or in a nested call. Consolidate their local output in a conversation-owning or deterministic join node before updating shared context.

createGraphStateAnnotation() declares the context channel. Custom Annotation.Root() schemas can import GraphContextChannel from @picoflow/ezgraph and declare context: new GraphContextChannel(). Outside execution, inspect saved context in the session returned by GraphEngine.getSession().

In QuoteGraph, coverage calculation and quote adjustment write rating timestamps. Acceptance reads that shared metadata and saves an acceptance timestamp. Business facts remain in node state, request options remain in config, and context is included in prompts only when application code explicitly adds it. See the QuoteGraph context example.

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. A conversational or deterministic owner can use graph.saveNodeState() for another node’s channel. An internal LlmNode may write only its own channel and token usage.

@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);
}

The node publishes the tool’s name, description, and zod schema from defineTool(); @Tool(name) binds the handler. Arguments are schema-validated before the handler runs, and invalid arguments return { accepted: false, error } to the model instead of throwing.

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.

With conditional fan-out, save parent-owned input before returning fanout(Child1Node, Child2Node). Each worker supplies explicitly selected facts through getPrompt(state) or constructs a task message in onEnter(), then saves accepted output in onResponse(). Its model history is ephemeral, not a copy of a named conversation history. The parent remains the durable conversation cursor until the joined conversational stage responds. See execution ownership and fan-out and join.