Caching
Cache step results so they don’t re-execute if the workflow runs again.
Enable caching
Section titled “Enable caching”Pass a cache to the workflow:
const workflow = createWorkflow('workflow', deps, { cache: new Map(),});Use keys to cache steps
Section titled “Use keys to cache steps”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;});Key requirements
Section titled “Key requirements”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}, orfetchUser: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()}` } // RandomThunks required for caching
Section titled “Thunks required for caching”Pass a function, not the result of calling the function:
// Without thunk - executes immediately, caching ignoredconst user = await step('fetchUser', () => deps.fetchUser('1'), { key: 'user:1' });
// With thunk - can be cachedconst user = await step('fetchUser', () => deps.fetchUser('1'), { key: 'user:1' });step.run, step.all, step.map and caching
Section titled “step.run, step.all, step.map and caching”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 timeawait step.all('fetchAll', { user: () => deps.fetchUser('1') });await step.map('fetchUsers', ids, deps.fetchUser);Cache scope
Section titled “Cache scope”The cache persists across workflow runs:
const cache = new Map();const workflow = createWorkflow('workflow', deps, { cache });
// First run - fetches userawait workflow.run(async ({ step, deps }) => { const user = await step('fetchUser', () => deps.fetchUser('1'), { key: 'user:1' }); return user;});
// Second run - uses cached valueawait workflow.run(async ({ step, deps }) => { const user = await step('fetchUser', () => deps.fetchUser('1'), { key: 'user:1' }); return user; // No fetch - returns cached value});Clearing the cache
Section titled “Clearing the cache”Clear specific keys or the entire cache:
const cache = new Map();
// Clear one keycache.delete('user:1');
// Clear allcache.clear();Custom cache implementations
Section titled “Custom cache implementations”Any object with Map-like get, set, has, delete methods works:
// Redis-backed cacheconst 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 });Caching and errors
Section titled “Caching and errors”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 fetchawait 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.
When to use caching
Section titled “When to use caching”| Use case | Caching helps |
|---|---|
| Idempotent operations | Yes - payments, API calls |
| Resume after crash | Yes - completed steps skipped |
| Expensive computations | Yes - don’t recompute |
| Time-sensitive data | No - data may be stale |
| Non-idempotent operations | Careful - may cause issues |