# Scheduling Steps

Schedule subsequent steps and build multi-step or fan-out workflows.

`api.scheduleNextStep()` queues the next step to execute after a delay. Calling it multiple times in the same step creates a **fan-out** — parallel branches that each run independently.

## Signature

```js
await api.scheduleNextStep({ delay, action, payload });
```

| Option | Type | Description |
| --- | --- | --- |
| `delay` | `number \| string` | Seconds until the next step runs. Accepts a number or a duration string. Minimum: 10 seconds. Maximum: 400 days |
| `action` | `string` | The method name on your `Workflow` class to call. It must be a named continuation step, not `start` |
| `payload` | `any` | Data passed as the `data` argument to the next step |

## Duration strings

Instead of raw seconds, you can pass a human-readable string:

| String | Duration |
| --- | --- |
| `"30 sec"` / `"30 seconds"` | 30 seconds |
| `"5 min"` / `"5 minutes"` | 5 minutes |
| `"2 hr"` / `"2 hours"` | 2 hours |
| `"1 day"` / `"3 days"` | 1 or 3 days |
| `"1 week"` / `"2 weeks"` | 1 or 2 weeks |

Unsupported strings throw an error. Months and years are not supported because they are not a fixed number of seconds.

## Simple linear chain

```js
export class Workflow {
  async start(data, headers, api) {
    // Schedule the next step to run in 10 minutes
    await api.scheduleNextStep({
      delay: '10 min',
      action: 'sendReminder',
      payload: { orderId: data.id, email: data.email },
    });
  }

  async sendReminder({ orderId, email }, headers, api) {
    console.log('Sending reminder for order', orderId, 'to', email);
    // ... send email via fetch()
  }
}
```

## Fan-out (parallel branches)

Call `scheduleNextStep()` multiple times in one step to launch parallel branches. Each call is an independent branch. The run is not marked complete until **all** branches finish.

```js
export class Workflow {
  async start(data, headers, api) {
    // Process each line item in parallel
    for (const item of data.line_items) {
      await api.scheduleNextStep({
        delay: 10,
        action: 'processItem',
        payload: { itemId: item.id, title: item.title },
      });
    }
    // start() completes here — all N branches are now running concurrently
  }

  async processItem({ itemId, title }, headers, api) {
    console.log('Processing item:', title);
    // Each branch runs independently
  }
}
```

> Note: Credits are consumed per step execution. A fan-out of 10 branches uses 10 credits for those branches, plus 1 for the `start` step that scheduled them.

## Chain depth

Each call to `scheduleNextStep()` increments the chain depth. Fan-out branches at the same level all share the same depth counter — a fan-out of 100 branches from `start` has depth 1, not 100.

There is no per-plan chain depth limit. A platform-wide circuit breaker prevents runaway infinite loops.

## Deduplication — api.dedupe()

`api.dedupe()` prevents a step from running more than once for the same logical event within a time window. It is useful for business-level idempotency, duplicate suppression on HTTP or external-event workflows, and other workflow-specific safety rules.

```js
const { locked } = await api.dedupe(unique_id, delay);
```

| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `unique_id` | `string` | required | An identifier that should be unique per logical event |
| `delay` | `number` | `120` | How long (in seconds) to hold the lock before it expires |

Returns `{ locked: boolean, key: string }`. If `locked` is `true`, another invocation already acquired the lock — skip processing.

For Shopify webhook triggers, duplicate delivery protection already happens before your workflow code runs. `api.dedupe()` is still useful there when you want additional workflow-specific or business-level deduplication. When you do use it in a Shopify webhook `start(...)`, prefer `headers['x-shopify-event-id']` as the primary dedupe key and fall back to a stable payload-based key if the header is missing.

```js
export class Workflow {
  async start(data, headers, api) {
    const dedupeKey =
      headers?.['x-shopify-event-id'] ||
      `order-${data.admin_graphql_api_id ?? data.id}`;
    const { locked } = await api.dedupe(dedupeKey, 300);
    if (locked) {
      console.log('Duplicate event for order', data.id, '- skipping');
      return;
    }

    // Safe to process — this invocation holds the lock
    await api.scheduleNextStep({ delay: 10, action: 'processOrder', payload: data });
  }
}
```