How to Fix 'state not updating in production' in CrewAI (TypeScript)
When CrewAI state updates work locally but stop changing in production, the bug is usually not in the agent logic. It’s almost always a mismatch between how state is stored, when it’s mutated, or whether the runtime is actually preserving the same object across steps.
In TypeScript projects, this shows up after deployment to serverless, containerized, or multi-instance environments. You’ll see symptoms like state not updating, stale tool output, or CrewExecutionError wrapping a task that clearly ran but never persisted its state.
The Most Common Cause
The #1 cause is mutating a nested state object in place and expecting CrewAI to detect it across task boundaries.
CrewAI’s TypeScript runtime is much happier when you treat state as immutable and return a new object from each step. If you mutate state.currentStep.result = ... or push into arrays directly, local dev may appear fine, but production builds often serialize, clone, or isolate execution contexts differently.
Broken vs fixed pattern
| Broken pattern | Fixed pattern |
|---|---|
| Mutates existing state in place | Returns a new state object |
| Relies on reference identity | Uses explicit assignment |
| Works accidentally in local runs | Survives production execution boundaries |
// BROKEN: in-place mutation
import { Crew, Agent, Task } from "@crewai/typescript";
type AppState = {
messages: string[];
lastResult?: string;
};
const task = new Task({
description: "Summarize the customer request",
agent: new Agent({ role: "Analyst", goal: "Summarize input" }),
execute: async ({ state }: { state: AppState }) => {
state.messages.push("task started");
state.lastResult = "summary text";
return { output: "done", state };
},
});
// FIXED: immutable update
import { Crew, Agent, Task } from "@crewai/typescript";
type AppState = {
messages: string[];
lastResult?: string;
};
const task = new Task({
description: "Summarize the customer request",
agent: new Agent({ role: "Analyst", goal: "Summarize input" }),
execute: async ({ state }: { state: AppState }) => {
const nextState: AppState = {
...state,
messages: [...state.messages, "task started"],
lastResult: "summary text",
};
return { output: "done", state: nextState };
},
});
If your code uses CrewExecutionError, inspect the wrapped cause. In a lot of cases the error isn’t “state failed,” it’s that the updated object never got returned to the runner.
Other Possible Causes
1) You are using a non-persistent in-memory store
If your app runs on Vercel functions, AWS Lambda, or multiple containers behind a load balancer, memory does not survive between invocations.
// BAD for production persistence
let sharedState = { count: 0 };
export async function handler() {
sharedState.count += 1;
return sharedState;
}
Use Redis, Postgres, DynamoDB, or another external store if the workflow needs durable updates.
// GOOD: fetch and save explicitly
const current = await redis.get("crew-state");
const next = { ...(current ? JSON.parse(current) : {}), count: 1 };
await redis.set("crew-state", JSON.stringify(next));
2) Your task returns output but not the updated state
A common CrewAI TypeScript mistake is assuming output automatically carries your changes forward. It doesn’t unless your workflow explicitly wires it that way.
// BROKEN
return { output: result };
// state changes are lost
// FIXED
return {
output: result,
state: nextState,
};
3) A schema mismatch is dropping fields during serialization
If you validate with Zod or another schema and forget to include a field, production builds often strip it while local debug logs still show the raw object.
import { z } from "zod";
const StateSchema = z.object({
messages: z.array(z.string()),
// missing lastResult here means it may get dropped/ignored
});
Fix the schema so every persisted field is declared.
const StateSchema = z.object({
messages: z.array(z.string()),
lastResult: z.string().optional(),
});
4) Async race conditions are overwriting newer state
If two tasks read the same base state and both write back later, the second write can clobber the first. This happens a lot with parallel tool calls.
const s1 = await loadState();
const s2 = await loadState();
s1.counter += 1;
s2.counter += 1;
await saveState(s1);
await saveState(s2); // overwrites s1 update
Use versioning or optimistic locking.
await saveState(nextState, { expectedVersion });
How to Debug It
- •
Log before and after every task
- •Print
stateat task entry. - •Print the returned value right before exit.
- •Confirm whether the mutation exists in memory but disappears after return.
- •Print
- •
Check whether production is serverless or multi-instance
- •If yes, assume memory is ephemeral.
- •Verify that your storage layer is external and writable.
- •
Inspect the actual wrapped error
- •Search for
CrewExecutionError. - •Log
error.cause,error.stack, and any serialized runner output. - •A hidden Zod parse failure or tool exception often looks like “state not updating.”
- •Search for
- •
Disable parallelism temporarily
- •Run tasks sequentially.
- •If the bug disappears, you likely have a race condition or overwrite problem.
Prevention
- •Treat CrewAI state as immutable.
- •Return
{ output, state }explicitly from every task that modifies workflow data. - •Persist important workflow data outside process memory if production can scale horizontally.
- •Add tests that assert both task output and final serialized state.
- •Validate your full state schema before and after each step so missing fields fail fast instead of disappearing silently.
If you’re seeing state not updating in production with CrewAI TypeScript, start with immutable returns and persistent storage first. In real deployments, those two fixes solve most of these bugs before you ever touch agent prompts or model settings.
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