Skip to content

Caching

Cache step results so they don’t re-execute if the workflow runs again.

Pass a cache to the workflow:

const workflow = createWorkflow('workflow', deps, {
cache: new Map(),
});

Give steps a key to enable caching:

const result = await workflow.run(async ({ step, deps }) => {
// This step is cached with key 'user:1'
const user = await step('fetchUser', () => deps.fetchUser('1'), { key: 'user:1' });
// Subsequent calls with same key return cached value
const sameUser = await step('fetchUser', () => deps.fetchUser('1'), { key: 'user:1' });
return user;
});

Keys must be:

  • Unique per step: Different steps need different keys
  • Stable: Same input should produce same key
  • Deterministic: Don’t use timestamps or random values
  • Scoped: Prefer step-scoped keys to avoid collisions (e.g. fetchUser:${id}, user:${id}, or fetchUser:user:${id})
  • Short-ish: Keys appear in logs, events, and snapshots

The step’s first argument is the label (category); the key is the instance identity (per iteration or entity).

// Good keys
{ key: 'user:123' }
{ key: `posts:${userId}` }
{ key: `order:${orderId}:validate` }
{ key: `fetchUser:${id}` } // step-scoped
// Bad keys
{ key: `user:${Date.now()}` } // Changes every call
{ key: `user:${Math.random()}` } // Random

Pass a function, not the result of calling the function:

// Without thunk - executes immediately, caching ignored
const user = await step('fetchUser', () => deps.fetchUser('1'), { key: 'user:1' });
// With thunk - can be cached
const user = await step('fetchUser', () => deps.fetchUser('1'), { key: 'user:1' });

step.run — With createWorkflow, use a getter so the step only runs on cache miss: step.run('fetchUser', () => fetchUser('1'), { key: 'user:1' }). If you pass the promise directly, it is created at call site and caching cannot skip the work.

step.all and step.map — They only use the cache when you pass an explicit key. If you omit options or omit key, the step is not cached (no cache by step id). This matches core run() semantics.

// Cached (explicit key)
await step.all('fetchAll', { user: () => deps.fetchUser('1') }, { key: 'fetch:1' });
await step.map('fetchUsers', ids, deps.fetchUser, { key: 'users:batch' });
// Not cached (no key) — runs every time
await step.all('fetchAll', { user: () => deps.fetchUser('1') });
await step.map('fetchUsers', ids, deps.fetchUser);

The cache persists across workflow runs:

const cache = new Map();
const workflow = createWorkflow('workflow', deps, { cache });
// First run - fetches user
await workflow.run(async ({ step, deps }) => {
const user = await step('fetchUser', () => deps.fetchUser('1'), { key: 'user:1' });
return user;
});
// Second run - uses cached value
await workflow.run(async ({ step, deps }) => {
const user = await step('fetchUser', () => deps.fetchUser('1'), { key: 'user:1' });
return user; // No fetch - returns cached value
});

Clear specific keys or the entire cache:

const cache = new Map();
// Clear one key
cache.delete('user:1');
// Clear all
cache.clear();

Any object with Map-like get, set, has, delete methods works:

// Redis-backed cache
const redisCache = {
async get(key: string) {
const value = await redis.get(key);
return value ? JSON.parse(value) : undefined;
},
async set(key: string, value: unknown) {
await redis.set(key, JSON.stringify(value));
},
async has(key: string) {
return await redis.exists(key) > 0;
},
async delete(key: string) {
return await redis.del(key) > 0;
},
};
const workflow = createWorkflow('workflow', deps, { cache: redisCache });

Errors are cached by default. If a step fails, subsequent runs return the same error:

// First run - fetchUser returns err('NOT_FOUND')
await workflow.run(async ({ step, deps }) => {
const user = await step('fetchUser', () => deps.fetchUser('999'), { key: 'user:999' });
return user;
});
// result.error === 'NOT_FOUND'
// Second run - returns cached error, no fetch
await workflow.run(async ({ step, deps }) => {
const user = await step('fetchUser', () => deps.fetchUser('999'), { key: 'user:999' });
return user;
});
// result.error === 'NOT_FOUND' (from cache)

To retry on error, clear the cache key first.

Use caseCaching helps
Idempotent operationsYes - payments, API calls
Resume after crashYes - completed steps skipped
Expensive computationsYes - don’t recompute
Time-sensitive dataNo - data may be stale
Non-idempotent operationsCareful - may cause issues

Learn about Persistence →