Scenario · Expense approval · Email magic link
A finance approval gated through a signed email link
Same durable-approval mechanism as the other scenarios, but a channel with no platform-level signature to lean on — email's own answer is a self-contained, expiring token, not just a different send API.
An expense report clears the threshold
A finance-ops agent processes a submitted expense report and calls reimburse_expense. Under a threshold it's auto-allowed; this one is $4,200, over the limit actauth.yml sets for that tool — it matches an "ask" rule instead.
The call pauses, and a real email goes out
requestDurableApproval fires immediately — no request held open. EmailNotifier builds two links, Approve and Deny, each a signed, expiring token (pendingId + decision, HMAC-SHA256'd) embedded directly in the link itself, and sends the email.
The approver clicks Approve, from their inbox
A plain browser GET lands on the host's own route (examples/notifier-handler/email.ts's handleEmailApprovalClick). It verifies the token — signature and expiry — then makes POST /pending-approvals/:id/resolve server-side, on the human's behalf. Same resolve route every other channel uses underneath.
Why the link needs its own signature
Unlike Slack or a signed webhook, an emailed link has no platform behind it vouching for the click — it can sit in an inbox for days, get forwarded, get copied into another tab. The token carries its own expiry (3 days by default) and is checked before it ever reaches loopengine's own routes, so a stale or tampered link just renders "expired or invalid," not a resolved approval.
The reimbursement runs, and the tab just says so
The turn resumes from its durable checkpoint, reimburse_expense actually executes, and the click's own browser tab renders a small confirmation page — Approved, or Already resolved if a second click (or a colleague on the same thread) got there first.
Not just a channel swap
Slack and Lark sign their own callbacks — the platform vouches for the click. A clicked email link is a bare GET request with nothing attached, so EmailNotifier signs its own: pendingId and decision, HMAC'd with a server secret, expiring by default in 3 days. One verification function (verifyMagicLink) is shared by the sending and receiving sides so the two can't silently drift apart.
Want the other channels? See the customer-service scenario for a signed generic webhook, or the incident-triage scenario for Slack's own interactive buttons.