Human Approval
An approval pauses a run durably until a person approves, rejects, or a configured deadline wins.
Declare an approval
<approval id="review" name="Review refund" timeout="24h" on-timeout="reject">
<notify>
<telegram chats="123456789" bot-token="{{secrets.TELEGRAM_BOT_TOKEN}}" />
</notify>
<when-approved><step id="refund"><script>return issueRefund();</script></step></when-approved>
<when-rejected><step id="record"><script>return { rejected: true };</script></step></when-rejected>
</approval>The optional <notify> comes first, followed by required approved and rejected arms. Either decision arm may be empty.
Durable decision
WOML persists the wait and opaque capability before sending notifications. The first valid decision wins; identical repeats are idempotent and an opposing decision conflicts. Restarting the runtime does not lose the wait.
Decisions use the HTTP endpoint printed or embedded by the provider. There is no woml.resume() JavaScript API. The result at context.steps.review records the decision, source, and decision time.
Build an approval workflow
<approval
id="refundApproval"
name="Approve refund"
description="A manager must approve refunds above the automatic limit."
timeout="24h"
on-timeout="reject"
>
<notify>
<telegram
chats="123456789"
bot-token="{{secrets.TELEGRAM_BOT_TOKEN}}"
/>
</notify>
<when-approved>
<step id="issueRefund" name="Issue refund">
<script>
return { refunded: true, amount: context.payload.amount };
</script>
</step>
</when-approved>
<when-rejected>
<step id="recordRejection" name="Record rejection">
<script>
return { refunded: false };
</script>
</step>
</when-rejected>
</approval>The optional notification comes first, followed by the required approved and rejected arms. Either decision arm may be empty when the decision itself is the only required output.
Understand the durable wait
Before sending a notification, WOML stores the waiting state and an opaque decision capability. A runtime restart therefore does not lose the approval. Notification providers receive separate delivery tokens, but every destination shares one first-decision-wins authority.
The first valid approved or rejected decision wins. Repeating the same decision is idempotent; submitting the opposite decision afterward conflicts.
Use the result
After settlement, the approval publishes data such as:
{
"decision": "approved",
"source": "human",
"decidedAt": "2026-08-04T12:00:00.000Z"
}Read it from context.steps.refundApproval. source identifies a human or timeout decision.
With on-timeout="reject", timeout publishes a rejected result and executes the rejected arm. With on-timeout="fail", the run fails without executing either arm. Omitting timeout creates no WOML deadline, although deployment and workflow policies may still impose broader limits.
Integrate any client
Provider buttons call the same HTTP decision surface that a mobile app, Slack integration, or curl client can call. The token is a capability and must be treated as sensitive. There is deliberately no large client package and no woml.resume() function.
Do not place approval around work that must remain fully synchronous through services.workflows.call(); long approval-waiting synchronous child calls are outside the current v1 boundary.