--- title: 'A refund gated by a human approval' description: 'A refund workflow that parks until a person decides, then runs the payout step only on an approval.' position: 2 --- # A refund gated by a human approval > The run pauses on `requestApproval` until a person decides, and the side > effect runs only after a fresh `approved` decision. If nobody decides within > the timeout, the `fallback` wins. `requestApproval` is a workflow primitive: > call it from the body, never from a step. The whole workflow is a single `workflows/refund.workflow.ts` file. You start it like any other workflow, as [Getting started with workflows](/docs/sdk/workflows/building-workflows#3-triggering-a-run) shows. ```ts // workflows/refund.workflow.ts import { z } from '@stackbone/sdk'; import { requestApproval } from '@stackbone/sdk/workflow'; export const inputSchema = z.object({ orderId: z.string(), amount: z.number().positive(), }); export const outputSchema = z.object({ orderId: z.string(), refunded: z.boolean(), decision: z.string(), }); export async function refundWorkflow(input: z.infer) { 'use workflow'; const decision = await requestApproval({ // No `token`: the SDK assigns a resume key unique to this run. topic: 'refund', payload: { orderId: input.orderId, amount: input.amount }, title: 'Approve refund', timeout: '24h', fallback: 'reject', }); if (decision.status !== 'approved') { return { orderId: input.orderId, refunded: false, decision: decision.status }; } await performRefund(input.orderId, input.amount); return { orderId: input.orderId, refunded: true, decision: decision.status }; } async function performRefund(orderId: string, amount: number) { 'use step'; // the side effect, gated behind the approval. Keep it idempotent return { refundId: `rf_${orderId}_${amount}` }; } ``` A reviewer decides from the dashboard or with `stackbone hitl approve --yes`, which wakes the parked run. Two details decide whether this holds up in production. - Leave `token` unset. The SDK then assigns a resume key unique to the run. A retry, or a second refund run for the same order while the first approval is pending, cannot collide with a key another live run holds. A collision resolves to the `fallback` instead of waiting for a person. See [when to pass your own token](/docs/sdk/humans/approval#when-to-pass-your-own-token). - Keep `performRefund` idempotent. The runtime re-runs a step that crashes part-way through, so key the payout on `orderId` and let a repeat call return the existing refund rather than issuing a second one. ## What's next - **[An onboarding pipeline](/docs/examples/workflows/onboarding-pipeline)**: a linear run with no pause in it. - **[A scheduled digest with a long wait](/docs/examples/workflows/scheduled-digest)**: start a run from a cron schedule and pace it with `sleep`. - **[What is a workflow](/docs/sdk/workflows/overview)**: the model behind this example. - **[Human-in-the-loop](/docs/sdk/humans/approval)**: the full `requestApproval` surface and the deciding commands.