Reference

API reference

A JSON REST API under /api plus one WebSocket. Ids are 12-character hex strings, timestamps are ISO 8601 UTC with a Z suffix, and every mutation is capability-checked and audit-logged.

Base URL in development: http://localhost:8000. Interactive OpenAPI docs are served by the backend at http://localhost:8000/docs (schema at /openapi.json). Every path below is relative to the base URL, and — unless it is auth-exempt — passes through the authentication and role-based access-control middleware described next.

Authentication & errors

Every request under /api resolves an actor and that actor's effective permissions. The middleware checks credentials in strict precedence order:

  • X-API-Key header — highest precedence. A key linked to a member acts with that member's effective permissions; an unlinked key acts with its own plain role. A missing or revoked key is rejected with 401.
  • Authorization: Bearer <session token> — a token minted by /api/auth/login. The request acts as that member; an expired token or a non-active member returns 401.
  • No credential — in AUTH_MODE=open (the default for local development) the request is treated as the workspace owner, with full admin permissions. In AUTH_MODE=required it is rejected with 401.
Credential precedence
# 1. API key — acts as the linked member, or the key's own role
$ curl http://localhost:8000/api/workflows -H "X-API-Key: aos_1c3f9a…"

# 2. Session bearer token from POST /api/auth/login
$ curl http://localhost:8000/api/workflows -H "authorization: Bearer eyJhbGciOi…"

# 3. No credential — only works in AUTH_MODE=open (acts as the owner);
#    AUTH_MODE=required returns 401 { "detail": "Authentication required" }
$ curl http://localhost:8000/api/workflows

Four paths bypass auth entirely (they are how you obtain a credential or probe liveness): /api/health, /api/auth/login, /api/auth/signup, and /api/auth/accept-invite. Every path under /api/hooks/ is also exempt — there, the per-workflow secret embedded in the URL is the credential.

Capabilities

Once the actor is known, the middleware derives the single capability the route requires from its method and path, and returns 403 if the credential lacks it. Roles carry default capabilities; per-member overrides adjust them in either direction. Every endpoint below names its required capability.

FieldTypeDescription
viewall rolesRead anything — every GET, plus /validate and the MCP entrypoint.
run_workflowseditor +Trigger runs via POST /api/workflows/{id}/runs.
approve_gateseditor +Resolve paused human gates via POST /api/runs/{id}/approve.
cancel_resumeeditor +POST /api/runs/{id}/cancel and /api/runs/{id}/resume.
create_edit_workflowseditor +Create & edit workflows, checks, tools, schedules, webhooks, templates.
publish_workflowseditor +Reserved; the update that sets status:published is gated on create_edit_workflows.
delete_workflowsadminDELETE /api/workflows/{id}.
manage_membersadminInvite, update, approve, remove members and edit their permissions.
manage_api_keysadminCreate and revoke API keys.
manage_workspaceadminRename the workspace and change its plan.

Error envelope & status codes

Every error — validation, auth, quota, or not-found — uses FastAPI's envelope: a single detail string.

Error envelope
{ "detail": "Workflow not found" }
{ "detail": "Workflow graph invalid: Node 'x' uses unknown tool 'custom:foo'" }
{ "detail": "This action requires the 'delete_workflows' permission" }
{ "detail": "Monthly run limit reached (1,000/1,000 on the free plan) — upgrade to keep running" }
FieldTypeDescription
400Bad RequestInvalid graph or checks, bad cron, malformed body, duplicate tool name, or a last-admin guard.
401UnauthorizedMissing / invalid API key, or an expired session token (or no credential in required mode).
402Payment RequiredMonthly run quota reached. Returned by run triggers (REST, webhook, MCP).
403ForbiddenAuthenticated, but the credential lacks the required capability.
404Not FoundUnknown id — or a wrong webhook id/secret, deliberately indistinguishable.
429Too Many RequestsPer-IP rate limit. Off by default; enabled with RATE_LIMIT_PER_MINUTE.

Conventions

FieldTypeDescription
idsstring12-character hex, unique per resource.
timestampsstringISO 8601 UTC with a Z suffix, e.g. 2026-07-05T14:10:05Z.
paginationlimit / offsetOnly /api/runs and /api/audit paginate — {runs|logs, total}. limit caps at 200. Other lists return the full array.
Idempotency-KeyheaderOn POST /api/workflows/{id}/runs: a repeated key returns the original run instead of starting a duplicate.
Content-TypeheaderAll request and response bodies are JSON.

Run lifecycle

pendingrunning
success
failedresumable from checkpoint
pausedapprove → running · reject/cancel → failed

A run moves pending running → a terminal success or failed, or parks at paused when it reaches a human gate. Individual steps carry their own status: running, success, failed, or waiting (a gate awaiting review). Failed runs keep their last checkpoint and are resumable; paused runs continue when a gate is approved.

Workflows

GET/api/workflows

List workflows, newest-updated first, each with its embedded checks and aggregate run_stats. Capability: view. Not paginated.

Response (excerpt)
[
  {
    "id": "9bc62f209c52",
    "name": "Support triage",
    "description": "Classify and reply to inbound tickets.",
    "status": "published",
    "version": 2,
    "graph": { "nodes": [ … ], "edges": [ … ] },
    "checks": [
      { "id": "c1a2", "name": "No PII in reply", "type": "not_contains",
        "target": "reply", "value": "SSN", "case_sensitive": false }
    ],
    "run_stats": { "total_runs": 6, "success_rate": 83.3, "avg_cost_usd": 0.004083 },
    "created_at": "2026-07-05T13:49:20Z",
    "updated_at": "2026-07-05T14:02:11Z"
  }
]
POST/api/workflows

Create a workflow. The graph is optional — an empty workflow is a valid draft. Capability: create_edit_workflows. Returns 201 with the full workflow.

FieldTypeDescription
namestringHuman-readable workflow name.
descriptionstringOptional prose description.
graphGraphNodes and edges. Node types: agent, tool, condition, human_gate, loop, merge.
Request
{ "name": "Support triage", "description": "Classify and reply.", "graph": { "nodes": [], "edges": [] } }
GET/api/workflows/{id}

Fetch one workflow with its full graph, checks, and run_stats. Capability: view. 404 if unknown.

PUT/api/workflows/{id}

Update name, description, status, and/or graph. Capability: create_edit_workflows. Any graph change snapshots the previous version and bumps the version number.

FieldTypeDescription
namestring | nullNew name.
descriptionstring | nullNew description.
status"draft" | "published" | nullPublish or unpublish. published workflows count toward active_workflows metrics.
graphGraph | nullReplacing the graph snapshots the prior graph as an immutable version and increments version.
Request
{ "status": "published" }
// or replace the graph (this bumps version):
{ "graph": { "nodes": [ … ], "edges": [ … ] } }
DELETE/api/workflows/{id}

Delete the workflow and its runs, steps, checkpoints, and schedules. Capability: delete_workflows. Returns 204.

POST/api/workflows/{id}/validate

Structural and expression validation without running. Capability: view (read-only despite being a POST). Errors block runs; warnings do not.

Response
{
  "valid": false,
  "errors": ["Edge 'e1' references missing target node 'ghost'"],
  "warnings": ["Condition node 'check' has no 'false' branch — runs end there when false"]
}
PUT/api/workflows/{id}/checks

Replace the workflow's evaluation checks (does not version the graph). Capability: create_edit_workflows. Invalid checks return 400.

Check types: contains, not_contains, equals, regex, max_cost_usd, max_duration_ms, and expression. Each is scored against the final state after a successful run; results appear as evals on the run.

Request
{
  "checks": [
    { "name": "No PII in reply", "type": "not_contains", "target": "reply", "value": "SSN" },
    { "name": "Under budget", "type": "max_cost_usd", "value": 0.05 }
  ]
}
GET/api/workflows/{id}/versions

Immutable graph snapshots, newest first — one entry per graph save. Capability: view.

Response (excerpt)
[ { "id": "1de2a9c04b11", "version": 1, "graph": { … }, "created_at": "2026-07-05T13:49:20Z" } ]

Webhook management

A workflow can expose an inbound webhook. These three endpoints manage it; the inbound trigger itself is documented under Webhook triggers.

POST/api/workflows/{id}/webhook

Enable the inbound webhook, or rotate its secret if already enabled. Capability: create_edit_workflows. Returns { enabled, url }.

Response
{ "enabled": true, "url": "http://localhost:8000/api/hooks/9bc62f209c52/4f7c…e21a" }
GET/api/workflows/{id}/webhook

Current webhook state. Capability: view. url is null while disabled.

DELETE/api/workflows/{id}/webhook

Disable the webhook — the old URL stops working immediately. Capability: create_edit_workflows. Returns 204.

Trigger a run

POST/api/workflows/{id}/runs

Trigger a run. Capability: run_workflows. Checks quota (402), then validates the graph (400 on errors), then returns 201 immediately — execution is asynchronous.

FieldTypeDescription
inputobjectRun input variables. Merged into initial run state and available as {placeholders} and to expressions.

Send an Idempotency-Key header to make the trigger safe to retry: a repeated key returns the original run instead of starting a duplicate. The response is a RunOut in pending status — subscribe to the event stream or poll GET /api/runs/{id} to follow it.

Idempotent trigger
$ curl -s -X POST http://localhost:8000/api/workflows/9bc62f209c52/runs \
    -H "content-type: application/json" \
    -H "Idempotency-Key: ticket-8841-triage" \
    -d '{"input": {"ticket": "I was charged twice for my March invoice"}}'
// → 201
{ "id": "af772b3602ef", "workflow_id": "9bc62f209c52", "status": "pending",
  "input": { "ticket": "I was charged twice…" }, "cost_usd": 0.0, "created_at": "2026-07-05T14:10:05Z", … }

Runs

GET/api/runs

Paginated history, newest first. Capability: view. Filters: workflow_id, status. Params: limit (max 200), offset. Steps are omitted in list responses.

FieldTypeDescription
workflow_idstringRestrict to one workflow.
statusstringpending · running · paused · success · failed.
limitintPage size, capped at 200.
offsetintRows to skip.
Response (excerpt)
{
  "runs": [
    { "id": "af772b3602ef", "workflow_id": "9bc62f209c52", "workflow_name": "Support triage",
      "status": "paused", "cost_usd": 0.0038, "duration_ms": null, "steps": [],
      "pending_gate": { "node_id": "review", "prompt": "Review the drafted reply…" },
      "created_at": "2026-07-05T14:10:05Z" }
  ],
  "total": 41
}
GET/api/runs/{id}

Full run detail: status, input/output, cost, duration, error, pending gate, eval results, and every step with its own input, output, cost, and timing. Capability: view.

Response (excerpt)
{
  "id": "af772b3602ef",
  "workflow_id": "9bc62f209c52",
  "workflow_name": "Support triage",
  "status": "paused",
  "input": { "ticket": "I was charged twice…" },
  "output": null,
  "cost_usd": 0.0038,
  "duration_ms": null,
  "error_message": null,
  "pending_gate": { "node_id": "review", "prompt": "Review the drafted reply…" },
  "evals": null,
  "created_at": "2026-07-05T14:10:05Z",
  "steps": [
    { "node_id": "classify", "node_type": "agent", "label": "Classify Ticket",
      "status": "success", "cost_usd": 0.0021,
      "input": null, "output": { "text": "billing", "model": "claude-…", "input_tokens": 220, "output_tokens": 4 },
      "started_at": "2026-07-05T14:10:05Z", "finished_at": "2026-07-05T14:10:06Z", "error": null },
    { "node_id": "review", "node_type": "human_gate", "label": "Review Reply",
      "status": "waiting", "input": { "prompt": "Review the drafted reply…" }, "cost_usd": 0.0 }
  ]
}

A successful run also carries an evals summary when the workflow has checks: { "passed": 2, "total": 2, "results": [ … ] }.

POST/api/runs/{id}/approve

Resolve a paused human gate. Capability: approve_gates. 400 unless the run is paused on a gate.

FieldTypeDescription
approvedbooleantrue resumes past the gate; false fails the run with 'Rejected at human gate …'.
inputobject | nullMerged into run state on approval — inject a correction or a reviewer decision.

Approval records the decision in state under approval_<node_id>, marks the gate completed, rewrites the checkpoint, and resumes execution. Rejection settles the run as failed.

Request
{ "approved": true, "input": { "reply_tone": "formal" } }
POST/api/runs/{id}/resume

Re-run a failed run from its last checkpoint — completed nodes never re-execute. Capability: cancel_resume. 400 if the run is not failed (paused runs use /approve) or has no checkpoint.

POST/api/runs/{id}/cancel

Cancel a pending, running, or paused run. Capability: cancel_resume. Running work is interrupted mid-node; the run settles as failed with 'Cancelled by user' and keeps its checkpoints. 400 if already finished.

WS/api/runs/{id}/stream

Server-push WebSocket of run events — step lifecycle, pauses, completion, evaluation. Connect while a run is active.

Every event payload, the required ?token= query parameter in required mode, and a full client example live on Events & streaming.

Templates

GET/api/templates

The seeded template gallery — complete graphs ready to instantiate, sorted by name. Capability: view.

Response (excerpt)
[ { "id": "3f9a01d2c7b4", "name": "Support Triage Crew", "category": "Support",
    "description": "Classify, draft, and gate replies.", "graph": { "nodes": [ … ], "edges": [ … ] } } ]
POST/api/templates/{id}/instantiate

Create a workflow from a template, copying its graph. Capability: create_edit_workflows. Body: { name? }. Returns 201 with the new workflow.

Request
{ "name": "Triage — EU region" }   // optional; defaults to the template name

Custom tools

Registered tools are addressable from a workflow's tool nodes as custom:<name>. See the Tool reference for node wiring.

GET/api/tools

Registered tools, sorted by name. Capability: view.

POST/api/tools

Register an external endpoint (kind http) or a remote MCP tool (kind mcp) as custom:<name>. Capability: create_edit_workflows. Duplicate name → 400; kind mcp without mcp_tool → 400.

FieldTypeDescription
namestringLowercase slug ^[a-z0-9][a-z0-9-_]{1,40}$. Referenced as custom:<name>.
labelstringDisplay label.
descriptionstringWhat the tool does.
kind"http" | "mcp"http calls endpoint_url; mcp calls a remote MCP server's tool.
endpoint_urlstringHTTP endpoint (http) or MCP server URL (mcp).
methodstringHTTP verb; uppercased on save.
headersobjectStatic request headers, e.g. auth.
mcp_toolstring | nullRequired for kind mcp — the remote tool name to call.
input_schemaobjectJSON Schema validated on /test and at run time.
timeout_secondsintPer-call timeout, 1–600.
retry_countintRetries on failure, 0–5.
Request
{
  "name": "score-lead",
  "label": "Score lead",
  "description": "Scores an inbound lead 0-100.",
  "kind": "http",
  "endpoint_url": "https://my-agent.example.com/score",
  "method": "POST",
  "headers": { "Authorization": "Bearer …" },
  "input_schema": {
    "type": "object",
    "properties": { "company": { "type": "string" } },
    "required": ["company"]
  },
  "timeout_seconds": 60,
  "retry_count": 1
}
GET/api/tools/{id}

Fetch one tool registration. Capability: view.

PUT/api/tools/{id}

Update any registration field (partial). Capability: create_edit_workflows. Workflows referencing custom:<name> pick the change up on their next run.

DELETE/api/tools/{id}

Remove a tool. Capability: create_edit_workflows. Returns 204. Workflows still referencing it fail validation before their next run.

POST/api/tools/{id}/test

Invoke the tool once with sample arguments — schema validation included. Capability: create_edit_workflows. Always 200; success is reported in the body's ok field.

Request → response
{ "arguments": { "company": "Acme" } }
// → 200 on success
{ "ok": true, "output": { "score": 87 }, "duration_ms": 214 }
// → 200 on failure (ok:false, with the error message)
{ "ok": false, "error": "HTTP 500 from https://my-agent.example.com/score", "duration_ms": 512 }

Schedules

Cron-based triggers, evaluated in UTC by an in-process scheduler (~20s tick). Full syntax and execution semantics live on Scheduled triggers. All five endpoints require create_edit_workflows to mutate and view to read.

POST/api/workflows/{id}/schedules

Create a schedule: { cron, input? }. Capability: create_edit_workflows. Invalid cron → 400 naming the field problem. Returns 201 with the schedule and its computed next_run_at.

Request → response
{ "cron": "0 9 * * 1-5", "input": { "source": "daily-digest" } }
// → 201
{ "id": "b8d1f2a90c34", "workflow_id": "9bc62f209c52", "cron": "0 9 * * 1-5",
  "input": { "source": "daily-digest" }, "enabled": true,
  "last_run_at": null, "next_run_at": "2026-07-07T09:00:00Z", "created_at": "2026-07-06T14:21:08Z" }
GET/api/workflows/{id}/schedules

Schedules attached to one workflow, newest first. Capability: view.

GET/api/schedules

Every schedule in the workspace — the fleet view. Capability: view.

PUT/api/schedules/{id}

Update cron, input, or enabled. Capability: create_edit_workflows. Editing the cron recomputes next_run_at; disabling pauses firing without deleting.

DELETE/api/schedules/{id}

Delete the schedule; runs it already started are unaffected. Capability: create_edit_workflows. Returns 204.

Workspace & team

Workspace & plan

GET/api/workspace

Workspace name, plan, member_count, and created_at. Capability: view.

Response
{ "id": "0a11b2c3d4e5", "name": "Acme Ops", "plan": "pro", "member_count": 7, "created_at": "2026-05-01T09:00:00Z" }
PUT/api/workspace

Rename the workspace: { name }. Capability: manage_workspace.

PUT/api/workspace/plan

Switch plan tiers: { plan: free | starter | pro | enterprise }. Capability: manage_workspace. Quotas and retention apply immediately; run triggers past the monthly quota then return 402.

Members

GET/api/members

All members, oldest first. Capability: view. invite_code is present only while status is 'invited'.

Response (excerpt)
[ { "id": "77aa11bb22cc", "name": "Dana Lee", "email": "dana@acme.com",
    "role": "editor", "status": "active", "invite_code": null, "joined_at": "2026-05-02T10:11:00Z" } ]
POST/api/members/invite

Invite a member: { email, name?, role? }. Capability: manage_members. Returns 201 with an invite_code (emailed when Resend is configured). Duplicate email → 400.

Request
{ "email": "sam@acme.com", "name": "Sam Ortiz", "role": "editor" }
GET/api/members/{id}

One member with the full capability matrix (default, override, effective per capability) and api_key_count. Capability: view.

Response (excerpt)
{
  "id": "77aa11bb22cc", "name": "Dana Lee", "email": "dana@acme.com", "role": "editor", "status": "active",
  "api_key_count": 2,
  "permissions": [
    { "capability": "run_workflows", "label": "Run workflows", "default": true, "override": null, "effective": true },
    { "capability": "delete_workflows", "label": "Delete workflows", "default": false, "override": true, "effective": true }
  ]
}
PUT/api/members/{id}

Change a member's role: { role }. Capability: manage_members. Demoting or removing the last active admin is refused with 400.

PUT/api/members/{id}/permissions

Set per-capability overrides: { overrides: { capability: true | false | null } }. Capability: manage_members. null clears an override; unknown capabilities → 400.

Request
{ "overrides": { "delete_workflows": true, "publish_workflows": null } }
GET/api/members/{id}/stats

Per-member activity: runs_triggered, approvals, rejections, workflows_created, cost_usd, a 30-day activity_by_day series, and recent_actions. Capability: view.

POST/api/members/{id}/approve

Approve a self-signup awaiting review — flips status pending → active. Capability: manage_members. 400 unless the member is pending.

DELETE/api/members/{id}

Remove a member. Capability: manage_members. Returns 204. The last active admin cannot be removed (400).

API keys

GET/api/keys

All keys, newest first, with a masked prefix, role, linked member_id, last_used_at, and revoked flag. Capability: view. The full secret is never returned here.

POST/api/keys

Mint a key: { name, role?, member_id? }. Capability: manage_api_keys. A member_id links the key to that member's effective permissions; otherwise the plain role applies. Returns 201 with the secret — shown exactly once.

Request → response
{ "name": "CI pipeline", "role": "editor" }
// → 201 — copy the secret now; it is not retrievable again
{ "id": "aa00bb11cc22", "name": "CI pipeline", "prefix": "aos_1c3f9a2b…", "role": "editor",
  "member_id": null, "revoked": false, "last_used_at": null, "created_at": "2026-07-06T15:00:00Z",
  "secret": "aos_1c3f9a2b7d8e0f1a2b3c4d5e6f708192a3b4c5d6" }
DELETE/api/keys/{id}

Revoke a key (soft delete — the row stays, revoked becomes true). Capability: manage_api_keys. Returns 204. Subsequent requests with that key get 401.

Usage

GET/api/usage

Current billing-month usage: plan, runs_used, runs_limit (null on unlimited plans), cost_usd, and the period window. Capability: view.

Response
{ "plan": "pro", "runs_used": 3120, "runs_limit": 50000, "cost_usd": 42.19,
  "period_start": "2026-07-01T00:00:00Z", "period_end": "2026-07-31T23:59:59Z" }

Audit & metrics

GET/api/audit

Append-only audit trail, newest first. Capability: view. Params: limit (max 200), offset. Each entry carries the actor, action, target, and a details object.

Actions include create, update, delete, run, approve, reject, resume, cancel, webhook_run, schedule_run, plan_change, and the auth and member events.

Response (excerpt)
{
  "logs": [
    { "id": "77b21c09aa04", "action": "approve", "actor_id": "77aa11bb22cc", "actor_name": "Dana Lee",
      "target_type": "run", "target_id": "af772b3602ef", "details": { "node_id": "review" },
      "created_at": "2026-07-05T14:12:40Z" }
  ],
  "total": 128
}
GET/api/metrics/overview

Dashboard aggregates. Capability: view. total_runs, success_rate (of finished runs), total_cost_usd, active_workflows (published), a 14-day runs_by_day series, and eval_pass_rate / evaluated_runs.

Response
{
  "total_runs": 412, "success_rate": 91.2, "total_cost_usd": 6.84, "active_workflows": 5,
  "runs_by_day": [ { "date": "2026-07-05", "count": 31, "cost_usd": 0.412 } ],
  "eval_pass_rate": 96.7, "evaluated_runs": 240
}
GET/api/health

Liveness probe. Auth-exempt. Returns { "status": "ok", "service": "ballast" }.

Governance & budgets

Runtime policy controls and their inspectable event stream. Every execution-time decision — a denied tool, a redaction, a blocked model or fallback, a budget block, an override — is recorded as a governance event, distinct from the tamper-evident audit log of API mutations.

GET/api/governance/events

The governance / policy event stream — every execution-time policy decision, most recent first. Filter by run_id, workflow_id, event_type, decision, and limit.

FieldTypeDescription
run_idqueryOnly events for this run.
workflow_idqueryOnly events for this workflow.
event_typequerypolicy_denied · tool_argument_blocked · data_redacted · budget_blocked · model_fallback_blocked · approval_override_granted.
decisionquerydenied · redacted · blocked · granted · allowed.
limitqueryMax rows (capped at 1000).
An event (evidence is redacted — counts/ids only, never plaintext)
{
  "id": "ev_7c1a…", "event_type": "data_redacted", "decision": "redacted",
  "policy": "dlp", "workflow_id": "9bc62f209c52", "version": 3,
  "run_id": "af772b3602ef", "node_id": "classify", "actor_id": null,
  "reason": "redacted 2 sensitive value(s) before the prompt left the box",
  "evidence": { "redacted_count": 2 }, "created_at": "2026-07-13T18:04:11Z"
}
PUT/api/workflows/{id}/webhook/config

Harden a workflow's inbound webhook: require_signature (HMAC), timestamp_tolerance_seconds (replay window), dedup_window_seconds (ingest de-dup), event_id_field. See the Webhooks guide.

POST/api/runs/{id}/budget-override

Grant an audited override of the per-run budget cap and resume a run that failed on budget. Requires manage_workspace — that authorization is the approval. Body { approved, cap_usd?, reason? }; omit cap_usd to lift the cap. Writes a budget_override audit entry and an approval_override_granted governance event.

PUT/api/workflows/{id}/eval-gate

Set the server-side evaluation deploy gate { dataset_id?, min_pass_rate?, min_expected_match_rate?, max_avg_cost_usd? }. A subsequent POST /release returns 422 when the latest evaluation for that version regresses past the thresholds.

Budgets (POST/PUT /api/budgets) now accept reservation_estimate_usd — a per-run hold that makes concurrent triggers admit atomically instead of racing past a limit — and their status carries committed_usd and a projected_usd forecast. Governance (PUT /api/governance) accepts redact_fields — named PII types (email/phone/credit_card/ssn/ip_address/api_key) scrubbed alongside your regexes. Block budgets are enforced on every trigger surface: manual, replay, webhook, schedule, and MCP.

MCP server

POST /api/mcp is a streamable-HTTP MCP server (JSON-RPC 2.0, protocol 2025-06-18). It passes the normal auth middleware — required capability view to reach the endpoint — and then each tools/call enforces the capability of its REST equivalent, so a viewer credential can inspect but not run.

FieldTypeDescription
initializemethodHandshake; returns protocolVersion, capabilities.tools, and serverInfo.
tools/listmethodThe six tools with their JSON input schemas.
tools/callmethodlist_workflows · get_workflow · run_workflow · get_run · list_pending_approvals · approve_gate.
pingmethodLiveness check; returns an empty result.
notifications/*notificationAccepted and acknowledged with 202 (e.g. notifications/initialized).

Per-tool capabilities, structured results, and a Claude Code connection example live on the MCP page.

Auth

Signup, login, and accept-invite are auth-exempt — they are how you get a token. me and change-password require an authenticated session (or the owner, in open mode).

POST/api/auth/signup

Self-serve registration. Auth-exempt. Governed by SIGNUP_MODE: open (active immediately, returns a token), approval (pending — returns { pending: true }, no token), or closed (403).

Request → response
{ "name": "Kim", "email": "kim@acme.com", "password": "correct horse battery" }
// → 201 (open mode)
{ "token": "eyJhbGciOi…", "member": { "id": "…", "role": "viewer", "status": "active", … }, "pending": false }
// → 201 (approval mode) — no token until an admin approves
{ "token": null, "member": { "…": "…", "status": "pending" }, "pending": true }
POST/api/auth/login

Exchange email + password for a session token. Auth-exempt. 401 on bad credentials; 403 if the account is still awaiting admin approval.

Request → response
{ "email": "dana@acme.com", "password": "…" }
// → 200
{ "token": "eyJhbGciOi…", "member": { "id": "77aa11bb22cc", "role": "editor", "status": "active", … } }
POST/api/auth/accept-invite

Claim an invite: { email, invite_code, name?, password }. Auth-exempt. Activates the member, sets their password, and returns a session token. 400 on a missing or mismatched code.

GET/api/auth/me

The current member plus the server's auth_mode (open | required). Requires an authenticated session; 401 otherwise.

Response
{ "member": { "id": "77aa11bb22cc", "name": "Dana Lee", "role": "editor", "status": "active", … },
  "auth_mode": "required" }
POST/api/auth/change-password

Set a new password: { current_password?, new_password }. Requires an authenticated session. Returns 204. 400 if current_password is wrong for a member who already has one.

The backend serves interactive OpenAPI documentation at http://localhost:8000/docs — useful for exploring request schemas live. Real-time run event payloads are specified in Events & streaming.
Every mutating endpoint writes an entry to the audit log with the resolved actor — visible via GET /api/auditand attributed on the member's stats page.