Workflow automation that compiles the graph you draw into the plan that runs, checkpoints every step, and resumes a run after the worker dies. Self-hosted, open source, Python.
Captured from a development instance signed in as the demo account ops@flowos.example, which docs/assets/capture.mjs seeds; nothing in the frame is a real person or a real deployment.
Python 3.13, uv and Node 22 are the only prerequisites. Development mode runs on in-memory adapters, so there is no database to install; data is lost on restart.
git clone --recurse-submodules https://github.com/Yasser-Ameur/flow-os.git
cd flow-os
uv sync --all-packages
uv run --package flowforge uvicorn flowforge.presentation.main:app --port 8000The API answers on http://localhost:8000, with the OpenAPI reference at /docs, liveness at /health/live and readiness at /health/ready. In a second terminal:
cd frontend
npm install
npm run devOpen http://localhost:5173, create the first administrator, and open a workflow. Run sends a payload, and the execution page streams every event as it happens.
The scenes: the workflow list, the Order gate in the editor, the run dialog, the finished execution with its graph and events, and the executions list where a release gate waits on an operator.
Saving a workflow compiles it once into a CompiledExecutionPlan: a validated graph, every node type resolved against the registry, and batches in topological order (compiler.py). The executor walks the batches (executor.py) and never reads the workflow JSON again.
Every step writes a checkpoint carrying a dense sequence number. A job whose worker died is redelivered and resumes from the last checkpoint; a stale worker's late write is refused by that ordering mark. Jobs go through a JobQueue port (ports.py) with an in-memory adapter for development and an ARQ adapter on Redis for production, and each claim carries its own lease so an acknowledgment from the wrong owner is ignored.
| Promise | Test |
|---|---|
| A run resumes from its last checkpoint after the worker crashes | test_scheduler.py test_resume_continues_after_worker_crash |
| A crash before the acknowledgment redelivers the job; a crash after it does not | test_worker_restart.py test_a_crash_before_the_acknowledgment_redelivers_the_job, test_a_crash_after_the_acknowledgment_does_not_redeliver |
| A resumed run never reuses a sequence number | test_scheduler.py test_repeated_resume_never_reuses_schedule_sequence |
| Two workers cannot disagree about a workflow version on one execution | jepsen/test_checkpoint_monotonicity.py test_concurrent_checkpoints_cannot_disagree_about_the_workflow_version |
| No acknowledged enqueue is lost when Redis fsyncs and is then killed | jepsen/test_queue_durability.py test_no_acknowledged_enqueue_is_lost_when_redis_fsyncs |
| A failed run starts its error workflow once, with the documented payload | test_error_workflows.py test_failure_submits_the_error_workflow_with_documented_payload, test_a_failed_error_run_does_not_trigger_again |
| Concurrent OAuth2 refreshes agree on one winner | test_oauth2.py test_authorization_code_concurrent_refresh_agree_on_one_winner |
| Outbound requests refuse private, loopback and link-local targets | test_ssrf_guard.py |
docs/failure-semantics.md says what is and is not promised, including the gaps: rate limiting is per replica, and PostgreSQL concurrency is measured for the checkpoint table only.
- Eleven core nodes:
core.constant,core.delay,core.http.request,core.template,core.condition,core.fail,core.noop,core.set,core.email.smtp,core.workflow.callandcore.wait(pyproject.toml). - Eleven connector operations across seven services, each a JSON file rather than code: Slack, Discord, Telegram, GitHub, GitLab, SendGrid and Stripe (connectors/, docs/connectors.md).
core.waitpauses a run in one of four modes,webhook,form,intervalorat(wait.py); a paused execution holds a single-use resume token (execution.py).- Cron schedules with a timezone per schedule (schedule.py), error workflows, execution retention by age and by count (test_retention.py), and sub-workflows through
core.workflow.call. - OAuth2 client credentials, and authorization code with PKCE (test_oauth2_endpoints.py
test_authorize_returns_pkce_authorization_url). - A worker that scales apart from the API:
uv run --package flowforge flowforge worker(worker.py), withFLOWOS_START_WORKER=falseon the API process (config.py). - Readiness probe, Prometheus
/metrics, OpenTelemetry traces, an audit log, login rate limiting, and RBAC withviewer,operatorandadminroles (identity.py).
Four packages with dependencies pointing inward, under backend/src/flowforge:
| Package | Holds | Depends on |
|---|---|---|
domain/ |
workflow, graph, nodes, policies, execution, schedule, credentials, identity | the standard library, and croniter for cron parsing |
application/ |
compiler, executor, scheduler, event bus, OAuth2, ports | domain/ |
infrastructure/ |
SQLAlchemy repositories, the ARQ queue, plugin hosts, connectors, the HTTP client with the SSRF guard | application/ ports |
presentation/ |
the FastAPI composition root and routers | everything above |
The domain package imports nothing from FastAPI, SQLAlchemy or Pydantic. The SDK under sdk/ has no dependency on the engine, and contracts/ holds the language-neutral wire format plugins speak over. The editor under frontend/ is React 19 with TypeScript, Vite, an XYFlow canvas and a Monaco configuration editor (package.json).
The API mounts routers for auth, workflows, runs, executions, schedules, credentials, OAuth2, nodes, plugins, webhooks, audit, health, metrics and the websocket stream at /ws (main.py). The full reference is served at /docs while the API runs, and docs/api.md walks it.
Every node is a plugin: a Python package publishing flowforge.nodes entry points against an SDK with no dependency on the engine. A plugin can run in its own process, so one bad node cannot take the API down. The reference plugin under examples/reference_plugin shows the whole pattern in one file.
uv run --package flowforge python -m flowforge.cli.plugin init ./my-plugin --namespace acme
cd my-plugin && pytestThe same command is installed as the flowforge console script; the module form above is the one that also runs where a policy blocks console-script executables.
docs/plugins.md describes discovery, isolation, supervision, permissions and packaging; docs/sdk.md the node API. Sample workflows and the MiniGoogle, NotiFly and Pulse integration nodes live under examples/.
Everything the editor does goes through the same API. Sign in for a bearer token, create a workflow from one of the examples, run it with a payload, and read the execution back:
curl -s -X POST http://localhost:8000/auth/login -H 'content-type: application/json' \
-d '{"email":"ops@flowos.example","password":"frontier tick checkpoint 4"}'
# {"token":"...","token_type":"bearer","user":{...}}
curl -s -X POST http://localhost:8000/workflows -H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' -d @examples/workflows/hello.json
# {"id":"04c06060-...","name":"Hello template","version":1,...}
curl -s -X POST http://localhost:8000/workflows/$WORKFLOW_ID/runs -H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' -d '{"payload":{"user":"Grace"}}'
# {"job_id":"...","workflow_id":"...","execution_id":"e264323c-...","status":"queued"}
curl -s http://localhost:8000/executions/$EXECUTION_ID -H "authorization: Bearer $TOKEN"
# {"execution_id":"e264323c-...","status":"succeeded","started_at":"...","finished_at":"...",...}A run is accepted with 202 and queued; the execution moves through running to succeeded, failed, paused or cancelled, and GET /executions/{id} shows the trigger, the payload and, for a paused run, the resume URL. Webhooks at /webhooks/{id} trigger a workflow from outside, and PUT /workflows/{id} requires the version you edited, so a stale edit gets 409 instead of overwriting someone's work.
Settings are read from FLOWOS_* environment variables (config.py); .env.example is a starting point and docs/deployment.md the full guide.
| Variable | Default | Purpose |
|---|---|---|
FLOWOS_ENV |
development |
Anything else selects the SQL and Redis adapters |
FLOWOS_DATABASE_URL |
unset | Async SQLAlchemy URL, postgresql+asyncpg://... in production |
FLOWOS_REDIS_URL |
unset | Redis backing the ARQ job queue |
FLOWOS_TOKEN_SECRET |
a development default | Signs bearer tokens; set a strong value in production |
FLOWOS_SECRET_ENCRYPTION_KEY |
unset | Fernet key for credentials at rest; required outside development |
FLOWOS_START_WORKER |
true |
Whether the API process also runs the job worker |
FLOWOS_EXECUTION_RETENTION_DAYS |
30 |
Terminal executions older than this are pruned; 0 disables |
FLOWOS_EXECUTION_RETENTION_MAX |
10000 |
Terminal executions beyond this count are pruned, oldest first |
FLOWOS_PLUGIN_DIRECTORIES |
unset | Restricts plugin discovery to these directories |
FLOWOS_OTEL_ENDPOINT |
unset | OTLP/HTTP collector; unset means tracing is a no-op |
FLOWOS_LOG_FORMAT |
console |
console or json |
FLOWOS_PUBLIC_URL |
http://localhost:5173 |
The editor's address, used in OAuth2 redirects |
docker-compose.yml builds the API, the worker, PostgreSQL 17 and Redis 7 from this checkout. It was not run on the machine this README was written on.
/health/live says the process is up; /health/ready runs every probe against the configured stores. Prometheus scrapes /metrics, traces go to the OTLP endpoint when one is set, and every sensitive action lands in the audit log at /audit. A worker that dies mid-run loses nothing: the job is redelivered and resumes from its last checkpoint, which is the first row of the guarantees table.
The crash suites under backend/tests/jepsen drop tables and kill servers, so they run only when FLOWOS_TEST_POSTGRES_DSN or FLOWOS_TEST_REDIS_URL and FLOWOS_TEST_DESTRUCTIVE=1 point them at a disposable database. In CI the integration job in ci.yml runs them against PostgreSQL and Redis containers the suite kills itself.
uv run --package flowforge ruff check .
uv run --package flowforge ruff format --check .
uv run --package flowforge mypy
uv run --package flowforge pytest -qThe backend suite is 2188 tests passing and 25 skipped (each skip needs a live MiniGoogle, NotiFly or Pulse, or a real Redis or PostgreSQL). CI fails the build under 95 percent coverage (coverage report --fail-under=95 in ci.yml). The editor runs npm run lint, npm test (155 vitest cases) and npm run build, and npm run e2e drives five Playwright specs against a running API. On Windows, start the API without --reload when process plugins are enabled (docs/developer.md).
docs/ holds the architecture, engine, database, API, observability and deployment guides. The screenshots above are produced by docs/assets/capture.mjs and docs/assets/gif.py; the visual identity is in DESIGN.md, and CHANGELOG.md records each release.
Read CONTRIBUTING.md before opening an issue or a pull request, and the Code of Conduct. Vulnerabilities go through SECURITY.md, not a public issue. Released under the MIT License.

