FlowForge¶
A distributed workflow orchestration engine for DAG-shaped jobs, built on Django, PostgreSQL, RabbitMQ and Redis.
FlowForge aims to make the hard parts of distributed execution explicit — leases, fencing tokens, a transactional outbox and at-least-once delivery — instead of hiding them behind a task decorator.
Status: v1.0.0 released — all 11 phases implemented
Every layer is built and exercised end to end in CI: the REST API and the Python SDK/CLI, the scheduler and its transactional outbox, the publisher, the worker with leases and fencing tokens, retries and dead-lettering, multi-tenancy with RBAC and an append-only audit log, observability, and the operational surface (Kubernetes, backups with a restore drill, HA guidance).
What it guarantees. At-least-once delivery: a broker message is only a hint, and the row is the decision, so a lost message is recoverable. Fencing tokens: a worker that lost its lease cannot write over the current owner. A transactional outbox: a dispatch exists if and only if the state change that required it committed.
What it does not guarantee. Exactly-once execution. A handler can run more than once across a crash, which is why handlers should be idempotent and the API offers opt-in idempotency keys. Nothing here claims otherwise.
Every failure mode is mapped to a named test in the failure model, with the untested ones marked UNVERIFIED; the known limitations list what is deliberately not proven.
Where to start¶
-
Understand the design
The data model, the state machines, and the definition engine — each written down before it was built.
-
Run it locally
Docker Compose, or a local
uvenvironment. Both are verified in CI. -
See what is actually done
Per-phase status, verification links to the CI runs, and an explicit list of known limitations.
-
Read the decisions
Every non-obvious choice, with the alternative that was rejected and why.
The shape of the thing¶
flowchart LR
client["Client / SDK"] --> api["Django REST API"]
api --> db[("PostgreSQL<br/>authoritative state")]
sched["Scheduler"] --> db
sched --> broker["RabbitMQ"]
broker --> worker["Workers"]
worker --> db
worker --> broker
PostgreSQL is the single source of truth. A broker message is a hint that work may be available; the row is the decision. That ordering is what makes at-least-once delivery safe and exactly-once delivery unnecessary to promise.
Principles¶
- The database decides. State changes are conditional
UPDATEs that report whether they won, not read-then-write sequences that assume they did. - A lost race is not an error. It is a
Falsereturn, and the caller decides what to do about it. - Validate on the way in and on the way out. A definition that becomes unrunnable is caught when it is read, not when a scheduler tries to run it.
- No claim without evidence. Every verification section links to a CI run.