Skip to main content

cairnq (Python)

SQLite-first, cross-language, storage-centered durable task runtime. The Python SDK. API and worker processes coordinate only through a shared SQLite file.

from cairnq import CairnQ, Worker

# Worker side — a handler always receives (ctx, payload).
worker = Worker.sqlite("tasks.db")

@worker.task                          # registered under the function name, "create_summary"
async def create_summary(ctx, payload):
    await ctx.progress(0.2, "reading")
    return {"summary": await llm.summarize(payload["text"])}

worker.serve()                        # blocking entry point; Ctrl-C closes cleanly

# API side (in your server) — submit returns immediately.
tasks = CairnQ.sqlite("tasks.db")
task = await tasks.submit("create_summary", {"text": text}, key=f"summary:{aid}")

@worker.task defaults the task name to the function's name. Pass a string for a dotted/namespaced name: @worker.task("summary.create").

Synchronous call (submit + wait):

from cairnq import TaskFailed, TaskTimeout

try:
    result = await tasks.call("create_summary", {"text": text}, timeout_ms=10_000)
except TaskFailed as e:
    log(e.code, e.message, e.retryable)   # envelope fields, no e.error["code"] digging
except TaskTimeout as e:
    # The task keeps running — resume the wait instead of submitting again.
    result = await tasks.wait(e.task_id, timeout_ms=60_000)
    # …or tasks.wait_by_key(key), from a process that never held the id.

Inspect a task by id/key without memorizing status strings:

task = await tasks.get_by_key(key)
if task and task.succeeded:        # also .failed / .canceled / .running / .queued / .is_terminal
    use(task.result)

Optionally define a task once and share the symbol across both ends — the name lives in one place (no string drift), and call() is typed as the task's result:

from cairnq import TaskDef

summarize = TaskDef[dict, dict]("summarize")

@worker.task(summarize)            # registered under summarize.name
async def handle(ctx, payload): ...

result = await tasks.call(summarize, {"text": text})

Opt-in: every API still accepts a plain name string (cross-language callers use it).

Running it in production

worker = Worker.sqlite(
    "tasks.db",
    concurrency=4,            # handler calls at once (a batch call counts as one)
    retry_backoff_ms=1_000,   # window doubles per attempt, capped at retry_backoff_max_ms (30s),
                              # jittered over its upper half; 0 disables
    on_error=lambda exc, info: log.warning("worker survived %s: %s", info, exc),
)

# Nothing else deletes rows, so give the client a retention cutoff — it sweeps
# terminal tasks in bounded batches for as long as the handle is open. Tiered
# retention is the same option in its wider forms: a per-status map, or a list
# of RetentionRule filtering by anything purge() can (queue, status, name).
tasks = CairnQ.sqlite("tasks.db", retention=7 * 24 * 3600_000)

A sync handler (def, not async def) is dispatched to a thread, so the usual shape around a blocking GPU or HTTP call keeps the worker's event loop — and with it every lease this worker holds — alive:

@worker.task("score")
def score(ctx, payload):
    return {"score": model.forward(payload["image"])}  # blocking, off the loop

A handler that does real side effects should bail out when it loses its lease — the task is already running on another worker and nothing it writes is recorded:

@worker.task("long.job")
async def long_job(ctx, payload):
    for chunk in chunks:
        if ctx.lost_lease or await ctx.canceled():
            return
        await process(chunk)

Multi-host

Same code, Postgres instead of the file — CairnQ.postgres(dsn) / Worker.postgres(dsn). Install with pip install cairnq[postgres].

schema puts cairnq's tables in a schema of their own: CairnQ.postgres(dsn, schema="cairnq") creates it if absent and sets search_path on every connection. Every process in a deployment must agree on it — a queue whose API and worker resolve to different schemas is two empty queues, and both sides come up healthy. cairnq refuses to connect where it can see that about to happen; the TypeScript SDK takes the same option and applies the same rule.

Sharing the application's connection

Given a PgExecutor instead of a DSN, cairnq runs inside a session the application already has — no second driver, no second pool:

from cairnq import CairnQ, PgExecutor

executor: PgExecutor = ...   # ~40 lines over your driver
tasks = CairnQ.postgres(executor)

An adapter passes rows through as its driver produced them; cairnq normalizes what the drivers disagree about. An executor cairnq was handed is never closed by cairnq.

That shared session is also what lets a task's settlement commit together with the rows the task produced:

@worker.task("render.document")
async def render_document(ctx, payload):
    rendered = await render(payload)

    async def write(session):
        await session.query("insert into pages (doc, n) values ($1, $2)", [...])
        return {"pages": len(rendered)}   # becomes the task's result

    return await ctx.succeed_in(write)

Without it the two are separate transactions, and a crash between them leaves work durable while the task still reads as running — on retry, recomputed. If the lease turns out to be gone, the settlement matches no row and the caller's writes roll back with it.

The protocol (schema + canonical SQL) lives in ../cairnq-protocol and is shared verbatim with the TypeScript SDK. See ../cairnq-protocol/PROTOCOL.md.

Metadata

Release files for cairnq 0.15.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cairnq 0.15.1
File Size Uploaded
cairnq-0.15.1.tar.gz 177.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for cairnq 0.15.1
File Interpreter ABI Platform
cairnq-0.15.1-py3-none-any.whl Python 3 none any Details

Total release size: 325.3 kB

Release files / cairnq-0.15.1.tar.gz

Download URL cairnq-0.15.1.tar.gz
Size 177.5 kB
Tags Source
SHA-256 checksum
How to use checksums
45861073daa51290738c34149c1d3d9849127105c3cb7f5915cdac7deff64751
BLAKE2b-256 checksum
How to use checksums
60dca4e5607c2b512a3266c72291dd235e13808f92a820ce77b33c735bf17e15
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.

Transparency log

Release files / cairnq-0.15.1-py3-none-any.whl

Download URL cairnq-0.15.1-py3-none-any.whl
Size 147.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
57c77c15a367831d37a6d1d940cc27d49a873d5b8e6cd3d01b0b46435ce36e52
BLAKE2b-256 checksum
How to use checksums
51a4b97b30f08492d3246e7b98eafa01b63fd495921c696c1317012da07b8969
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.15.1 This release

2 release files

0.15.0

2 release files

0.14.0

2 release files

0.13.0

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page