CrewAI Tutorial (TypeScript): persisting agent state for advanced developers
This tutorial shows how to persist CrewAI agent state in a TypeScript app so your agents can resume with memory, previous task context, and durable run metadata. You need this when you’re building long-running workflows for support, claims, underwriting, or any process where losing context between runs creates bad outputs and manual rework.
What You'll Need
- •Node.js 20+
- •TypeScript 5+
- •A CrewAI-compatible TypeScript project
- •
@crewai/crewai - •
zod - •An LLM API key configured for your provider
- •A persistence layer:
- •local JSON files for development, or
- •PostgreSQL / Redis / S3 in production
- •Basic familiarity with:
- •agents
- •tasks
- •crews
- •async/await
Step-by-Step
- •Start with a small project and install the packages you need. For this tutorial, we’ll persist agent state to a JSON file so the pattern is easy to inspect and replace later with a database.
npm init -y
npm install @crewai/crewai zod
npm install -D typescript tsx @types/node
- •Create a typed state model and persistence helpers. Keep the state shape explicit so you can evolve it safely when you add more agents or more workflow steps.
// state.ts
import { promises as fs } from "node:fs";
export type AgentState = {
agentId: string;
runCount: number;
lastSummary: string;
updatedAt: string;
};
const STATE_FILE = "./agent-state.json";
export async function loadState(): Promise<AgentState> {
try {
const raw = await fs.readFile(STATE_FILE, "utf8");
return JSON.parse(raw) as AgentState;
} catch {
return {
agentId: "claims-agent",
runCount: 0,
lastSummary: "",
updatedAt: new Date().toISOString(),
};
}
}
export async function saveState(state: AgentState): Promise<void> {
await fs.writeFile(STATE_FILE, JSON.stringify(state, null, 2), "utf8");
}
- •Build an agent that consumes persisted context on every run. The important part is that the prior summary gets injected into the prompt so the model can continue from where it left off instead of starting fresh.
// crew.ts
import { Agent, Task, Crew } from "@crewai/crewai";
import { loadState, saveState } from "./state";
export async function runCrew() {
const state = await loadState();
const agent = new Agent({
role: "Claims Analyst",
goal: "Review customer claim notes and produce a concise next-action summary",
backstory:
"You handle insurance claims and must preserve continuity across sessions.",
verbose: true,
allowDelegation: false,
llm: "openai/gpt-4o-mini",
});
const task = new Task({
description: `
Current persisted summary:
${state.lastSummary || "No previous summary."}
Analyze the latest claim update and produce:
1. a short summary
2. next action items
3. any open risk flags
`,
expectedOutput: "A structured claim follow-up note.",
agent,
});
const crew = new Crew({
agents: [agent],
tasks: [task],
verbose: true,
});
const result = await crew.kickoff();
return { result, state };
}
- •Wrap execution with a post-run persistence step. This is where you turn CrewAI from stateless orchestration into something that can survive retries, human handoffs, and scheduled reruns.
// index.ts
import { runCrew } from "./crew";
import { loadState, saveState } from "./state";
async function main() {
const { result } = await runCrew();
const state = await loadState();
const nextState = {
...state,
runCount: state.runCount + 1,
lastSummary: String(result),
updatedAt: new Date().toISOString(),
};
await saveState(nextState);
console.log("Persisted state:", nextState);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
- •Add a simple retry-safe pattern for production use. If your crew crashes after generating output but before persistence completes, you want an idempotent write strategy rather than blind overwrites.
// durable-write.ts
import { promises as fs } from "node:fs";
export async function atomicWriteJson(path: string, data: unknown) {
const tempPath = `${path}.tmp`;
const payload = JSON.stringify(data, null, 2);
await fs.writeFile(tempPath, payload, "utf8");
await fs.rename(tempPath, path);
}
Then swap saveState to use atomicWriteJson:
// state.ts excerpt
import { atomicWriteJson } from "./durable-write";
export async function saveState(state: AgentState): Promise<void> {
await atomicWriteJson(STATE_FILE, state);
}
Testing It
Run the app twice and compare the output in agent-state.json. The second execution should show runCount incrementing and the prior lastSummary being injected into the task prompt.
Use different input text in your task description or wire in real upstream data from a ticketing system. If the agent references earlier context correctly on the second run, your persistence layer is doing its job.
For a stronger test, kill the process after the crew returns but before persistence completes. With atomic writes in place, you should never end up with a half-written state file.
Next Steps
- •Move
AgentStateinto PostgreSQL with versioned rows so multiple workers can update safely. - •Persist richer artifacts like tool outputs, retrieved documents, and human review decisions.
- •Add conversation-style memory per customer or case ID instead of one global state file.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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