Skip to content

Repository files navigation

FlowOS

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.

CI Python 3.13+ License: MIT

The FlowOS editor with the Order gate example open: a node palette on the left, six connected nodes on the canvas with true and false routes out of the gate, and the inspector on the right.

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.

Try it

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 8000

The 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 dev

Open 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.

Five scenes of one task: the workflow list, the Order gate open in the editor, the run dialog, the execution detail showing the run succeeded with its events, and the executions list with a paused release gate.

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.

What runs

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.

Guarantees, and the test that keeps each

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.

What ships

  • 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.call and core.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.wait pauses a run in one of four modes, webhook, form, interval or at (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), with FLOWOS_START_WORKER=false on the API process (config.py).
  • Readiness probe, Prometheus /metrics, OpenTelemetry traces, an audit log, login rate limiting, and RBAC with viewer, operator and admin roles (identity.py).

How it is built

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.

Extend 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 && pytest

The 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/.

Use the API

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.

Configure it

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.

Operate it

/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.

Develop

uv run --package flowforge ruff check .
uv run --package flowforge ruff format --check .
uv run --package flowforge mypy
uv run --package flowforge pytest -q

The 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.

Contributing and security

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.

About

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

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages