windmill-labs/windmill · error · RuntimeError
wait_for_approval can only be called inside a @workflow
Error message
wait_for_approval can only be called inside a @workflow
What it means
wait_for_approval pauses a workflow until a human approves via a generated form. The pause/resume mechanism is implemented by the workflow context (_workflow_ctx), so calling wait_for_approval outside a @workflow cannot suspend anything and the library raises this RuntimeError instead of hanging or silently continuing.
Source
Thrown at python-client/wmill/wmill/client.py:3459
Args:
timeout: Approval timeout in seconds (default 1800).
form: Optional form schema for the approval page.
self_approval: Whether the user who triggered the flow can approve it (default True).
key: Optional checkpoint key naming this approval step.
Example::
urls = await step("urls", lambda: get_approval_urls("manager"))
await step("notify", lambda: send_email(urls["resume"], urls["cancel"]))
result = await wait_for_approval(key="manager", timeout=3600)
"""
ctx: WorkflowCtx | None = _workflow_ctx.get(None)
if ctx is not None:
return await ctx._wait_for_approval(
timeout=timeout, form=form, self_approval=self_approval, key=key
)
raise RuntimeError("wait_for_approval can only be called inside a @workflow")
async def parallel(items, fn, *, concurrency: Optional[int] = None):
"""Process items in parallel with optional concurrency control.
Each item is processed by calling ``fn(item)``, which should be a @task.
Items are dispatched in batches of ``concurrency`` (default: all at once).
Example::
@task
async def process(item: str):
...
results = await parallel(items, process, concurrency=5)
"""
if not items:
return []View on GitHub (pinned to e474e8803c)
Solutions
- Run the code as part of a flow step (@wmill.workflow) so the approval can suspend/resume the run
- Replace wait_for_approval outside flows with the approval REST API (create a suspended run / use wmill CLI approval commands) if you must orchestrate manually
- Structure the flow so approval stays inside the flow definition rather than in a shared helper called from a plain script
Example fix
// before
wait_for_approval(form=approvers_form) # plain script -> RuntimeError
// after
import wmill
@wmill.workflow
def order_flow():
wmill.wait_for_approval(form=approvers_form)
return "approved" Defensive patterns
Strategy: validation
Validate before calling
from wmill.client import _workflow_ctx
if _workflow_ctx.get(None) is None:
raise RuntimeError("wait_for_approval requires a @workflow (flow) context") Type guard
def in_workflow() -> bool:
from wmill.client import _workflow_ctx
return _workflow_ctx.get(None) is not None Try / catch
try:
wmill.wait_for_approval(form=f)
except RuntimeError as e:
if "can only be called inside a @workflow" in str(e):
handle_outside_flow_approval() # e.g. use approval REST API
else:
raise Prevention
- Place wait_for_approval directly in flow steps, not in helpers reused by plain scripts
- Never call it from scheduled standalone scripts
- Test approval flows by running the actual flow, not by invoking the helper locally
When it happens
Trigger: Calling wmill.wait_for_approval(...) at the top level of a plain script, inside a non-flow job, or in a context where _workflow_ctx.get() is None (e.g. approval logic factored out of the flow into a helper executed outside it).
Common situations: Testing approval logic locally with `python script.py` instead of running the flow; reusing an approval helper in a scheduled standalone script; calling wait_for_approval from an inline script step that is not part of a flow.
Related errors
- task_script("{path}") can only be called inside a @workflow
- task_flow("{path}") can only be called inside a @workflow
- could not read the run of job ${id}: ${res.status} ${await r
- could not record run ${experiment_id}: ${res.status} ${await
- file path escapes the build directory: ${rel}
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/5a0d29275849cea3.
Report an issue: GitHub.