Skip to content

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().

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.

const { token, resumeUrl } = await api.waitForEvent(options);
OptionTypeDefaultDescription
actionstringrequiredWorkflow method to call when resumed. Must be a named step method ('start' is not allowed)
payloadanyrequiredData passed to the next step alongside any event data sent by the caller. Pass {} if you do not need stored payload fields
timeoutnumber | string86400Seconds until the token expires. Accepts numbers (seconds) or strings like "7 days", "2 hours". Max: 30 days
redirect_urlstringnullIf set, browser callers are redirected to this URL after processing instead of seeing the default confirmation page

Returns { token: string, resumeUrl: string }.

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...
}
}

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.

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

Terminal window
curl -X POST "{resumeUrl}" \
-H "Content-Type: application/json" \
-d '{ "approvedBy": "[email protected]", "note": "Looks good" }'

In the resumed step:

async onApproved(data, headers, api) {
data.event.approvedBy // "[email protected]"
data.event.note // "Looks good"
data.orderId // from the payload set in waitForEvent
}

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:

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.

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: [
{
to: [{ email: '[email protected]' }],
},
],
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.

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',
});

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

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.

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

StringSeconds
"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.