{"record":{"id":"32f29e651ed8ebf7","repo":"windmill-labs/windmill","slug":"waitforapproval-can-only-be-called-inside-a-workfl","errorCode":null,"errorMessage":"waitForApproval can only be called inside a workflow()","messagePattern":"waitForApproval can only be called inside a workflow\\(\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"typescript-client/client.ts","lineNumber":2334,"sourceCode":" *\n * Pass `key` to name the step, then `getApprovalUrls(key)` yields the URLs that\n * resume exactly this approval — route them through your own channel. Without a\n * key the steps are named `approval`, `approval_2`, ...\n *\n * @example\n * const urls = await step(\"urls\", () => getApprovalUrls(\"manager\"));\n * await step(\"notify\", () => sendEmail(urls.resume, urls.cancel));\n * const { value, approver } = await waitForApproval({ key: \"manager\", timeout: 3600 });\n */\nexport function waitForApproval(options?: {\n  timeout?: number;\n  form?: object;\n  selfApproval?: boolean;\n  key?: string;\n}): PromiseLike<{ value: any; approver: string; approved: boolean }> {\n  const ctx: WorkflowCtx | null = _workflowCtx ?? Reflect.get(globalThis, \"__wmill_wf_ctx\");\n  if (!ctx) {\n    throw new Error(\"waitForApproval can only be called inside a workflow()\");\n  }\n  return ctx._waitForApproval(options);\n}\n\n/**\n * Resume/cancel/approval-page URLs bound to one `waitForApproval` step.\n *\n * Unlike `getResumeUrls()`, which signs a random nonce, these address the very\n * `resume_job` record the step's built-in approval buttons use, so they are\n * stable across replays and safe to embed in a custom notification.\n *\n * `stepKey` must match the `key` given to `waitForApproval`. Keys must be unique\n * within a workflow; reusing one throws rather than silently renaming it. The URL\n * only resumes while that step is awaiting approval; used at any other moment it is\n * rejected rather than banking a row a different approval would consume. Send it\n * ahead of time — approvers just cannot act before the workflow reaches the step.\n *\n * `resume` and `cancel` are step-bound; `approvalPage` is not — it opens the job's","sourceCodeStart":2316,"sourceCodeEnd":2352,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/typescript-client/client.ts#L2316-L2352","documentation":"waitForApproval delegates to the active workflow context (_workflowCtx or globalThis.__wmill_wf_ctx). If neither is set, there is no flow step to suspend and later resume, so the client throws this error (typescript-client/client.ts:2334). Approvals are inherently flow-scoped: outside a workflow there is nothing to pause or to receive the approval on.","triggerScenarios":"Calling waitForApproval in a standalone script, a plain function, or inside workflow-adjacent code that never registered the context; the global __wmill_wf_ctx removed by a sandbox or duplicated client bundles.","commonSituations":"Reusing approval code from a flow inside a scheduled standalone script; running the module where the worker did not set the global context; two bundled copies of the client so the set context is not the one read.","solutions":["Call waitForApproval only within a workflow() step; restructure standalone scripts to skip or mock approval.","If logic is shared, branch on context presence before calling waitForApproval.","Dedupe dependencies so a single copy of the Windmill client is bundled and the context global matches.","For tests, install a stub context via setWorkflowCtx that returns a canned result."],"exampleFix":"// before\nexport async function main() {\n  await waitForApproval({ key: 'ok' }); // standalone -> throws\n}\n// after\nexport async function main() {\n  const ctx = _workflowCtx ?? Reflect.get(globalThis, '__wmill_wf_ctx');\n  if (!ctx) return { skipped: 'approval requires a workflow' };\n  return await waitForApproval({ key: 'ok' });\n}","handlingStrategy":"type-guard","validationCode":"const approvalAvailable = () =>\n  Boolean(_workflowCtx ?? Reflect.get(globalThis, '__wmill_wf_ctx'));","typeGuard":"function hasWorkflowCtx(): ctx is WorkflowCtx {\n  return (_workflowCtx ?? Reflect.get(globalThis, '__wmill_wf_ctx')) != null;\n}","tryCatchPattern":"try {\n  return await waitForApproval({ key: 'ok' });\n} catch (e) {\n  if (e.message === 'waitForApproval can only be called inside a workflow()') {\n    return { skipped: 'approval requires a workflow' };\n  } else throw e;\n}","preventionTips":["Call waitForApproval only inside workflow() steps.","Branch on context presence before calling in shared code.","Dedupe the client dependency so the context global is a single instance.","Install a stub context via setWorkflowCtx for tests."],"tags":["workflow","approval","context"],"backgroundTag":"function-requires-workflow-context","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}