# Steps & Workflow Design

What steps are, when to use them, fan-out patterns, and best practices for reliable workflows.

Steps are how JsWorkflows breaks a workflow into reliable, independent pieces of work.

Designing your workflow around clear steps is one of the main differences between a workflow that is easy to maintain and recover, and one that becomes hard to retry, extend, or reason about.

## What a step is

A **step** is a single method on your `Workflow` class. Each time a step runs, it is a fresh, independent execution with:

- its own execution context
- no shared in-memory state between steps
- full access to the platform API (`api.scheduleNextStep`, `api.runStore`, and more)
- its own `data`, `headers`, and `api` arguments

When `api.scheduleNextStep()` is called, the current step exits normally. After the specified delay, the next step is dispatched as a separate execution, not a continuation of the same process.

## Why steps exist

Real workflows often need to:

- **Wait** — send a follow-up message later, check a status after a delay, or resume after an external event
- **Spread load** — process many records without forcing everything into one execution
- **Run in parallel** — let independent work happen in separate branches
- **Separate orchestration from work** — validate and queue in `start`, then do the delayed, retryable, or external work in later steps

All of these are solved by splitting the work across steps.

## How `start` should be used

`start` is the reserved entry point for every workflow run.

In many workflows, `start` is best used for:

- validating the incoming payload
- preparing the payload for later steps
- scheduling the next step
- deciding which branch or path should run next

That is usually the right design because `start` cannot be scheduled again with `api.scheduleNextStep()`. If work may need to be delayed, retried, or broken into smaller units, it should usually live in later named steps.

### Trigger-specific behavior

`start` behaves a little differently depending on the trigger type:

- **Shopify webhook triggers**: JsWorkflows acknowledges the webhook before your code runs. Your `start` step is not responsible for sending the HTTP response back to Shopify.
- **HTTP triggers**: `start` can return the HTTP response. If it returns a `Response`, that response is sent directly. If it returns a plain object or value, JsWorkflows returns it as JSON. If it returns nothing, the platform returns `{ "success": true }`.
- **Email triggers**: `start` receives the parsed email as `data` (sender, subject, body, and attachment descriptors). Attachments are not passed directly; use `api.getAttachment()` in a later retryable step to read the file content.
- **Scheduled triggers**: `start` is simply the entry point for the scheduled run.

### A good `start` step

```js
async start(data, headers, api) {
  if (!data.id) return;

  await api.scheduleNextStep({
    delay: '30 sec',
    action: 'processOrder',
    payload: { orderId: data.id },
  });
}
```

## Linear chains

The simplest multi-step pattern is a linear chain: `start` → `stepA` → `stepB` → done.

```js
export class Workflow {
  async start(data, headers, api) {
    await api.scheduleNextStep({
      delay: '1 day',
      action: 'sendFollowUp',
      payload: { orderId: data.id, email: data.email },
    });
  }

  async sendFollowUp({ orderId, email }, headers, api) {
    await api.scheduleNextStep({
      delay: '7 days',
      action: 'sendSecondFollowUp',
      payload: { orderId, email },
    });
  }

  async sendSecondFollowUp({ orderId, email }, headers, api) {
    // Runs later as a separate execution
  }
}
```

Each step only needs to know about the next one. Steps do not share memory, so pass everything the next step needs in `payload`.

## Fan-out (parallel branches)

Call `api.scheduleNextStep()` multiple times in one step to launch **parallel branches**. Each call starts an independent execution of the target method. The run is not marked complete until all branches finish.

```js
export class Workflow {
  async start(data, headers, api) {
    for (const item of data.line_items) {
      await api.scheduleNextStep({
        delay: '30 sec',
        action: 'checkLineItem',
        payload: { itemId: item.id, sku: item.sku, quantity: item.quantity },
      });
    }
  }

  async checkLineItem({ itemId, sku, quantity }, headers, api) {
    console.log(`Processing item ${sku} ×${quantity}`);
  }
}
```

Fan-out is useful when each item can be processed independently.

## Collecting results from fan-out branches

Use `api.runStore.push()` to accumulate results from parallel branches, then read them later in a consolidation step or lifecycle hook:

```js
export class Workflow {
  async start(data, headers, api) {
    for (const item of data.line_items) {
      await api.scheduleNextStep({
        delay: '30 sec',
        action: 'checkLineItemStock',
        payload: { itemId: item.id, sku: item.sku },
      });
    }
  }

  async checkLineItemStock({ itemId, sku }, headers, api) {
    const res = await fetch(`https://your-inventory-api.com/stock/${sku}`);
    const { quantity } = await res.json();

    await api.runStore.push('stockResults', { sku, quantity, inStock: quantity > 0 });
  }
}
```

The run state (`api.runStore`) is scoped to the current run and shared across all branches.

A later step or lifecycle hook can read `stockResults` and use it to build a summary, trigger a follow-up action, or write a report.

## Using steps to control large workloads

If your workflow processes large lists, doing all the work in one step can create throttling, retries, or hard-to-debug failures. Steps let you spread that work across time or across parallel branches.

### Spread work across staggered steps

```js
async start(data, headers, api) {
  const items = data.line_items;
  for (let i = 0; i < items.length; i++) {
    await api.scheduleNextStep({
      delay: `${30 + i} sec`,
      action: 'updateItem',
      payload: { itemId: items[i].id },
    });
  }
}
```

### Page through large result sets across steps

This is a more advanced example. Focus on the step pattern: fetch one page, process it, then schedule the next page only if more data remains.

```js
async start(data, headers, api) {
  await api.scheduleNextStep({
    delay: '30 sec',
    action: 'fetchPage',
    payload: { cursor: null, pageNum: 1 },
  });
}

async fetchPage({ cursor, pageNum }, headers, api) {
  const res = await fetch(
    `https://${env.SHOPIFY_STORE}/admin/api/${env.SHOPIFY_API_VERSION}/graphql.json`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        query: `query GetProducts($cursor: String) {
          products(first: 50, after: $cursor) {
            nodes { id title }
            pageInfo { hasNextPage endCursor }
          }
        }`,
        variables: { cursor },
      }),
    }
  );
  const { data: gqlData } = await res.json();
  const { nodes, pageInfo } = gqlData.products;

  // process this page

  if (pageInfo.hasNextPage) {
    await api.scheduleNextStep({
      delay: '30 sec',
      action: 'fetchPage',
      payload: { cursor: pageInfo.endCursor, pageNum: pageNum + 1 },
    });
  }
}
```

## Idempotency

Design every step so it is safe to retry. Running the same step twice with the same input should not create a worse outcome than running it once.

Practical strategies:

- **Use `api.dedupe()` when your workflow needs business-level deduplication** — for example, to prevent sending the same reminder twice for the same order or to stop an HTTP-triggered workflow from re-processing the same external event
- **Check before writing** — before tagging an order or sending a message, check whether it already happened
- **Use external idempotency keys** — many APIs support this; use a stable event or business ID

For Shopify webhook triggers, duplicate delivery protection is already handled by the platform before your workflow code runs. `api.dedupe()` is still useful when you want your own workflow-specific deduplication rules.

## Payload design

The `payload` you pass to `api.scheduleNextStep()` is persisted until the later step runs. Keep it lean:

- **Pass only what the next step needs**
- **Avoid large blobs when possible**
- **Do not depend on in-memory state**

If a later step needs something, pass it in `payload` or store it in `api.runStore`.

## When not to use a step

Not every workflow needs multiple steps. A single `start` step can be enough when:

- the work completes very quickly
- you are using an HTTP trigger and want to return the response directly from `start`
- you are in a scheduled trigger and the job is short and straightforward

The overhead of scheduling another step is unnecessary for work that can safely run in `start`.