Library / SDK
dbos-inc/dbos-transact-py avatar
dbos-inc/dbos-transact-py

DBOS Transact checkpoints workflow steps into Postgres rows

Database-Backed Durable Python Workflows

1,646 stars108 forksPythonMIT

At a glance

What is it?
DBOS Transact is a Python library that makes functions durable by writing their state into Postgres, so a crashed process resumes from the last completed step. It replaces a separate orchestrator and queue broker with annotations plus a database, at the cost of making Postgres a hard runtime dependency.
Who is it for?
Adopt DBOS Transact when your Python service already runs on Postgres and you are hand-rolling retry and checkpoint logic, since the annotation plus database approach removes an orchestrator, a broker and a state store from the architecture.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Postgres is the orchestrator, the queue and the state store

DBOS provides lightweight durable workflows built on top of Postgres, and the pitch is that you skip managing a workflow orchestrator or task queue. The README is blunt about the trade: instead of extra infrastructure to configure and manage, you annotate your existing code. Its stated audience is applications that need to reliably handle failures, with a payments service that must survive a mid-operation crash and a data pipeline that should resume from checkpoints as the two named examples.

The mechanism is checkpointing. A durable workflow writes its state into Postgres, so if the program fails and restarts, workflows resume from the last completed step. Nothing else in the design is a separate service. Queues do not need a queueing service or a message broker, which the README states in as many words.

The dependency list backs that up. `pyyaml`, `python-dateutil`, `croniter` for scheduling, `psycopg[binary]`, `click` for the CLI and `sqlalchemy[asyncio]` are the runtime requirements, and `requires-python` is `>=3.10` with classifiers running through 3.14. That is a small, ordinary dependency set for a library doing durability work, and it is also a direct picture of the architecture: a Postgres driver, a query builder and a cron parser.

One wrinkle worth noting: `aiosqlite` is available as an optional extra alongside a `validation` extra pulling in pydantic, and an `otel` extra for OpenTelemetry. The README frames the product around Postgres, so treat the SQLite path as an extra rather than an equal target.

Two decorators, and the distinction that makes it work

Adding durability to an existing program means decorating ordinary functions as workflows and steps:

python
from dbos import DBOS

@DBOS.step()
def step_one():
    ...

@DBOS.step()
def step_two():
    ...

@DBOS.workflow()
def workflow()
    step_one()
    step_two()

The split is the whole contract. `@DBOS.workflow()` marks the function whose progress is checkpointed, and `@DBOS.step()` marks the units of work whose completion is recorded. On recovery the workflow re-enters and skips the steps already recorded as done.

That has a direct consequence for how you write code. A side effect that lives in a plain helper function called from a workflow is not checkpointed, so on replay it happens again. The step boundary is where the correctness argument lives, and the README's framing for AI agents, described as applications relying on unreliable or non-deterministic APIs, only holds if the calls into those APIs are steps.

The stated use cases are orchestrating business processes that recover from failure, building observable fault-tolerant data pipelines, and running agents.

Queues give you concurrency limits, timeouts and dedup

Durable queues run tasks in the background, and a task can be a single step or an entire workflow. DBOS guarantees tasks complete and that callers get results without resubmitting, even if the application is interrupted. A queue has to be registered after `DBOS.launch()`:

python
DBOS.register_queue("example_queue")

@DBOS.step()
def process_task(task):
  ...

@DBOS.workflow()
def process_tasks(tasks):
  task_handles = []
  for task in tasks:
    handle = DBOS.enqueue_workflow("example_queue", process_task, task)
    task_handles.append(handle)
  return [handle.get_result() for handle in task_handles]

Each `DBOS.enqueue_workflow` returns a handle, and `handle.get_result()` blocks for the outcome, which is how a fan-out and join is expressed in a handful of lines.

Flow control is where this stops being a plain queue. The README lists concurrency limits per queue or per process, task timeouts, rate limiting on how often queued tasks execute, deduplication and prioritization. Per-process concurrency limits in particular matter for fan-out jobs, since they stop N application instances from stampeding an external API together.

The design point is that all of that is rows in Postgres, which removes a component from your deployment and puts the durability burden into a database you already back up and monitor.

Workflows as rows, which turns an outage into a query

Because workflows are stored as rows in a Postgres table, the library exposes a client for operating on them programmatically rather than only through code paths inside your service.

The example that sells the feature is a mass recovery after an external outage:

python
client = DBOSClient(system_database_url=system_database_url)
workflows = client.list_workflows(status="ERROR", 
  start_time="2025-04-22T03:00:00Z", end_time="2025-04-22T05:00:00Z")
for workflow in workflows:
    steps = client.list_workflow_steps(workflow)
    if len(steps) >= 3 and isinstance(steps[2]["error"], ServiceOutage):
        DBOS.fork_workflow(workflow.workflow_id, 2)

Querying by status and time window, inspecting which step failed, and forking from that step is an incident response procedure expressed as three function calls. `fork_workflow` is the notable primitive: it re-runs a workflow from a chosen step rather than from the beginning, which is what makes a partial recovery cheap when step two of forty was the broken one.

That cuts both ways. Because the same table is your operational interface, an incident responder needs database access and some knowledge of the schema. This is not a case where you hand a page to someone who does not know Postgres.

Event dedup through a workflow ID, and what the release history says

Webhook handling is the case where duplicate delivery and lost work collide, and DBOS addresses it with `SetWorkflowID` used as an idempotency key:

python
def handle_message(request: Request) -> None:
  event_id = request.body["event_id"]
  with SetWorkflowID(event_id):
    DBOS.start_workflow(message_workflow, request.body["event"])

`start_workflow` returns immediately so the event can be acknowledged, and the work continues durably in the background. The same idea extends to Kafka through `@DBOS.kafka_consumer(config,["alerts-topic"])` stacked above `@DBOS.workflow()`.

Now the caveats. Every guarantee above is a guarantee about Postgres holding a consistent view. If your application and its database disagree about whether a step completed, correctness follows the database, so you are accepting Postgres durability and replication behaviour as your floor. The repository contains a `chaos-tests/` directory, which tells you failure injection is a deliberate part of how the project is tested, and that is a good sign about the design and a reminder that your own Postgres is now in the critical path of every workflow.

The project is young and moving quickly. Version 3.0.0 landed on 2026-09-16, 3.1.0 on 2026-09-24 and 3.2.0 on 2026-09-29, and the last push was on 2026-10-16. Three feature releases in two weeks means minor versions can change behaviour you depend on, so pin an exact version rather than tracking the range. The MIT licence permits use in applications subject to its notice requirements, and `pyproject.toml` also registers a console entry point `dbos = "dbos.cli.cli:app"` for administrative work from a shell.

Editorial conclusion

Adopt DBOS Transact when your Python service already runs on Postgres and you are hand-rolling retry and checkpoint logic, since the annotation plus database approach removes an orchestrator, a broker and a state store from the architecture. Do not adopt it if you cannot commit to Postgres as a correctness dependency, because workflows, queues, scheduling and the management client all read and write the same database, and a `chaos-tests/` directory in the repository is a fair hint that failure injection is part of the model. Verify first that your workflows call only `@DBOS.step()` functions for side effects, since plain function calls inside a workflow are replayed rather than resumed.

Frequently asked questions

What does DBOS Transact need to run?

A Postgres database, since workflows, queues and state are all stored there. Python 3.10 or newer is required, and the runtime dependencies include `psycopg[binary]`, `sqlalchemy[asyncio]`, `croniter` and `click`.

What is the difference between a DBOS workflow and a DBOS step?

The `@DBOS.workflow()` decorated function is checkpointed and resumes after a failure, while `@DBOS.step()` marks individual units of work whose completion is recorded so replay skips them. Side effects should live inside steps.

Do I need a message broker to use DBOS queues?

No. The README states that queues do not require a separate queueing service or message broker, only Postgres. Concurrency limits, timeouts, rate limiting, deduplication and prioritization are all handled through that.

How do I handle duplicate webhook deliveries with DBOS?

Use the event ID as an idempotency key by starting the workflow inside `SetWorkflowID(event_id)`, then call `DBOS.start_workflow` and acknowledge the request immediately while the work continues in the background.

Can I restart failed DBOS workflows without rerunning the whole workflow?

Yes. `DBOS.fork_workflow(workflow_id, step)` reruns from a chosen step, and the client lets you find the candidates with `list_workflows` and inspect failures with `list_workflow_steps`.

Official sources

  1. dbos-inc/dbos-transact-py on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dbos-inc-dbos-transact-py.svg)](https://hysenlabs.com/projects/dbos-inc-dbos-transact-py)

Was this useful?