Files
windmill/system_prompts/auto-generated/sdks/wac-python.md
T
63cb46d7bb feat: add a minimal skin for the approval page and slack/teams (#11061)
* feat: add an approval skin to the approval page and slack/teams messages

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* chore: point ee-repo-ref at the teams approval skin commit

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* fix: resolve the approval skin from the step awaiting approval

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* fix: shorten the slack approval message to fit the button value limit

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* fix: rename skins to detailed/minimal and keep long slack messages

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* feat: title the minimal approval page from the step and flow summaries

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* feat: let wait_for_approval set the description approvers see

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* fix: keep a finished workflow's approval description, still gated

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* fix: keep a login-required approval locked after the run moves on

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* feat: hide the windmill version on the approval page

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* chore: update ee-repo-ref to e92abc9d1fba3ba898640df0cfeb0af8a50849b4

This commit updates the EE repository reference after PR #786 was merged in windmill-ee-private.

Previous ee-repo-ref: bf1766ff49458f62d3f11746f1a06893ae3c2325

New ee-repo-ref: e92abc9d1fba3ba898640df0cfeb0af8a50849b4

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-09-10 18:50:09 +00:00

8.8 KiB

Python Workflow-as-Code API (wmill)

Import: from wmill import workflow, task, task_script, task_flow, step, sleep, wait_for_approval, get_approval_urls, get_resume_urls, parallel, TaskError

# Raised when a WAC ``task`` or ``step`` failed.
#
# Attributes:
#     step_key: The checkpoint key of the failed step.
#     child_job_id: The UUID of the failed child job, or ``None`` for a
#         ``step()``, which runs in the workflow job and has no child job.
#     result: ``{"error": {"name", "message", "stack"?, "extra"?}}`` — the
#         same shape whether a task or a step failed. ``name`` and ``message``
#         are always present; ``stack`` only when the failure had a traceback,
#         and ``extra`` only when it carried custom fields of its own, dropped
#         with ``extra_omitted: True`` beside it when too large to checkpoint.
class TaskError(Exception):
    def __init__(self, message: str, *, step_key: str = '', child_job_id: Optional[str] = None, result = None)

# Get URLs needed for resuming a flow after suspension.
#
# Args:
#     approver: Optional approver name
#     flow_level: If True, generate resume URLs for the parent flow instead of the
#         specific step. This allows pre-approvals that can be consumed by any later
#         suspend step in the same flow.
#
# Returns:
#     Dictionary with approvalPage, resume, and cancel URLs
def get_resume_urls(approver: str = None, flow_level: bool = None) -> dict

# Decorator that marks a function as a workflow task.
#
# Works in both WAC v1 (sync, HTTP-based dispatch) and WAC v2
# (async, checkpoint/replay) modes:
#
# - **v2 (inside @workflow)**: dispatches as a checkpoint step.
# - **v1 (WM_JOB_ID set, no @workflow)**: dispatches via HTTP API.
# - **Standalone**: executes the function body directly.
#
# A task runs as its own job, so its result is always encoded as JSON and
# decoded back before the caller sees it: a ``datetime`` comes back as a
# string, a tuple as a list.
#
# ``retry`` re-dispatches the task after a failure, inside ``@workflow`` only.
# Every attempt is a step of its own (``call_api``, ``call_api#2``, ...) and
# the wait between two of them is a durable sleep, so a retrying task holds no
# worker while it backs off. Keys: ``attempts`` (retries after the first
# failure, a whole number from 0 to 100), ``delay`` (seconds before the first
# retry, sub-second delays dropped), ``multiplier`` (applied to the delay
# after each attempt, 1 keeps it constant), ``max_delay`` (ceiling in
# seconds). ``attempts`` is required, and an out-of-range or unknown key is
# rejected where the policy is written.
#
# A workflow sleeps once per round, so tasks backing off in the same fan-out
# wait one after another rather than together: the delay before a fan-out
# retries is the sum of every backoff pending in it, not the longest one, and
# it grows with both the width of the fan-out and ``attempts``. Retries with
# no ``delay`` all go out in a single round.
#
# Usage::
#
#     @task
#     async def extract_data(url: str): ...
#
#     @task(path="f/external_script", timeout=600, tag="gpu")
#     async def run_external(x: int): ...
#
#     @task(retry={"attempts": 3, "delay": 30, "multiplier": 2})
#     async def call_api(payload: dict): ...
def task(_func = None, *, path: Optional[str] = None, tag: Optional[str] = None, timeout: Optional[int] = 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, retry: Optional[dict] = None)

# Create a task that dispatches to a separate Windmill script.
#
# ``retry`` takes the same policy as :func:`task`.
#
# Usage::
#
#     extract = task_script("f/data/extract", timeout=600)
#
#     @workflow
#     async def main():
#         data = await extract(url="https://...")
def task_script(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, retry: Optional[dict] = None)

# Create a task that dispatches to a separate Windmill flow.
#
# ``retry`` takes the same policy as :func:`task`.
#
# Usage::
#
#     pipeline = task_flow("f/etl/pipeline", priority=10)
#
#     @workflow
#     async def main():
#         result = await pipeline(input=data)
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, retry: Optional[dict] = None)

# Decorator marking an async function as a workflow-as-code entry point.
#
# The function must be **deterministic**: given the same inputs it must call
# tasks in the same order on every replay. Branching on task results is fine
# (results are replayed from checkpoint), but branching on external state
# (current time, random values, external API calls) must use ``step()`` to
# checkpoint the value so replays see the same result.
def workflow(func)

# Execute ``fn`` inline and checkpoint the result.
#
# On replay the cached value is returned without re-executing ``fn``.
# Use for lightweight deterministic operations (timestamps, random IDs,
# config reads) that should not incur the overhead of a child job.
#
# ``fn``'s result is encoded as JSON and decoded back before it is returned,
# so the round that runs the body sees the same types every replay sees:
# a ``datetime`` comes back as a string, a tuple as a list.
async def step(name: str, fn)

# Server-side sleep — suspend the workflow for the given duration without holding a worker.
#
# Inside a @workflow, the parent job suspends and auto-resumes after ``seconds``.
# Outside a workflow, falls back to ``asyncio.sleep``.
async def sleep(seconds: int)

# Suspend the workflow and wait for an external approval.
#
# Pass ``key`` to name the step, then ``get_approval_urls(key)`` yields the URLs
# that resume exactly this approval — route them through your own channel.
# Without a key the steps are named ``approval``, ``approval_2``, ...
#
# Returns a dict with ``value`` (form data), ``approver``, and ``approved``.
#
# 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.
#     skin: ``"minimal"`` shows approvers only the request (form and approve/reject)
#         instead of the detailed page with the workflow's details.
#     description: Shown to approvers above the form: a string, or a rich value such as
#         ``{"markdown": "..."}``.
#
# 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)
async def wait_for_approval(timeout: int = 1800, form: dict | None = None, self_approval: bool = True, key: str | None = None, skin: Literal['detailed', 'minimal'] | None = None, description: str | dict | None = None) -> dict

# Get the resume/cancel/approval-page URLs bound to one ``wait_for_approval`` step.
#
# Unlike :func:`get_resume_urls`, which signs a random nonce, these address the
# very ``resume_job`` record the step's built-in approval buttons use, so they
# are stable across replays and safe to embed in a custom notification.
#
# Args:
#     step_key: Checkpoint key of the approval step, as passed to
#         ``wait_for_approval(key=...)``. Keys must be unique within a workflow;
#         reusing one raises rather than silently renaming it. The URL only
#         resumes while that step is awaiting approval; used at any other moment
#         it is rejected rather than banking a row a different approval would
#         consume. Send it ahead of time — approvers just cannot act before the
#         workflow reaches the step.
#         ``resume`` and ``cancel`` are step-bound; ``approvalPage`` is not — it
#         opens the job's approval page, which acts on whichever approval is
#         pending when it is used.
#     approver: Optional approver name
#
# Returns:
#     Dictionary with approvalPage, resume, and cancel URLs
def get_approval_urls(step_key: str = 'approval', approver: str = None) -> dict

# 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)
async def parallel(items, fn, *, concurrency: Optional[int] = None)