Relay is a multi-tenant workflow automation platform inspired by Make.com and n8n.
- Copy env defaults
cp .env.example .env
- Start the stack
make up
- Run database migrations
make migrate
- Open the services
- API health: http://localhost:8000/health
- Frontend: http://localhost:5173
- MinIO console: http://localhost:9001
- Run tests
make test
- Register a user (dev only)
curl -X POST http://localhost:8000/api/v1/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"owner@example.com","password":"password123","name":"Owner"}'
- Login
curl -X POST http://localhost:8000/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"owner@example.com","password":"password123"}'
- Create a workspace
curl -X POST http://localhost:8000/api/v1/workspaces \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{"name":"Relay Ops"}'
- Create an API key (owner-only)
curl -X POST http://localhost:8000/api/v1/workspaces/<workspace_id>/api-keys \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{"name":"CI Key"}'
- Publish a workflow from the editor UI or via API:
curl -X POST http://localhost:8000/api/v1/workspaces/<workspace_id>/workflows/<workflow_id>/publish \
-H 'Authorization: Bearer <access_token>'
- Trigger a manual run:
curl -X POST http://localhost:8000/api/v1/workspaces/<workspace_id>/workflows/<workflow_id>/run \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{"trigger_payload":{"hello":"world"}}'
- Inspect runs + steps:
GET /api/v1/workspaces/<workspace_id>/runsGET /api/v1/workspaces/<workspace_id>/runs/<run_id>/steps
Notes:
- At-least-once execution; steps may be retried.
- Cancellation stops future steps but does not terminate in-flight tasks.
Supported node types:
core.http_requestcore.transformlogic.if_elsecore.delaycore.failcore.stop
The HTTP node blocks private, loopback, link-local, and metadata IPs by default. You can also configure host allow/deny lists via env vars:
HTTP_ALLOWLIST_HOSTS(comma-separated)HTTP_DENYLIST_HOSTS(comma-separated)HTTP_DENYLIST_CIDRS(comma-separated)
Sensitive headers (Authorization, Cookie, Set-Cookie, X-API-Key) are redacted in outputs. You can specify extra redacted fields via the node config.
HTTP body modes:
- JSON
- Form URL-Encoded
- Multipart (simple key/value)
Expressions allow mapping data between steps using {{ ... }} syntax. Examples:
{{ trigger.body.customer_id }}{{ steps.http1.output.body.id }}Hello {{ steps.http1.output.body.name }}
Roots:
trigger(trigger payload)steps.<nodeKey>.output(prior step outputs)
Functions:
coalesce(a,b,...)default(x, fallback)upper(s),lower(s)len(x)now()dateAdd(ts, amount, unit)where unit isdays|hours|minutestoJson(x),fromJson(s)concat(a,b,...)substr(s, start, len)
Notes:
- Expressions are deterministic and do not execute code.
- Missing paths resolve to
nullin full expression mode or empty string in templates. - The editor uses Monaco for advanced expression editing (autocomplete + inline errors).
- Configure draft triggers:
curl -X PUT http://localhost:8000/api/v1/workspaces/<workspace_id>/workflows/<workflow_id>/draft/triggers \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"triggers": [
{
"type": "webhook",
"name": "Inbound",
"config": {
"signature_required": true,
"replay_protection_enabled": true
}
}
]
}'
- Publish and fetch active triggers:
curl http://localhost:8000/api/v1/workspaces/<workspace_id>/workflows/<workflow_id>/triggers \
-H 'Authorization: Bearer <access_token>'
- Rotate secret (shown once):
curl -X POST http://localhost:8000/api/v1/workspaces/<workspace_id>/workflows/<workflow_id>/triggers/<trigger_id>/webhook/rotate-secret \
-H 'Authorization: Bearer <access_token>'
- Send a signed webhook: Headers:
X-Timestamp: <unix_seconds>
X-Nonce: <unique nonce>
X-Signature: HMAC_SHA256(secret, "<timestamp>.<nonce>.<raw_body>")
Example (bash + openssl):
body='{"hello":"world"}'
timestamp=$(date +%s)
nonce=$(uuidgen)
signature=$(printf "%s.%s.%s" "$timestamp" "$nonce" "$body" | openssl dgst -sha256 -hmac "<secret>" | sed 's/^.* //')
curl -X POST http://localhost:8000/api/v1/hooks/<public_id> \
-H "Content-Type: application/json" \
-H "X-Timestamp: $timestamp" \
-H "X-Nonce: $nonce" \
-H "X-Signature: $signature" \
-d "$body"
Configure a cron schedule on the draft triggers:
curl -X PUT http://localhost:8000/api/v1/workspaces/<workspace_id>/workflows/<workflow_id>/draft/triggers \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"triggers": [
{
"type": "schedule",
"name": "Every minute",
"config": {
"cron": "* * * * *",
"timezone": "UTC"
}
}
]
}'
The scheduler scans every SCHEDULE_SCAN_INTERVAL_SECONDS and enqueues due runs.
- owner: full control (manage members + API keys + workflows)
- admin: manage members + workflows (cannot assign owner, cannot revoke API keys)
- member: create and edit workflows
- viewer: read-only workspace access
Workflows are versioned automation graphs with a professional lifecycle:
- Draft: Editable graph stored on the workflow. Autosaves on changes.
- Published Version: Immutable snapshot created when you publish. Executions reference these.
- Rollback: Set a previous version as the current published version (history preserved).
- Create a workflow
curl -X POST http://localhost:8000/api/v1/workspaces/<workspace_id>/workflows \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{"name":"My Workflow"}'- Save draft graph
curl -X PUT http://localhost:8000/api/v1/workspaces/<workspace_id>/workflows/<workflow_id>/draft \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"graph": {
"nodes": [
{"id": "n1", "key": "webhook", "type": "trigger.webhook", "position": {"x": 100, "y": 100}, "config": {}}
],
"edges": [],
"metadata": {}
}
}'- Validate the draft
curl -X POST http://localhost:8000/api/v1/workspaces/<workspace_id>/workflows/<workflow_id>/validate \
-H 'Authorization: Bearer <access_token>'- Publish (creates immutable version)
curl -X POST http://localhost:8000/api/v1/workspaces/<workspace_id>/workflows/<workflow_id>/publish \
-H 'Authorization: Bearer <access_token>'- View version history
curl http://localhost:8000/api/v1/workspaces/<workspace_id>/workflows/<workflow_id>/versions \
-H 'Authorization: Bearer <access_token>'- Rollback to a previous version
curl -X POST http://localhost:8000/api/v1/workspaces/<workspace_id>/workflows/<workflow_id>/rollback \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{"version_id": "<version_id>"}'- Exactly 1 trigger node (type starts with
trigger.) - No cycles (DAG only)
- All edges connect existing nodes
- Node keys must be unique
- Required config fields per node type
- Warns if nodes are unreachable from trigger
{
"nodes": [
{
"id": "unique-id",
"key": "human-readable-key",
"type": "trigger.webhook|action.http|...",
"position": {"x": 100, "y": 100},
"config": {},
"retry": {"max_attempts": 3, "initial_delay_ms": 1000},
"timeout_ms": 30000
}
],
"edges": [
{"id": "edge-id", "source": "node-id", "target": "node-id"}
],
"metadata": {"viewport": {"x": 0, "y": 0, "zoom": 1}}
}| Method | Endpoint | Description |
|---|---|---|
| POST | /workspaces/{id}/workflows |
Create workflow |
| GET | /workspaces/{id}/workflows |
List workflows |
| GET | /workspaces/{id}/workflows/{wf_id} |
Get workflow |
| PATCH | /workspaces/{id}/workflows/{wf_id} |
Update metadata |
| DELETE | /workspaces/{id}/workflows/{wf_id} |
Soft delete |
| GET | /workspaces/{id}/workflows/{wf_id}/draft |
Get draft |
| PUT | /workspaces/{id}/workflows/{wf_id}/draft |
Save draft |
| POST | /workspaces/{id}/workflows/{wf_id}/validate |
Validate draft |
| POST | /workspaces/{id}/workflows/{wf_id}/publish |
Publish version |
| GET | /workspaces/{id}/workflows/{wf_id}/versions |
List versions |
| GET | /workspaces/{id}/workflows/{wf_id}/versions/{v_id} |
Get version |
| POST | /workspaces/{id}/workflows/{wf_id}/rollback |
Rollback |
backend/apiFastAPI servicebackend/workerCelery workerbackend/schedulerCelery Beatbackend/commonshared libs (config, logging, DB, schemas)frontendReact app (React Flow + TanStack Router/Query + shadcn/ui + Zustand)infraDocker + Compose
make upStart all servicesmake downStop all servicesmake logsTail service logsmake migrateApply Alembic migrationsmake testRun backend + frontend tests
See .env.example for defaults used by Docker Compose.
- Backend:
pytest - Migrations:
alembic -c backend/common/alembic.ini upgrade head - Frontend:
npm run typecheck && npm run build