Built-in tasks
A tour of flux.tasks — the standard library of ready-made tasks for time, randomness, concurrency, pausing, progress reporting, and cross-workflow calls.
flux.tasks ships a small standard library of tasks you can drop into any workflow. Each one follows the same rules as a hand-written task: the result is recorded in the execution log, replays skip the call, and the task runs inside the current ExecutionContext. Import what you need and await it.
from flux.tasks import now, uuid4, sleep, choice, randint, randrange
from flux.tasks import parallel, pipeline
from flux.tasks import pause, progress, call
now — deterministic timestamps
now() -> datetime
Returns datetime.now() as a durable task. Calling datetime.now() directly inside a workflow is unsafe: on a replay the call returns a different value, which breaks determinism. now records the timestamp on the first run and returns the recorded value on every subsequent replay.
from flux import ExecutionContext
from flux.tasks import now
from flux.workflow import workflow
@workflow
async def timestamped_job(ctx: ExecutionContext):
started_at = await now()
# ... do work ...
return {"started_at": str(started_at)}
Use now wherever you need the current time inside a workflow body. Avoid time.time(), datetime.utcnow(), or any other live clock call for the same reason.
uuid4 — durable unique IDs
uuid4() -> uuid.UUID
Generates a uuid.UUID (version 4) and records it. Like now, calling uuid.uuid4() directly is non-deterministic: each replay produces a different UUID. Using the task ensures the same ID is returned every time the workflow runs through that point.
from flux import ExecutionContext
from flux.tasks import uuid4
from flux.workflow import workflow
@workflow
async def create_order(ctx: ExecutionContext[dict]):
order_id = await uuid4()
return {"order_id": str(order_id), "payload": ctx.input}
The returned value is a uuid.UUID object. Convert to string with str(order_id) when you need a plain text representation.
sleep — durable delays
sleep(duration: float | timedelta)
Pauses workflow execution for duration seconds. Accepts either a plain float (number of seconds) or a datetime.timedelta.
from datetime import timedelta
from flux import ExecutionContext
from flux.tasks import sleep
from flux.workflow import workflow
@workflow
async def polling_loop(ctx: ExecutionContext[str]):
for attempt in range(5):
result = await check_external_api(ctx.input)
if result["ready"]:
return result
await sleep(timedelta(seconds=30)) # wait 30 s between checks
return {"status": "timed out"}
choice, randint, randrange — durable randomness
Raw calls to the random module are non-deterministic: each replay picks a different value. These three tasks wrap the corresponding random calls as durable tasks so the result is fixed after the first execution.
choice
choice(options: list[Any]) -> Any
Returns a randomly selected element from options. The return type matches the element type of the list.
from flux import ExecutionContext
from flux.tasks import choice
from flux.workflow import workflow
REGIONS = ["us-east-1", "eu-west-1", "ap-southeast-1"]
@workflow
async def deploy_canary(ctx: ExecutionContext[str]):
region = await choice(REGIONS)
return {"image": ctx.input, "region": region}
randint
randint(a: int, b: int) -> int
Returns a random integer N such that a <= N <= b. Both endpoints are inclusive, matching the behaviour of random.randint.
from flux import ExecutionContext
from flux.tasks import randint
from flux.workflow import workflow
@workflow
async def roll_dice(ctx: ExecutionContext):
die1 = await randint(1, 6)
die2 = await randint(1, 6)
return {"roll": die1 + die2}
randrange
randrange(start: int, stop: int | None = None, step: int = 1)
Returns a randomly selected integer from range(start, stop, step). Mirrors random.randrange with the same argument semantics: stop is exclusive, step defaults to 1.
from flux import ExecutionContext
from flux.tasks import randrange
from flux.workflow import workflow
@workflow
async def pick_even(ctx: ExecutionContext):
# Pick a random even number between 0 and 98 (inclusive)
n = await randrange(0, 100, 2)
return n
parallel — concurrent fan-out
parallel(*functions: Coroutine) -> list[Any]
Runs multiple task coroutines concurrently and returns their results as a list in the order the arguments were passed. This is the right primitive when tasks do not depend on each other’s output.
parallel has a dedicated page with full examples, .map() fan-out, and error semantics. See Tasks → parallel.
pipeline — linear chaining
pipeline(*tasks: Callable, input: Any)
Runs a sequence of tasks left to right, passing the output of each step as the input to the next, which fits linear data transformations cleanly. pipeline has a dedicated page with full examples and error propagation details. See Tasks → pipeline.
pause — human-in-the-loop checkpoints
pause(name: str, output: Any = None) -> Any
Suspends the workflow at the call site and waits for an external resume signal. When the workflow resumes, pause returns whatever value was passed to the resume call — typically a human decision, an approval, or a form submission.
from flux import ExecutionContext
from flux.tasks import pause
from flux.workflow import workflow
@workflow
async def document_approval(ctx: ExecutionContext[dict]):
doc = ctx.input
# Workflow pauses here; the execution_id is surfaced to the requester
decision = await pause(
"review-document",
output={"document": doc["title"], "action": "approve_or_reject"},
)
if decision == "approved":
return {"status": "approved", "title": doc["title"]}
return {"status": "rejected", "title": doc["title"]}
The output argument is optional payload surfaced to the resume caller. Use it to pass context that helps a person or external system decide what to do next. The string name is a label that appears in the execution log and is used to route resume signals correctly when a workflow has multiple pause points.
For the full pause-and-resume lifecycle — including how to send the resume signal via CLI, REST API, or SDK — see Workflow control → pause and resume.
progress — streaming status updates
progress(value: Any) -> None
Emits an ephemeral progress event from inside a task. Progress events are streamed to connected clients in real time but are never written to the execution log and never replayed. They are fire-and-forget signals for dashboards and long-running task feedback loops.
from flux import ExecutionContext
from flux.task import task
from flux.tasks import progress
from flux.workflow import workflow
@task
async def process_batch(items: list[str]) -> list[str]:
results = []
for i, item in enumerate(items):
await progress({"step": i + 1, "total": len(items), "item": item})
results.append(item.upper())
return results
@workflow
async def batch_job(ctx: ExecutionContext[list]):
return await process_batch(ctx.input)
progress can be called multiple times per task, and the value can be anything JSON-serialisable: a percentage, a dict with step metadata, a log line, or a custom object. Because progress events are ephemeral, the task’s durable output is still only the value returned from the function.
For how to consume these events over SSE from the server, see Workflows → streaming output.
call — invoke another workflow
call(workflow: workflow | str, *args, mode: Literal["sync", "async"] = "sync") -> Any
Calls a registered workflow via the Flux HTTP API from inside a running workflow. Use it for cross-workflow orchestration when you need to trigger a workflow registered under a different name or namespace, or when you want the child workflow’s execution tracked independently.
from flux import ExecutionContext
from flux.tasks import call
from flux.workflow import workflow
@workflow
async def orchestrator(ctx: ExecutionContext[dict]):
payload = ctx.input
# Synchronous: waits for the child workflow to finish, returns its output
result = await call("default/data-pipeline", payload)
# Asynchronous: submits and returns the execution_id immediately
execution_id = await call("default/notification-sender", payload, mode="async")
return {"pipeline_result": result, "notification_id": execution_id}
mode="sync" (the default) blocks until the child workflow completes and returns its output. mode="async" submits the workflow and returns the execution_id string immediately, leaving the child to run independently.
Example: combining primitives
A workflow that generates a unique order ID, waits for human approval, then fans out work in parallel:
from flux import ExecutionContext
from flux.task import task
from flux.tasks import uuid4, pause, parallel
from flux.workflow import workflow
@task
async def process_item(item: str) -> str:
return item.strip().upper()
@workflow
async def order_pipeline(ctx: ExecutionContext[list]):
order_id = await uuid4()
decision = await pause("approve-order", output={"order_id": str(order_id)})
if decision != "approved":
return {"order_id": str(order_id), "status": "rejected"}
results = await parallel(*(process_item(item) for item in ctx.input))
return {"order_id": str(order_id), "items": results, "status": "completed"}
What’s next
- Parallel tasks — full coverage of fan-out patterns with
paralleland.map(). - Pipeline tasks — linear chaining with
pipeline. - Workflow control → pause and resume — the complete pause lifecycle: sending resume signals, routing multiple pause points, and building approval UIs.
- Workflows → streaming output — consuming
progressevents over SSE.