Steps & Caching
Steps are the fundamental building block of code workflows. They enable efficient caching, replay after approval, and reliable execution.
Why steps matter
When a workflow runs:
- Code starts from the beginning
- Completed steps are skipped (cached result returned immediately)
- On the first uncached step, execution proceeds
- Tool calls may pause for approval if in approval-required mode
- After approval, code re-runs with updated cache
- Process repeats until
complete()is called
This model makes workflows resumable and efficient — if a run pauses for approval, it picks up exactly where it left off.
How caching works
const emails = await step('gather', async () => {
return await tool('search_emails', { query: 'is:unread' });
});The first time this step runs:
- Execute the callback
- Store the return value
- Associate it with step name
'gather'
On subsequent runs:
- Check if
'gather'has a cached result - If yes, return cached value immediately (skip callback)
- If no, execute callback
Replay model
Best practices
Keep steps focused
Each step should do one thing. This makes caching efficient and code readable.
// Good: focused steps
const emails = await step('gather', async () => { ... });
const filtered = await step('filter', async () => { ... });
const formatted = await step('format', async () => { ... });
// Avoid: everything in one step
const result = await step('do_everything', async () => {
// Too much logic here defeats the purpose of step caching
});Handle empty cases
Always handle the case where there’s no data to process.
const emails = await step('gather', async () => {
return await tool('search_emails', { query: '...' });
});
await step('process', async () => {
if (emails.length === 0) {
await tool('send_email_to_user', {
subject: 'Nothing to report',
body: 'No matching emails found.'
});
return;
}
// Process emails...
});Unique step names
Step names must be unique within a workflow. Use descriptive names.
// Good
await step('gather_emails', async () => { ... });
await step('filter_by_sender', async () => { ... });
await step('format_summary', async () => { ... });
// Bad - duplicate names will cause errors
await step('process', async () => { ... });
await step('process', async () => { ... }); // Error!Don’t put code outside steps
All logic must be inside step callbacks. Code outside steps runs on every replay.
// Bad - runs every replay
console.log('Starting workflow');
const config = loadConfig();
// Good - runs once, cached
const config = await step('load_config', async () => {
return loadConfig();
});Step dependencies
Steps can depend on previous step results:
const emails = await step('gather', async () => {
return await tool('search_emails', { query: 'newer_than:1d' });
});
// This step depends on 'gather' result
const summary = await step('summarize', async () => {
return emails.map(e => e.subject).join('\n');
});
// This step depends on 'summarize' result
await step('send', async () => {
await tool('send_email_to_user', {
subject: 'Daily Summary',
body: summary
});
});Approval and replay
When a workflow pauses for approval:
- Current step state is saved
- User receives approval request
- On approval, workflow restarts from beginning
- Cached steps return immediately
- Execution resumes at the paused step
This means:
- Approvals don’t lose progress
- Each step only executes once (unless explicitly re-run)
- State is preserved across pauses
Caching happens per-run. A new trigger creates a fresh run with no cached steps.
Related
- Code Workflows — Overview
- Calling Agents — AI in code workflows