Skip to main content
After this guide, a run pauses on an approval until someone approves, denies, or edits the payload, or waits for a signal your webhook delivers, and the next step reads the outcome as data. For a step that needs typed input from a person rather than a decision, call ctx.suspend(payload) and resume it with lua workflows resume; see Operate runs. Verified against lua-cli 3.33.0. Before you begin
1

Add an approval step

.approval(id, options) shows the previous entry’s output as the payload and parks the run until a decision arrives. Put a .map() in front of it so the payload is exactly what the approver should see and edit.
src/workflows/refund-approval.ts
approver is 'creator' (the default), 'org-admins', { users: [...] }, { role }, { group }, or { governance: { policyId } }; excludeInitiator keeps whoever started the run from deciding. timeoutHours defaults to 168 and is at most 720; businessHours makes the deadline count working time. onTimeout is 'deny', 'cancel-run', 'fail', or up to three { escalateTo, timeoutHours } hops ending in one of those; 'continue' behaves as 'deny' with decision: 'timed_out'. A non-empty editablePaths (amount, drafts[*].body, summary.title) makes the payload editable, editedPayloadSchema validates the edit, and fourEyes: { edit, approve } keeps the editor from also approving.
2

Read the decision in the next step

The step after an approval receives the approval’s output, never the payload, so give its inputSchema a .passthrough() and read the original with getStepResult('<map id>') or getInitData().
approved is false on a denial and on a timeout under 'deny'; decision is 'approved', 'denied', or 'timed_out'; text is the approver’s note or the decision word, and note holds the note verbatim when one was left. editedPayload and editRevision appear when the payload was edited; timedOut, escalations, and items when a deadline chain or per-item approval decided. Under the default onDeny: 'continue' the run continues and your step decides what a denial means; onDeny: 'fail' fails the run.
3

Wait for a signal

.waitForSignal(id, options) parks the run until a named signal arrives from outside it. A signal that arrives before the wait begins is held and consumed when it does.
src/workflows/invoice-paid.ts
schema validates each delivery and rejects a payload that does not match. acceptedSources defaults to ['webhook', 'api', 'user']; add 'agent' to let the agent deliver it from chat. The step completes with { payload, source, signalId, receivedAt } (receivedAt in epoch milliseconds), or with { received: false, timedOut: true } when onTimeout is 'continue'; under the default 'fail' a timeout fails the step.
Local runs only. Under lua test workflow, --signal completes the wait with the payload itself rather than the { payload, source, signalId, receivedAt } envelope, so this step returns closed: false offline and closed: true on the platform.
4

Deliver the signal from a webhook

Start the run with a correlationKey (Workflows.start(…, { correlationKey }) or lua workflows start --correlation-key) and the webhook needs no run id.
src/webhooks/payment-received.webhook.ts
With a run id in hand, Workflows.signal(runId, 'payment.received', payload, { dedupeKey }) delivers to that run and answers { accepted, consumed, stepId }; dedupeKey makes a redelivered webhook a no-op. From a terminal, lua workflows signal <runId> payment.received --payload '{"invoiceId":"inv_abc123","amount":120}' --dedupe-key evt_abc123 does the same.
Local runs only. The deployed runtime fails Workflows.signal and Workflows.signalByKey with signal_unavailable, so a deployed webhook delivers the signal through the REST API instead; lua test webhook runs this file as written.
5

Answer an approval from the CLI

An approval is addressed by its wfa_… id, which lua workflows status <runId> --json exposes as suspendedFor.approvalId. To edit the payload, read it first: the fingerprint pins the revision you saw.
Output
Save the edited payload to a file and approve against that fingerprint, or deny with a note:
Only a signed-in person can decide; an API key is refused. The same decision is available in the desktop app and the inbox, and an organization policy can restrict which surfaces (desktop, inbox, chat-card, channel-reply, cli, api) may approve, exclude the initiator, or require a fresh sign-in (STEP_UP_REQUIRED).
6

Verify

Run both workflows offline: deny the refund, then deliver the payment signal.
Output
--approve [email protected] answers with an edited payload, checked against editedPayloadSchema before the run starts and against editablePaths when the approval is reached. A wait you did not answer stops with exit 2 and SIGNAL_UNANSWERED waitForSignal "payment" needs --signal payment.received=<json> (no interactive stdin).

If it isn’t working

Someone edited the payload after you read it. Run approval-payload again, apply your edit to the new revision, and approve with the new fingerprint.
You ran resume on an approval step, or resolve-step with an API key. Approvals are answered with approve, by a signed-in person.
The payload does not match the wait’s schema. The message lists the failing paths; a rejected delivery does not consume its dedupeKey, so send the corrected payload with the same key.

Next steps

Operate runs

Watch a parked run, resume a suspended step, and retry or resolve a parked one.

Workflow builder reference

Every approval() and waitForSignal() option and the output fields.

Handle a webhook

Verify the provider’s signature before you signal a run.

Workflows runtime API

signal, signalByKey, and resume from code.