CrewAI Tutorial (TypeScript): persisting agent state for intermediate developers
This tutorial shows you how to persist CrewAI agent state in a TypeScript app so a run can survive process restarts and keep context across intermediate steps. You need this when your agent workflow spans multiple requests, long-running tasks, or a human approval loop and you cannot afford to lose memory between executions.
What You'll Need
- •Node.js 18+
- •TypeScript 5+
- •A CrewAI TypeScript project already set up
- •
crewaiinstalled in your project - •An LLM provider API key, such as:
- •
OPENAI_API_KEY
- •
- •A place to persist state:
- •local JSON file for development
- •Redis, Postgres, or DynamoDB for production
- •Basic familiarity with:
- •
Agent - •
Task - •
Crew
- •
Step-by-Step
- •Start with a small project that has a single agent and task. We’ll add persistence around the run state instead of trying to persist the model itself.
npm install crewai
npm install -D typescript tsx @types/node
import { Agent, Task, Crew } from "crewai";
const analyst = new Agent({
name: "ClaimsAnalyst",
role: "Insurance claims analyst",
goal: "Summarize claim details clearly",
backstory: "You review incoming claims and extract structured notes.",
});
const task = new Task({
description: "Summarize the claim notes from the input text.",
expectedOutput: "A concise summary with key fields.",
agent: analyst,
});
const crew = new Crew({
agents: [analyst],
tasks: [task],
});
- •Define a serializable state object. Keep it boring: run ID, input payload, current step, outputs, and timestamps are enough for most production flows.
type RunState = {
runId: string;
input: string;
status: "pending" | "running" | "completed" | "failed";
currentStep: number;
outputs: string[];
updatedAt: string;
};
function createInitialState(input: string): RunState {
return {
runId: crypto.randomUUID(),
input,
status: "pending",
currentStep: 0,
outputs: [],
updatedAt: new Date().toISOString(),
};
}
- •Persist that state to disk for local development. In production, swap these functions for Redis or a database table with the same shape.
import { promises as fs } from "node:fs";
import path from "node:path";
const STATE_DIR = path.join(process.cwd(), ".crew-state");
async function saveState(state: RunState): Promise<void> {
await fs.mkdir(STATE_DIR, { recursive: true });
const filePath = path.join(STATE_DIR, `${state.runId}.json`);
await fs.writeFile(filePath, JSON.stringify(state, null, 2), "utf8");
}
async function loadState(runId: string): Promise<RunState> {
const filePath = path.join(STATE_DIR, `${runId}.json`);
const raw = await fs.readFile(filePath, "utf8");
return JSON.parse(raw) as RunState;
}
- •Wrap the CrewAI execution with state transitions. Save before execution starts, update after each step completes, and mark failures explicitly so you can resume or inspect later.
async function executeWithPersistence(input: string) {
const state = createInitialState(input);
state.status = "running";
state.updatedAt = new Date().toISOString();
await saveState(state);
try {
const result = await crew.kickoff({
inputs: { claimText: input },
});
state.outputs.push(String(result));
state.currentStep += 1;
state.status = "completed";
state.updatedAt = new Date().toISOString();
await saveState(state);
return state;
} catch (error) {
state.status = "failed";
state.outputs.push(`ERROR: ${(error as Error).message}`);
state.updatedAt = new Date().toISOString();
await saveState(state);
throw error;
}
}
- •Add a resume path so your app can continue from an existing run ID. This is the part most people miss; persistence is only useful if you can reload context and decide what happens next.
async function resumeRun(runId: string) {
const state = await loadState(runId);
if (state.status === "completed") {
return state;
}
const result = await crew.kickoff({
inputs: {
claimText: state.input,
previousNotes: state.outputs.join("\n"),
stepIndex: String(state.currentStep),
},
});
state.outputs.push(String(result));
state.currentStep += 1;
state.status = "completed";
state.updatedAt = new Date().toISOString();
await saveState(state);
return state;
}
- •Wire it into a runnable entry point and test both fresh runs and resumes. Keep the CLI thin so your persistence logic stays reusable in an API route or worker later.
async function main() {
const mode = process.argv[2];
const value = process.argv.slice(3).join(" ");
if (!process.env.OPENAI_API_KEY) {
throw new Error("OPENAI_API_KEY is required");
}
if (mode === "start") {
const saved = await executeWithPersistence(value);
console.log("Saved run:", saved.runId);
console.log(saved);
return;
}
if (mode === "resume") {
const resumed = await resumeRun(value);
console.log("Resumed run:", resumed.runId);
console.log(resumed);
return;
}
throw new Error('Use either "start <text>" or "resume <runId>"');
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
Testing It
Run a fresh execution first:
OPENAI_API_KEY=your_key npx tsx src/index.ts start "Customer reported water damage in kitchen"
You should see a runId printed and a JSON file created under .crew-state/. Open that file and confirm it contains status, currentStep, outputs, and timestamps.
Then rerun using the saved ID:
OPENAI_API_KEY=your_key npx tsx src/index.ts resume <run-id>
If your flow is working, the app will reload prior context instead of starting from scratch. In a real service, also verify that failed runs are marked failed and that retries do not overwrite successful output unless you intend them to.
Next Steps
- •Replace file storage with Redis or Postgres so multiple workers can share agent state.
- •Add schema validation with
zodbefore writing or loading persisted runs. - •Split long workflows into multiple tasks and persist after each task boundary instead of only at the end.
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