Skip to Content

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:

  1. Code starts from the beginning
  2. Completed steps are skipped (cached result returned immediately)
  3. On the first uncached step, execution proceeds
  4. Tool calls may pause for approval if in approval-required mode
  5. After approval, code re-runs with updated cache
  6. 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:

  1. Execute the callback
  2. Store the return value
  3. Associate it with step name 'gather'

On subsequent runs:

  1. Check if 'gather' has a cached result
  2. If yes, return cached value immediately (skip callback)
  3. 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:

  1. Current step state is saved
  2. User receives approval request
  3. On approval, workflow restarts from beginning
  4. Cached steps return immediately
  5. 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.

Last updated on