Skip to content

Background work runs as a task, not a workflow - #316

Merged
czpython merged 2 commits into
mainfrom
commonzenpython/eng-826-task-author-door
Aug 24, 2026
Merged

Background work runs as a task, not a workflow#316
czpython merged 2 commits into
mainfrom
commonzenpython/eng-826-task-author-door

Conversation

@czpython

Copy link
Copy Markdown
Owner

A Workflow carries a run row, a subject timeline, gates, and operator-tunable settings. That weight is right for work an operator watches on a board, and wrong for plumbing — periodic maintenance, a fire-and-forget side effect. Until now the only door was Workflow, so plumbing paid the full tax.

task is the lighter door: a durable, replay-recoverable function that runs its body in one checkpointed step.

from druks.workflows import task


@task(every="*/15 * * * *")
async def refresh_tokens() -> None:
    ...


@task(retries=4)
async def sync_labels(pull_request_id: int) -> None:
    ...
  • every= runs it on a fixed UTC cadence the code owns — a workflow's every= stays the one an operator can retune.
  • await sync_labels.enqueue(pull_request_id=7) runs one durably in the background, from a route, a subscriber, or a workflow body.
  • retries= sets retries after the first attempt; @step now takes the same knob.
  • A task keeps no run row and never reaches the timeline; it has no subject, gate, or operator settings, and cannot make agent calls. That last line is the boundary — agent work stays a Workflow.

The token- and model-refresh chores move to core/tasks.py. They never belonged on the run timeline, and their cadence is no longer an operator setting. Apps declare tasks in a tasks.py capability module, discovered like workflows.py.

@czpython
czpython force-pushed the commonzenpython/eng-826-task-author-door branch from 952fcc3 to f3e4335 Compare August 24, 2026 07:22
A Workflow carries a run row, a subject timeline, gates, and operator-tunable
settings — the weight is right for work an operator watches, and wrong for
plumbing. `task` is the lighter door: a durable, replay-recoverable function
that runs its body in one checkpointed step, with no run row, no timeline, no
subject, and no agent calls.

    @task(every="*/15 * * * *")
    async def refresh_tokens() -> None: ...

    @task(retries=4)
    async def sync_labels(pull_request_id: int) -> None: ...

`every=` runs it on a fixed cadence the code owns; `.enqueue(**kwargs)` runs one
in the background from a route, a subscriber, or a workflow body. `retries=`
sets retries after the first attempt, and `@step` now takes the same knob.

The token- and model-refresh chores become tasks — they never belonged on the
run timeline, and their cadence is no longer an operator setting. Apps declare
tasks in a `tasks.py` capability module.
A duplicate durable name raises at declaration instead of riding DBOS's
warn-and-overwrite, enqueue() validates the input model synthesized from
the signature and stores JSON — never a pickled live object — and refuses
to run inside a @step, where a retry would enqueue again. Doctor now also
flags a @task hiding in an off-canon module the discovery walk never
imports.
@czpython
czpython force-pushed the commonzenpython/eng-826-task-author-door branch from 60df8c7 to 0f12587 Compare August 24, 2026 08:09
@czpython
czpython merged commit 00cab55 into main Aug 24, 2026
1 check passed
@czpython
czpython deleted the commonzenpython/eng-826-task-author-door branch August 24, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant