Approvals and events
How orders wait for people and outside systems, how decisions and events reach them, and how cancelling undoes their work.
Two node types pause an order until something outside the Workflow happens:
Approval waits for a person, and
Wait for event waits for a business event such as
payment.received. Both call step.waitForEvent with a timeout, so a waiting order costs
nothing while it waits and survives restarts. The order’s OrderAgent
records every open wait and delivers to it.
Approvals
Section titled “Approvals”An Approval node has a title (a template, default Approve order {{order.id}}), an assignee
(free text, default manager) and a timeout (default 2 days). When it starts, the agent:
- adds the wait to the order’s live state, where the order page shows it;
- inserts a row in the
approvalstable with the title, assignee, the order total and statuspending, which the approvals inbox lists.
The node takes one of three branches:
| Branch | When |
|---|---|
approved |
The decision is approved |
rejected |
The decision is rejected |
escalated |
Nobody decided before the timeout |
The node’s output (nodes.<id>) is the decision payload: decision, by, note and
outcome. The approvals row is updated with the outcome, decidedBy (timeout for an
escalation) and the note. Escalation only picks the branch; what happens next is up to the flow,
for example a second approval for a more senior assignee.
Deciding
Section titled “Deciding”From the order page or the approvals inbox (/approvals), each decision has a note field
and a 5-second undo window. Nothing is sent until the window closes, so Undo is real: the
Workflow never hears about a decision that was taken back. If you leave the page during the
window, the pending decision is sent then.
The inbox filters by pending, approved, rejected, escalated or all. For a pending row it
shows why a person is being asked (for a credit check: how far over the limit, the limit and the
balance, all in USD, with a foreign-currency order shown in its own currency and in USD), and when the approval escalates and to which step. Keyboard: j/k (or the arrow keys)
move between rows, a approves, r rejects.
Deciding needs the approver role or higher. by is the signed-in person’s email (operator for
the shared workspace password); see Team and roles.
From the API:
curl -X POST https://orderloop.io/api/v1/orders/ord_…/approvals/approval \ -H "Authorization: Bearer $ORDERLOOP_TOKEN" -H 'content-type: application/json' \ -d '{ "decision": "approved", "note": "Paid last invoice" }'$ORDERLOOP_TOKEN is an API token with the approver role or higher. The decision is recorded
by the token (token:<name>) or the signed-in person; a body by is used only with the shared
APP_PASSWORD, where it defaults to api. A decision for a node that isn’t waiting for an approval fails with
That node is not waiting for an approval. See the REST API.
From the order chat, the assistant can prepare an approval or rejection, but a person confirms it on the order page, with the same undo window. See AI nodes and the copilot.
Waiting for events
Section titled “Waiting for events”A Wait for event node names an event (default payment.received) and a timeout (default
30 days). It takes received when the event arrives and timeout otherwise.
The event’s payload is stored in the run variables under the event name with every run of
non-alphanumerics replaced by _: payment.received → vars.payment_received,
return.requested → vars.return_requested. Later nodes read it from there; the storefront
flow’s Refund node, for example, takes vars.return_requested.amount. The agent adds event and
at (epoch ms) to every delivered payload.
Delivering an event
Section titled “Delivering an event”An event is delivered to whichever node is waiting for that event name. If none is, nothing
happens and the caller is told delivered: false; delivering again later works once a node is
waiting.
-
Order page: the open wait has a Simulate
<event>button (payload{ "source": "dashboard", "amount": <order total> }). -
Back office: lists every order waiting on an event, with a button to deliver it (payload
{ "source": "back-office" }). -
REST API, with the admin password:
curl -X POST https://orderloop.io/api/v1/orders/ord_…/events \-H "Authorization: Bearer $ORDERLOOP_TOKEN" -H 'content-type: application/json' \-d '{ "event": "payment.received", "payload": { "amount": 3106.78 } }'The response is
{ "delivered": true, "nodeId": "…" }or{ "delivered": false }. -
Signed webhook, for external systems that shouldn’t hold the admin password:
POST /api/v1/webhooks/eventswith the body signed byWEBHOOK_SECRET.
Signed events
Section titled “Signed events”POST /api/v1/webhooks/eventsX-OrderLoop-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(WEBHOOK_SECRET, "<t>.<raw body>")>
{ "orderId": "ord_…", "event": "payment.received", "payload": { "amount": 3106.78 } }The timestamp is part of the signed message and must be within 5 minutes of the server’s clock,
so a captured request can’t be replayed later. signWebhook(secret, rawBody) in
@orderloop/core produces the header; verifyWebhook checks it.
| Status | Meaning |
|---|---|
200 |
{ "delivered": true, "nodeId": "…" } |
409 |
{ "delivered": false }: no node is waiting for that event yet, so retry later |
400 |
The body isn’t { orderId, event, payload? } |
401 |
Missing or malformed header, stale timestamp, or wrong signature |
503 |
WEBHOOK_SECRET isn’t set |
Answering 409 lets the sender’s own retry policy deliver an event that arrives before the order
reaches its wait.
Timeouts and TIME_SCALE
Section titled “Timeouts and TIME_SCALE”Every wait and timeout is divided by TIME_SCALE (default 1440 in
apps/engine/wrangler.jsonc: a day becomes a minute, a 2-day escalation about two minutes), and
never less than one second. Set it to "1" for real time. The deadline the order page and the
inbox show is the scaled one.
Cancelling
Section titled “Cancelling”Cancel from the order page, the API (POST /api/v1/orders/:id/cancel with an optional reason),
or the order chat (prepared, then confirmed by a person). Cancelling an order that has already
ended does nothing. Otherwise the agent:
- terminates the Workflow instance;
- compensates the order: releases its inventory reservations, voids its payment authorizations
and cancels its open shipments (
compensateOrderinapps/engine/src/connectors.ts); - marks pending approvals
cancelledand clears the order’s waits; - sets the status to
cancelled, with the reason and the counts in the timeline; - sends the
order.cancelledoutbound webhook.
Captured or charged money isn’t refunded by a cancel; refunds are a node in the flow.
Cancelling is different from an order that fails: a failure rolls back inside the Workflow, which runs each compensable step’s rollback in reverse order (see OrderLoop Workflow).