Skip to content

Python API

The public labtasker package is synchronous and typed. Every resource operation is available both as a package-level function and as a method with the same name on Client. Methods return Labtasker domain models, not HTTP response wrappers.

Choose the Client form

Package-level functions share one lazily created process-wide Client:

import labtasker

task = labtasker.submit_task({"seed": 7}, routes=["sdxl"])
page = labtasker.list_tasks(status="pending")

Use an explicit Client for a large submission loop, deterministic cleanup, test isolation, or more than one Server:

from labtasker import Client

with Client(url="https://labtasker.example", token=token, queue="paper") as client:
    task = client.submit_task({"seed": 7}, routes=["sdxl"])

Client(url=None, token=None, queue=None) resolves each omitted field through environment, the current directory's .labtasker/config.toml, then defaults. Resolution happens once when the Client is constructed. Later chdir(), config, or environment changes do not retarget it. A non-null queue= on a Task method overrides only that operation's Queue; changing Servers requires a new Client.

close() is idempotent and never stops a local Server. Operations on a closed Client raise RuntimeError("Client is closed."). The package-level default Client has no close/reset API. There is no asynchronous Client.

Client.server_version is a read-only str | None: the normalized PEP 440 Server package version from the latest business response. It is None before a response or when that response has no usable version header. Reading it never makes a request. Older Servers may not advertise their version.

When a response reports a Server older than the Client, the Client writes an advisory warning to stderr recommending an upgrade. Each Client instance warns once per distinct older Server version, including patch and prerelease differences. Results, exceptions, and retries are unchanged; the warning does not mean the current operation is incompatible. There is no version preflight or automatic fallback. Applications needing feature-specific compatibility checks must account for server_version being unknown and still handle operation errors.

Task operations

submit_task(args=None, *, name=None, metadata=None, priority=0,
            max_attempts=3, routes=None, task_id=None, queue=None) -> Task
get_task(task_id, *, queue=None) -> Task
list_tasks(*, status=None, name=None, name_fuzzy=None, filter=None, order_by="created_at",
           descending=True, limit=100, cursor=None, queue=None) -> TaskPage
count_tasks(*, status=None, name=None, name_fuzzy=None, filter=None,
            group_by=None, limit=None, cursor=None, queue=None) -> int | GroupCountPage
update_task(task_id, changes, *, queue=None) -> Task
update_tasks(*, filter, changes, queue=None) -> BulkUpdateResult
cancel_task(task_id, *, queue=None) -> Task
requeue_task(task_id, *, queue=None) -> Task
delete_task(task_id, *, queue=None) -> None
Operation Contract
submit_task Creates one pending Task and returns it. args and metadata default to {}, routes to ["default"], and max_attempts to 3.
get_task Returns one Task or raises APIError with code task_not_found.
list_tasks Returns exactly one TaskPage; it never auto-fetches or streams every match. Selectors are combined with logical AND.
count_tasks Returns an int for the complete selection, or a GroupCountPage when group_by is supplied.
update_task Replaces supplied user-owned fields on one non-running Task and returns the resulting Task.
update_tasks Atomically updates all matching non-running Tasks and returns BulkUpdateResult(matched, updated). A non-empty filter is required.
cancel_task Cancels a pending or running Task. Repeating cancel on a cancelled Task is idempotent.
requeue_task Accepts pending, failed, or cancelled; returns it to pending, resets attempt to 0, and clears last_error.
delete_task Permanently deletes one non-running Task and returns None. Deleting an absent Task is idempotent.

Grouped counts

Use an ordered sequence of dimensions, not a comma-separated Python string:

page = client.count_tasks(status="pending", group_by=["routes", "status"])
for group in page.items:
    print(group.key, group.count)

Task dimensions are routes and status, singly or together in either order. GroupCountPage contains group_by, count, items: list[CountGroup], and next_cursor. Each CountGroup has key: dict[str, str] and count. The top-level count covers the complete selection. Multi-route Tasks appear in each compatible route group, so summing groups can exceed that total.

limit defaults to 100 groups and accepts 1–1000; cursor continues one page. Both require grouping. Groups sort by their keys in the requested dimension order. Follow cursors with the same selection and grouping; page size may change. Each page reads current data, so concurrent changes can affect later pages. Empty, repeated, whitespace-containing and unsupported dimensions are rejected. An old Server's scalar response to a grouped request raises TransportError.

Worker observations

list_workers(*, filter=None, limit=100, cursor=None, queue=None) -> WorkerPage
count_workers(*, filter=None, group_by=None, limit=None, cursor=None,
              queue=None) -> int | GroupCountPage

These methods also have package-level forms. WorkerPage contains items and next_cursor, sorted by Worker ID ascending with the same 1–1000 page limit. Each WorkerObservation exposes id, queue, route, status, nullable task_id, last_seen_at, and expires_at. Timestamps are UTC. All seven fields support the query language operators for their types.

workers = client.list_workers(filter='route == "sdxl" and status == "idle"')
counts = client.count_workers(group_by=["route", "status"])

Worker grouping supports route and status, singly or together. Plain counting returns an integer; grouped counting follows the Task page contract above. Only unexpired observations are returned. idle means awaiting work; busy includes execution, reporting and cleanup after finish(). Observations renew periodically and can be delayed. An advisory task_id may refer to a terminal or deleted Task. Use Task state for ownership and recovery decisions. Worker observation failures never block Task execution or consume the failure guard. There are no public Worker control methods.

Task submission details

Submission and idempotency

Task data must use strict JSON-compatible Python values. args, metadata, and result are objects; arrays are Python lists; object keys are strings. NaN, Infinity, cycles, arbitrary objects, and integers outside signed 64-bit range are rejected before transport.

When task_id is omitted, the Client generates one before its first network attempt. For retry safety across caller process restarts, persist a caller-chosen ID matching t_[A-Za-z0-9_-]{12} and replay the complete definition:

task = labtasker.submit_task(
    {"seed": 7},
    task_id="t_AbCdEf0123-_",
    name="baseline-seed-7",
    routes=["sdxl"],
)

The same ID and normalized definition return the Task's current representation, even if it is now running or terminal. A different definition at that ID raises APIError(code="task_id_conflict"); it never updates or overwrites the Task. Object-key order, route input order, and explicitly supplied default values do not change the normalized definition.

Selection and pagination

status, exact name, fuzzy name_fuzzy, and filter are combined with AND. limit must be from 1 through 1000. Ordering is stable and supports id, name, status, priority, attempt, max_attempts, last_route, and the public timestamps.

Name search uses list_tasks(name_fuzzy="tr ev") or count_tasks(name_fuzzy="tr ev"). It ignores case and outer whitespace; every whitespace-separated word must be a subsequence of the name, independently of word order. Empty searches do not restrict names. Punctuation is literal and ordering is unchanged. Use name="train_model_eval" or filter='name == "train_model_eval"' for strict equality.

Follow next_cursor with the same Queue, selectors, filter, order field, and direction:

page = labtasker.list_tasks(filter='status == "failed"', limit=100)
tasks = list(page.items)
while page.next_cursor is not None:
    page = labtasker.list_tasks(
        filter='status == "failed"',
        limit=100,
        cursor=page.next_cursor,
    )
    tasks.extend(page.items)

See Query language for the filter grammar. A cursor is opaque and is not a Task ID or offset.

Updates and lifecycle

changes is a TaskUpdate dictionary containing at least one of name, args, metadata, priority, max_attempts, routes, or result. Every supplied object or list is a complete replacement; there is no merge, dot-path patch, or add/remove operator. Unspecified fields remain unchanged.

Bulk update is one Server transaction. matched counts filter matches that are still non-running at execution time; updated counts rows whose stored value actually changed. A concurrent claim either sees the complete new values or wins first and excludes that running Task. If one matched non-running Task violates a state-dependent rule, the whole batch rolls back.

Current state Update Cancel Requeue Delete
pending yes yes yes yes
running no yes no no
succeeded yes no no; submit a new Task to rerun yes
failed yes no yes yes
cancelled yes idempotent yes yes

Lifecycle is changed only through these explicit actions; status is never a writable update field.

Queue operations

create_queue(name) -> Queue
list_queues() -> list[Queue]
delete_queue(name, *, cascade=False) -> None

create_queue is idempotent and returns the Queue. list_queues returns the complete, unpaginated list. Deleting an empty Queue succeeds without cascade; a non-empty Queue requires cascade=True. Deletion is rejected while any Task in the Queue is running, even with cascade. A successful cascade atomically deletes the Queue and every Task in it; it does not delete Worker journals or external artifacts.

The default Queue is created only with a fresh database. If explicitly deleted, it must be explicitly recreated.

Response models

Response models are frozen, strict Pydantic models. Known fields keep stable types; unknown response fields are ignored so a newer v2 Server may add optional fields. Use model_dump(mode="json") for a JSON-ready representation. Mutating a dict inside a returned model changes only that local object and never updates the Server.

Model Public fields
Task id, queue, status, name, args, metadata, priority, attempt, max_attempts, routes, result, last_error, last_route, created_at, updated_at, started_at, finished_at
TaskPage items: list[Task], next_cursor: str | None
BulkUpdateResult matched: int, updated: int
Queue name: str
LastError type, message, traceback, occurred_at, attempt, run_id
TaskInfo Every Task field plus the active run_id and absolute local run_dir

Task states are exactly pending, running, succeeded, failed, and cancelled. Timestamps are timezone-aware UTC datetime values.

Worker API

@loop(route="default", queue=None, idle_timeout=300,
      force_stop_timeout=None, max_consecutive_failures=5)
def worker(...): ...

TaskArg(default=..., path=None, resolver=None)
task_info() -> TaskInfo
finish(result=None, *, skip_if_no_labtasker=False) -> None
cancellation_requested() -> bool
set_force_stop_timeout(seconds: float | None) -> None

Only parameters marked by TaskArg(...) bind from Task args. Binding is strict and happens after claim; ordinary return succeeds with {}. finish() accepts one JSON object and completes the Task before local cleanup continues. It is stable once accepted and may be called only once. The context helpers require an active Worker execution; cancellation and force-stop helpers require a Python Worker execution.

See Python Workers for binding, cancellation, failure, and Worker-lifetime semantics.

Errors and retries

Invalid Python arguments raise ValueError before transport. Using an active-run helper outside its valid context raises RuntimeError. Operational failures use:

Exception Meaning
ConfigError Invalid current configuration. Exposes code, message, and details.
TransportError No usable Labtasker response: connection, timeout, local startup, malformed protocol, or incompatible response. Its code is transport_error.
APIError A valid Server rejection. Exposes status_code, stable code, readable message, and structured details.

Reads, list/count, and idempotent Task creation use bounded transport retries. Ordinary lifecycle, update, and deletion mutations are not automatically retried after an uncertain response; inspect current state and decide explicitly.

TransientError, TaskError, and FatalWorkerError are Worker outcome signals, not Client-operation errors and not subclasses of LabtaskerError.