CrewAI Tutorial (TypeScript): persisting agent state for intermediate developers

By Cyprian AaronsUpdated 2026-04-21
crewaipersisting-agent-state-for-intermediate-developerstypescript

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
  • crewai installed 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

  1. 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],
});
  1. 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(),
  };
}
  1. 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;
}
  1. 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;
  }
}
  1. 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;
}
  1. 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 zod before 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

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