LangGraph Tutorial (TypeScript): persisting agent state for advanced developers

By Cyprian AaronsUpdated 2026-04-22
langgraphpersisting-agent-state-for-advanced-developerstypescript

This tutorial shows how to persist LangGraph agent state in TypeScript so your agent can resume conversations, recover after process restarts, and keep per-user memory across requests. You need this when an agent is no longer a one-shot demo and starts behaving like a real service with session continuity.

What You'll Need

  • Node.js 18+
  • TypeScript 5+
  • @langchain/langgraph
  • @langchain/openai
  • dotenv
  • An OpenAI API key
  • A local SQLite database file for persistence
  • Basic familiarity with LangGraph nodes, edges, and state

Step-by-Step

  1. Start with a project that has a typed state and a checkpointer. The checkpointer is what makes state durable between runs, so every invocation can reload the latest thread state instead of starting from zero.
npm init -y
npm install @langchain/langgraph @langchain/openai dotenv
npm install -D typescript tsx @types/node
  1. Define your graph state and the model node. In this example, we store chat messages in state and bind the graph to a thread ID so each user session gets its own persisted history.
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";
import {
  Annotation,
  MessagesAnnotation,
  StateGraph,
  START,
  END,
} from "@langchain/langgraph";

const State = Annotation.Root({
  messages: MessagesAnnotation.spec,
});

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0,
});

async function callModel(state: typeof State.State) {
  const response = await model.invoke(state.messages);
  return { messages: [response] };
}
  1. Add persistence with a checkpointer. For local development, SQLite is the easiest option because it survives process restarts and gives you real thread-level memory without extra infrastructure.
import { SqliteSaver } from "@langchain/langgraph-checkpoint-sqlite";

const checkpointer = await SqliteSaver.fromConnString("file:./langgraph.db");

const graph = new StateGraph(State)
  .addNode("agent", callModel)
  .addEdge(START, "agent")
  .addEdge("agent", END)
  .compile({ checkpointer });
  1. Invoke the graph with a stable thread_id. This is the key detail: if you reuse the same thread ID, LangGraph loads prior state from SQLite and appends to it instead of creating a fresh conversation.
import { HumanMessage } from "@langchain/core/messages";

const config = {
  configurable: {
    thread_id: "customer-123",
  },
};

const first = await graph.invoke(
  { messages: [new HumanMessage("My policy number is P-44821. Remember it.")] },
  config
);

console.log(first.messages.at(-1)?.content);
  1. Run another turn against the same thread. The model now sees the prior persisted messages, so it can answer using context from earlier turns even though this is a separate process call.
const second = await graph.invoke(
  { messages: [new HumanMessage("What policy number did I give you?")] },
  config
);

console.log(second.messages.at(-1)?.content);
  1. Inspect or branch state when you need advanced behavior. In production, this is where you build features like human approval flows, retries after partial execution, or session replay for support teams.
const snapshot = await graph.getState(config);

console.log("Current values:", snapshot.values);
console.log("Next nodes:", snapshot.next);

await graph.updateState(config, {
  messages: [
    ...snapshot.values.messages,
    new HumanMessage("Override remembered policy number to P-99999"),
  ],
});

Testing It

Run the script twice or keep both invocations in one file and execute it once. The second turn should answer using the earlier message because both calls share the same thread_id. If you stop Node and run again with the same SQLite file, the conversation should still resume from disk.

If it does not persist, check three things first: the SQLite file path exists, checkpointer is passed into .compile(), and thread_id stays stable across invocations. If you change any of those per request, you are telling LangGraph to start a new session.

A good sanity test is to delete langgraph.db, rerun the script, then compare behavior before and after recreating it. That confirms your persistence layer is actually responsible for continuity rather than in-memory runtime state.

Next Steps

  • Add branching logic with conditional edges so persisted sessions can take different paths based on stored state.
  • Move from SQLite to Postgres when you need multi-instance deployment and stronger operational guarantees.
  • Store structured business fields in state alongside messages so your agent can persist claims data, case status, or underwriting inputs cleanly.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides