OutlabsTaskq
API

Workflows and schedules

Sealed workflows with dependency edges, plus database-time recurring schedules.

Sealed workflows

Create a workflow, attach dependency edges, then seal it. Until sealed, the graph is mutable; after seal, jobs move through blockedqueued as dependencies succeed.

from taskq import WorkflowKind

workflow = await tq.create_workflow(
    "publish-wave:2026-08-02",
    WorkflowKind.DAG,
    declared_queues=("publishing",),
    actor="api",
)

prepare = await tq.enqueue(
    PREPARE,
    {"article_id": "article-123"},
    workflow_id=workflow.workflow_id,
    step_key="prepare",
)
await tq.enqueue(
    PUBLISH,
    {"article_id": "article-123"},
    workflow_id=workflow.workflow_id,
    step_key="publish",
    depends_on=(prepare.job_id,),
)
await tq.seal_workflow(workflow.workflow_id, actor="api")

Workflow keys are caller-chosen durable identities. Declare every queue the workflow may use, give each member a unique step_key, and add dependencies by job id before sealing. A sealed DAG is immutable.

Use sealed workflows when you need multi-job dependency graphs. Prefer atomic follow-ups for simple parent→child fan-out on settle.

Shipped in migrations 0009_workflows.sql (ADR-026).

Database-time schedules

Recurring definitions live in Postgres and use database time. The standalone taskq scheduler run process evaluates recurrence and durably enqueues ordinary jobs; application workers execute their handlers.

topology
one scheduler clock -> queued TaskQ job -> application worker -> handler

The scheduler never imports application task registries or routers. See Standalone Scheduler for target binding, process placement, activation, monitoring, and rollback.

Source-owned manifests

Use YAML manifests for deployment-owned recurring work:

schedules.yaml
version: 1
namespace: myapp
source: deployment
schedules:
  nightly-cleanup:
    display_name: Nightly cleanup
    task: cleanup.run
    queue: maintenance
    cron: "0 3 * * *"
    timezone: UTC
    catchup: fire_once
    overlap: forbid
    max_lateness_seconds: 3600
    state: paused
Terminal
taskq --context staging schedule manifest plan schedules.yaml -o json
taskq --context staging schedule manifest apply schedules.yaml \
  --plan-digest "$PLAN_DIGEST" -o json

plan is read-only. apply is source-scoped, version-checked, and does not prune missing keys. Retire one removed key explicitly with taskq --context staging --yes schedule manifest retire.

In the current SQL contract, catchup: skip advances without enqueueing—even during ordinary continuous polling. Every recurring application manifest must set catchup: fire_once or catchup: fire_all explicitly.

Low-level operator API

Use put_schedule when schedules are genuinely dynamic application data rather than deployment-owned desired state:

dynamic_schedule.py
from taskq import (
    ScheduleCatchupPolicy,
    ScheduleCronRecurrence,
    ScheduleDefinition,
    ScheduleJobTarget,
)
from taskq.sql.transport import SqlTaskqTransport

definition = ScheduleDefinition(
    target=ScheduleJobTarget(
        queue="maintenance",
        job_type="cleanup.run",
        payload={},
    ),
    recurrence=ScheduleCronRecurrence(
        expression="0 3 * * *",
        timezone="UTC",
    ),
    catchup_policy=ScheduleCatchupPolicy.FIRE_ONCE,
    max_catchup=1,
    paused=True,
)

operator = SqlTaskqTransport.from_dsn(
    OPERATOR_DATABASE_URL,
    expected_environment="staging",
)
try:
    await operator.put_schedule(
        "nightly.cleanup",
        definition,
        actor="ops",
    )
finally:
    await operator.aclose()

put_schedule is an operator-transport operation, not a producer convenience method on TaskQ. Use a restricted taskq_operator connection; never give operator rights to producers or runners.

The base schedule contract shipped in migration 0010. Migrations 0019 and 0020 add target attestation, the standalone scheduler, durable decisions, manifest ownership, overlap/lateness policy, and auto-pause.

Finite projections

Observer-facing ready / running / finished / workflow pages are finite projections over the job ledger (migrations 00110013, ADR-029). Bounded worker presence is activated in 00140015 (ADR-033). Native workflow-member continuations are added and activated in 00160017.