# Waiting for Events

Pause a workflow run and resume it when an external event arrives.

`api.waitForEvent()` pauses a workflow branch and returns a **resume URL**. When that URL is called by a human or another system, the workflow continues from the specified next step. The caller can pass data via a POST JSON body or via URL query parameters on a GET request.

Use `api.waitForEvent()` for human approvals, external callbacks, and indefinite waits on outside systems. Do not use it for fixed delays or polling. For those cases, use [`api.scheduleNextStep()`](/workflow-api/scheduling/).

## Human-in-the-loop

The most common use case is inserting a human decision point into an automated workflow. The workflow pauses after reaching a step that requires human judgement, sends a message with the resume URL (via Slack, email, or another channel), and waits. When the person clicks the link or submits a form, the workflow resumes with their input.

Common examples:

- an order flagged as high-risk is held for manual review before being fulfilled
- a refund above a threshold requires manager approval before being issued
- a generated document or email draft is sent to a team member for sign-off before delivery
- a supplier change request pauses for a purchasing manager to confirm
- a customer escalation waits for a support lead to decide the resolution before the workflow applies it

In each case the workflow handles the automation before and after the decision, and the human only sees the specific question they need to answer.

## Signature

```js
const { token, resumeUrl } = await api.waitForEvent(options);
```

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `action` | `string` | required | Workflow method to call when resumed. Must be a named step method (`'start'` is not allowed) |
| `payload` | `any` | required | Data passed to the next step alongside any event data sent by the caller. Pass `{}` if you do not need stored payload fields |
| `timeout` | `number \| string` | `86400` | Seconds until the token expires. Accepts numbers (seconds) or strings like `"7 days"`, `"2 hours"`. Max: 30 days |
| `redirect_url` | `string` | `null` | If set, browser callers are redirected to this URL after processing instead of seeing the default confirmation page |

Returns `{ token: string, resumeUrl: string }`.

## Example: wait for approval

```js
export class Workflow {
  async start(data, headers, api) {
    // Pause this branch and get a resume URL
    const { resumeUrl } = await api.waitForEvent({
      action: 'onApproved',
      payload: { orderId: data.id },
      timeout: 86400,  // 24 hours
    });

    // Send the URL to someone who needs to act
    const { token } = await api.getOAuthToken('my-slack');
    await fetch('https://slack.com/api/chat.postMessage', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
      body: JSON.stringify({
        channel: '#approvals',
        text: `Order ${data.id} needs approval. <${resumeUrl}|Approve>`,
      }),
    });
    // start() returns here. The run stays open and waits for the resume URL to be called.
  }

  async onApproved(data, headers, api) {
    // data contains the payload passed to waitForEvent; data.event holds what the caller sent
    console.log('Order approved:', data.orderId);
    // Continue processing...
  }
}
```

## Resuming the workflow

The resume URL accepts both GET and POST. In the resumed step, the caller's data is always available at `data.event`, and the `payload` you set in `waitForEvent` is merged into `data` directly.

### POST with a JSON body

Send a JSON body to pass structured data to the resumed step:

```bash
curl -X POST "{resumeUrl}" \
  -H "Content-Type: application/json" \
  -d '{ "approvedBy": "jane@example.com", "note": "Looks good" }'
```

In the resumed step:

```js
async onApproved(data, headers, api) {
  data.event.approvedBy  // "jane@example.com"
  data.event.note        // "Looks good"
  data.orderId           // from the payload set in waitForEvent
}
```

### GET with query parameters

A GET request is useful for click-to-approve links in emails or Slack messages. Add any data you need as query parameters:

```
{resumeUrl}?approved=true&reviewer=alice
```

In the resumed step, query parameters arrive as strings at `data.event`:

```js
async onApproved(data, headers, api) {
  data.event.approved  // "true"
  data.event.reviewer  // "alice"
  data.orderId         // from the payload set in waitForEvent
}
```

If no query parameters are present, `data.event` is `null`.

Do not call `api.scheduleNextStep()` later in the same method invocation after `api.waitForEvent()`. The branch is already parked.

## Example: approve or reject by email

Generate one resume URL and append different query parameters to create two distinct action links sent in the same email. The resumed step reads `data.event` to know which link was clicked.

```js
export class Workflow {
  async start(data, headers, api) {
    const { resumeUrl } = await api.waitForEvent({
      action: 'onReviewed',
      payload: { orderId: data.id, amount: data.totalPrice },
      timeout: '24 hours',
    });

    const approveUrl = `${resumeUrl}?approve=true`;
    const rejectUrl  = `${resumeUrl}?approve=false`;

    const resp = await fetch('https://api.sendgrid.com/v3/mail/send', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${env.SENDGRID_API_KEY}`,
      },
      body: JSON.stringify({
        personalizations: [
          {
            to: [{ email: 'manager@example.com' }],
          },
        ],
        from: {
          email: 'noreply@example.com',
          name: 'JsWorkflows',
        },
        subject: `Refund request for order ${data.id}`,
        content: [
          {
            type: 'text/plain',
            value: `A refund of ${data.totalPrice} was requested for order ${data.id}.\n\nApprove: ${approveUrl}\nReject: ${rejectUrl}`,
          },
        ],
      }),
    });

    if (!resp.ok) {
      const errText = await resp.text();
      throw new Error(`SendGrid email send failed: ${resp.status} ${errText}`);
    }
  }

  async onReviewed(data, headers, api) {
    if (data.event.approve === 'true') {
      console.log('Refund approved for order', data.orderId);
      // issue the refund...
    } else {
      console.log('Refund rejected for order', data.orderId);
      // notify the customer...
    }
  }
}
```

Note that query parameter values are always strings, so compare `data.event.approve` against `'true'` and `'false'` rather than boolean values.

This example uses SendGrid's Mail Send API and expects a `SENDGRID_API_KEY` secret in the workflow.

## Browser redirect

Set `redirect_url` to send the browser to a custom page after resuming instead of the default confirmation screen:

```js
const { resumeUrl } = await api.waitForEvent({
  action: 'onApproved',
  payload: { orderId: data.id },
  redirect_url: 'https://mystore.com/approved',
});
```

## Resume responses

The resume endpoint adapts its response to the caller:

- browser requests that accept HTML are resumed in the background and receive either the default confirmation page or your `redirect_url`
- server or API callers are awaited and receive JSON: `{ ok: true }` on success or `{ ok: false, error: '...' }` on failure

## Timeout behaviour

If the resume URL is not called before `timeout` seconds have elapsed, the token expires. The run is then healed to **failed** either by the RunTracker alarm or sooner on the next runs-list refresh, for example when the merchant opens the runs dashboard.

> Caution: The resume URL is single-use. After a successful resume, later calls normally return `404`. Test runs return `{ token: 'test-mode', resumeUrl: '' }`, and the step does not actually pause.

## Duration strings

The `timeout` option accepts human-readable duration strings in addition to raw seconds:

| String | Seconds |
| --- | --- |
| `"30 sec"` / `"30 seconds"` | 30 |
| `"5 min"` / `"5 minutes"` | 300 |
| `"2 hr"` / `"2 hours"` | 7200 |
| `"1 day"` / `"7 days"` | 86400 / 604800 |
| `"1 week"` / `"2 weeks"` | 604800 / 1209600 |

Unsupported strings throw. Months and years are not supported.