CrewAI Tutorial (TypeScript): persisting agent state for beginners

By Cyprian AaronsUpdated 2026-04-21
crewaipersisting-agent-state-for-beginnerstypescript

This tutorial shows you how to persist CrewAI agent state in a TypeScript app so a run can be resumed, inspected, or used as context for the next turn. You need this when your agent is doing multi-step work across requests, or when you cannot afford to lose progress if the process restarts.

What You'll Need

  • Node.js 18+
  • A TypeScript project with ts-node or a build step
  • crewai installed in your project
  • An OpenAI API key exported as OPENAI_API_KEY
  • Optional but useful:
    • dotenv for local env loading
    • a writable folder for storing JSON state on disk

Install the packages:

npm install crewai dotenv
npm install -D typescript ts-node @types/node

Step-by-Step

  1. Start with a minimal CrewAI setup and define where state will live. For beginners, the simplest persistence layer is a JSON file on disk. That gives you something inspectable before moving to Redis, Postgres, or S3.
import "dotenv/config";
import { Agent, Task, Crew, Process } from "crewai";
import { readFileSync, writeFileSync, existsSync } from "node:fs";

type AgentState = {
  runId: string;
  lastOutput?: string;
  notes?: string[];
};

const STATE_FILE = "./agent-state.json";

function loadState(): AgentState {
  if (!existsSync(STATE_FILE)) {
    return { runId: crypto.randomUUID(), notes: [] };
  }
  return JSON.parse(readFileSync(STATE_FILE, "utf8")) as AgentState;
}

function saveState(state: AgentState) {
  writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}
  1. Create an agent and task that will produce output you can store. The important part is not the task itself; it is that you capture the result immediately after execution and write it back to your persistence layer.
const researcher = new Agent({
  role: "Research Assistant",
  goal: "Summarize the user's topic clearly",
  backstory: "You write concise summaries for internal teams.",
});

const task = new Task({
  description: "Summarize why persisting agent state matters in one paragraph.",
  expectedOutput: "A short practical explanation.",
  agent: researcher,
});

const crew = new Crew({
  agents: [researcher],
  tasks: [task],
  process: Process.sequential,
});
  1. Run the crew, then persist the output into your state object. This is the core pattern: execute, capture result, update state, save state.
async function main() {
  const state = loadState();

  const result = await crew.kickoff();
  const output = String(result);

  state.lastOutput = output;
  state.notes ??= [];
  state.notes.push(`Run ${state.runId} completed at ${new Date().toISOString()}`);

  saveState(state);

  console.log("Crew output:");
  console.log(output);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
  1. Use the saved state as input on the next run. In real systems this is where persistence becomes useful: you can pass prior outputs into new tasks instead of starting from zero every time.
async function followUpRun() {
  const state = loadState();

  const followUpTask = new Task({
    description: `Based on this previous output, improve it:\n\n${state.lastOutput ?? ""}`,
    expectedOutput: "A refined version of the previous summary.",
    agent: researcher,
  });

  const followUpCrew = new Crew({
    agents: [researcher],
    tasks: [followUpTask],
    process: Process.sequential,
  });

  const result = await followUpCrew.kickoff();
  state.lastOutput = String(result);
  saveState(state);

  console.log(String(result));
}
  1. If you want cleaner structure, separate persistence from orchestration. That keeps your CrewAI code focused on behavior and makes it easier to swap JSON files for a database later.
class JsonStateStore {
  constructor(private path: string) {}

  load(): AgentState {
    if (!existsSync(this.path)) return { runId: crypto.randomUUID(), notes: [] };
    return JSON.parse(readFileSync(this.path, "utf8")) as AgentState;
    }

  save(state: AgentState) {
    writeFileSync(this.path, JSON.stringify(state, null, 2));
    }
}

const store = new JsonStateStore(STATE_FILE);

Testing It

Run the script once and confirm that agent-state.json is created in your project root. The file should contain a runId, a lastOutput, and at least one note entry after execution.

Run it again and check that the same runId is reused while lastOutput changes based on the latest run. That tells you your app is reading prior state instead of generating fresh state every time.

If you use the followUpRun() pattern, verify that the second task includes text from the first output. That proves your persisted data is actually feeding future agent behavior.

Next Steps

  • Replace the JSON file with Redis if you need shared state across instances.
  • Add per-user keys so each customer gets isolated agent memory.
  • Persist more than text:
    • task status
    • timestamps
    • tool results

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