mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 16:02:36 +00:00
* fix: dispatch workflow-as-code tasks from a deployed flow's inline step * fix: give a workflow-as-code task its own result-cache key * fix: key a cached workflow-as-code task on its name and arguments * fix: hash a cached workflow-as-code task's arguments like any job's * chore: regenerate system prompts for the task cache_ttl docs * fix: key a cached workflow-as-code task on its step key, not its name * fix: key a cached workflow-as-code task on a fingerprint of its code * fix: keep the task() doc attached to task() * fix: key a cached inline task on its step key and the workflow input * docs: cache_ttl has no effect on a taskFlow target
195 lines
9.3 KiB
Markdown
195 lines
9.3 KiB
Markdown
## 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`
|
|
|
|
```python
|
|
# 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.
|
|
#
|
|
# ``cache_ttl`` serves a previous result of the task for that many seconds
|
|
# instead of running it again. A task is keyed on its step key (its name and
|
|
# call order) and the workflow's input, not on the arguments it is called
|
|
# with, so cache one only when whether it runs, and what it receives, follow
|
|
# from the workflow's input alone. A ``task_script`` target is keyed on the
|
|
# arguments it is called with. It has no effect on a ``task_flow`` target,
|
|
# which keeps its flow's own cache policy.
|
|
#
|
|
# 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)
|
|
```
|