windmill-labs/windmill · error · RuntimeError
task_script("{path}") can only be called inside a @workflow
Error message
task_script("{path}") can only be called inside a @workflow What it means
task_script(path) returns a wrapper that, when invoked, enqueues a Windmill script as a step — but only if a @workflow-decorated context exists in the current thread-local _workflow_ctx. When called outside a workflow, the wrapper deliberately raises this RuntimeError instead of silently running the script as a plain sub-call. It is a guard against using workflow-only task primitives in ordinary scripts.
Source
Thrown at python-client/wmill/wmill/client.py:3337
):
"""Create a task that dispatches to a separate Windmill script.
Usage::
extract = task_script("f/data/extract", timeout=600)
@workflow
async def main():
data = await extract(url="https://...")
"""
name = path.rsplit("/", 1)[-1]
_opts = {k: v for k, v in {"timeout": timeout, "tag": tag, "cache_ttl": cache_ttl, "priority": priority, "concurrent_limit": concurrency_limit, "concurrency_key": concurrency_key, "concurrency_time_window_s": concurrency_time_window_s}.items() if v is not None} or None
def wrapper(**kwargs):
ctx = _workflow_ctx.get(None)
if ctx is not None:
return ctx._next_step(name, path, dispatch_type="script", _task_options=_opts, **kwargs)
raise RuntimeError(f'task_script("{path}") can only be called inside a @workflow')
wrapper.__name__ = name
wrapper._is_task = True
wrapper._task_path = path
return wrapper
def task_flow(
path: str,
*,
timeout: Optional[int] = None,
tag: Optional[str] = None,
cache_ttl: Optional[int] = None,
priority: Optional[int] = None,
concurrency_limit: Optional[int] = None,
concurrency_key: Optional[str] = None,
concurrency_time_window_s: Optional[int] = None,
):View on GitHub (pinned to e474e8803c)
Solutions
- Wrap the calling code in @wmill.workflow and run it as a flow so _workflow_ctx is populated
- Replace the task_script call with a direct sub-run API (e.g. wmill.run_script or resume APIs) if you actually want to launch a script outside a flow
- If the code must work in both contexts, check ctx first: ctx = _workflow_ctx.get(None); branch to direct execution when ctx is None
Example fix
// before
def process():
return task_script("/u/scripts/transform")(payload)
// after
import wmill
@wmill.workflow
def process():
return task_script("/u/scripts/transform")(payload) Defensive patterns
Strategy: validation
Validate before calling
import wmill
from wmill.client import _workflow_ctx
if _workflow_ctx.get(None) is None:
raise RuntimeError("task_script requires a @workflow context")
# or: only call tasks inside @wmill.workflow functions Type guard
def in_workflow() -> bool:
from wmill.client import _workflow_ctx
return _workflow_ctx.get(None) is not None Try / catch
try:
result = my_task(x)
except RuntimeError as e:
if "can only be called inside a @workflow" in str(e):
result = run_directly(x) # fallback outside flows
else:
raise Prevention
- Only invoke @task-decorated wrappers inside @wmill.workflow functions
- Add an in_workflow() assert in shared helpers that use task primitives
- When testing tasks locally, replace them with direct function calls or the run API
When it happens
Trigger: Calling a function produced by wmill.task_script(...) inside a plain Python script, a Job/standalone run, a worker context, or any code path that is not executed under the @workflow (wmill.workflow) decorator — i.e. _workflow_ctx.get() returned None.
Common situations: Moving task-heavy code into a plain script for testing without realizing task_script only resolves within a workflow; importing a shared module that calls a @task at module level; refactoring a flow step into a standalone script and forgetting that step wiring (ctx._next_step) requires the workflow runtime.
Related errors
- task_flow("{path}") can only be called inside a @workflow
- wait_for_approval 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/476f9cf5a95d9643.
Report an issue: GitHub.