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

  1. Run the code as part of a flow step (@wmill.workflow) so the approval can suspend/resume the run
  2. 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
  3. 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

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


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/5a0d29275849cea3. Report an issue: GitHub.