Waiting for Events
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().
Human-in-the-loop
Section titled “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
Section titled “Signature”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
Section titled “Example: wait for approval”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
Section titled “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
Section titled “POST with a JSON body”Send a JSON body to pass structured data to the resumed step:
curl -X POST "{resumeUrl}" \ -H "Content-Type: application/json" \In the resumed step:
async onApproved(data, headers, api) { data.event.note // "Looks good" data.orderId // from the payload set in waitForEvent}GET with query parameters
Section titled “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=aliceIn the resumed step, query parameters arrive as strings at data.event:
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
Section titled “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.
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: [ { }, ], from: { 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
Section titled “Browser redirect”Set redirect_url to send the browser to a custom page after resuming instead of the default confirmation screen:
const { resumeUrl } = await api.waitForEvent({ action: 'onApproved', payload: { orderId: data.id }, redirect_url: 'https://mystore.com/approved',});Resume responses
Section titled “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
Section titled “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.
Duration strings
Section titled “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.