Reference

Operations & plans

What the platform enforces on the workspace's behalf when you run it for real: monthly run quotas, data retention windows, operational alerts, a full audit trail, role-based access control, API keys, and production authentication.

Everything on this page is already enforced in the local and self-hosted builds — the plan tier is just a field on the workspace. Wiring up a billing provider changes who may switch plans, not what the enforcement layer does.

Plans & run quotas

Every run-trigger surface — manual, webhook, MCP, and the scheduler — counts against a single monthly quota set by the workspace plan:

FieldTypeDescription
free1,000 / moSolo and trial workspaces.
starter10,000 / moSmall teams running in production.
pro200,000 / moHigh-volume automation.
enterpriseunlimitedNo monthly cap — the quota check short-circuits.

The quota window is the calendar month in UTC, resetting at 00:00 on the 1st. It counts every run created in the current month regardless of final status — a failed or cancelled run still consumes quota, because the work was attempted. The check runs before graph validation, so a workspace at its limit is refused even for a valid workflow.

At the limit, triggers return 402 Payment Required with a plain, itemised explanation — no runs are silently dropped:

Quota exhausted (402)
HTTP/1.1 402 Payment Required
{
  "detail": "Monthly run limit reached (1,000/1,000 on the free plan) — upgrade the workspace plan to keep running"
}

Behaviour per trigger surface

FieldTypeDescription
Manual402POST /api/workflows/{id}/runs raises 402. Checked after an idempotent replay returns the prior run, but before graph validation.
Webhook402POST /api/hooks/{workflow_id}/{secret} raises 402 after the secret is verified.
MCPtool errorThe run_workflow tool returns the same message as a tool error (isError), not an HTTP 402.
Scheduledskip + logThe scheduler tick is skipped and logged, never queued. Cadence resumes when quota frees up.
Usage is visible at GET /api/usage and in Settings → Usage, where runs_used is the exact count the 402 gate compares against the limit.

Managing the plan

PUT/api/workspace/plan

Switch tiers: { plan: free | starter | pro | enterprise }. Requires the manage_workspace capability (admin). Quotas and retention windows apply immediately; the change is audit-logged as plan_change with { from, to }.

Request → response
{ "plan": "pro" }
// →
{ "id": "0af31c92bd07", "name": "Acme", "plan": "pro", "member_count": 5, "created_at": "2026-06-01T00:00:00Z" }

Where billing plugs in

Locally and self-hosted, plan switching is an unmetered admin action. In the hosted product, checkout lands on exactly this seam — the enforcement layer (quotas, retention, 402s) is already real, so a payment integration changes who may call this endpoint, not what it does.

Budgets & cost enforcement

Quotas cap the number of runs; block budgets cap the dollars. A budget is now enforced on every trigger surface — manual POST /api/workflows/{id}/runs, replay, webhook, schedule, and MCP — so no path can quietly spend past the cap. A trigger that would exceed the budget is refused with 402 Payment Required and never queued.

Atomic reservations

A budget can carry a per-run reservation_estimate_usd. On each trigger the engine places a hold for that amount under a per-budget row lock, so concurrent triggers cannot race past the limit — at the boundary exactly one of N simultaneous triggers is admitted and the rest get 402. Each hold settles to the run's actual spend when it finishes, so an overestimate is released rather than lost.

Budget status reports the full ledger, so a dashboard can show headroom before a run is even created:

FieldTypeDescription
spent_usdnumberSettled cost of finished runs in the window.
reserved_usdnumberSum of open holds for in-flight runs, not yet settled.
committed_usdnumberspent_usd + reserved_usd — what is already promised against the cap.
remaining_usdnumberBudget limit minus committed_usd; the headroom a new trigger is checked against.
pctpercentcommitted_usd as a share of the limit.
overbooleanTrue once committed_usd meets or exceeds the limit — the state that returns 402.
projected_usdnumberForward projection of end-of-window spend from the current burn rate.
Set reservation_estimate_usd when you create or update a budget — POST /api/budgets and PUT /api/budgets/{id} both accept it. Leave it unset to enforce on settled spend alone, without per-run holds.

Override with approval

A run that failed on budget can be resumed past the cap, but only by an admin — the authorization is the approval, and the act is fully audited.

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

Lift or raise the per-run cost cap and resume a budget-failed run. Body { approved: true, cap_usd?, reason? }. Requires the manage_workspace capability. Writes a budget_override audit entry and an approval_override_granted governance event.

Request
{ "approved": true, "cap_usd": 5.00, "reason": "One-off backfill approved by finance" }

An override is a spending decision on the record

Because it can push a single run past the workspace budget, the override requires manage_workspace and leaves two trails: the tamper-evident budget_override audit entry (who, when, the cap) and an approval_override_granted governance event on the run.

Data retention

A background sweeper runs at startup and every 24 hours thereafter, deleting execution and audit data older than the plan window. Deleting a run cascades to its steps and checkpoints. A sweep that removes anything logs an audit entry (retention_sweep on the workspace) whose details are the counts it removed; a sweep that removes nothing is silent.

FieldTypeDescription
freeruns 7dRuns (with steps and checkpoints) kept 7 days; audit log kept 30 days.
starterruns 30dRuns kept 30 days; audit log kept 30 days.
proruns 90dRuns kept 90 days; audit log kept a year.
enterpriseruns keptBoth windows are unbounded — nothing is swept.

Windows are measured from each record's creation time. Runs and the audit log have independent windows, which is why an audit trail can outlive the runs it describes on the pro plan.

Retention is destructive by design

Swept runs — including their step traces and checkpoints — are gone. Export anything you need long-term before its window closes, or run a plan whose window matches your compliance requirements.

Failure & approval alerts

When a run fails or pauses at a human gate, the backend notifies your team. Delivery is entirely env-gated — unset means silent, so local development stays quiet. Slack and email fire independently: you can enable either, both, or neither.

FieldTypeDescription
SLACK_WEBHOOK_URLstringAn incoming-webhook URL. When set, posts one Slack message per failure or pending gate.
RESEND_API_KEYstringEnables email delivery (shared with the send_email tool and member invites).
ALERT_EMAILstringRecipient address for alert emails; requires RESEND_API_KEY to take effect.

Two events trigger an alert. A failedevent carries the run's error message; a paused event carries the human-gate prompt.

Slack messages
[Ballast] Run af772b3602ef of 'Support Triage Crew' failed: Node 'Fetch CRM' failed: tool 'crm' returned HTTP 500
[Ballast] Run b91c04d2ee01 of 'Support Triage Crew' is paused at a human gate: Review the drafted support reply before it is sent.
Email (subject + body)
Subject: [Ballast] Run failed — Support Triage Crew
Body:    Run af772b3602ef of 'Support Triage Crew' failed: Node 'Fetch CRM' failed: tool 'crm' returned HTTP 500

Subject: [Ballast] Approval needed — Support Triage Crew
Body:    Run b91c04d2ee01 of 'Support Triage Crew' is paused at a human gate: Review the drafted support reply before it is sent.
  • Alerts are fire-and-forget: the executor schedules them without awaiting, and any delivery problem is logged and never affects the run itself
  • The email body is the same one-line text as the Slack message, minus the [Ballast] prefix that only Slack carries
  • Member invites reuse the same email path — with RESEND_API_KEY set, invited members receive their invite code by email; the code stays visible in Settings → Members either way

Audit log

Every state-changing action is appended to an immutable trail with the actor, target, timestamp, and a small details object. Reads are never recorded. The actor is resolved from the request credential to a member name or email; actions taken by an unlinked API key (or, in open mode, by no credential at all) resolve to the workspace owner or to no actor.

GET/api/audit

Append-only audit trail, newest first. Params: limit (default 50, capped at 200), offset. Returns { logs, total }.

Response (excerpt)
{
  "logs": [
    {
      "id": "77b21c09aa04",
      "action": "approve",
      "actor_id": "3c1a9f22bd10",
      "actor_name": "Dana Ortiz",
      "target_type": "run",
      "target_id": "af772b3602ef",
      "details": { "node_id": "review" },
      "created_at": "2026-07-07T14:12:40Z"
    }
  ],
  "total": 128
}
FieldTypeDescription
create · update · deleteworkflowAuthoring changes; update bumps the version when the graph changes.
run · schedule_runworkflowManual and scheduler-fired triggers; details carry the run_id (schedule_run also carries schedule_id and cron).
approve · reject · resume · cancelrunHuman-gate decisions and run recovery.
schedule_create · schedule_update · schedule_deleteworkflowSchedule lifecycle; details carry schedule_id and cron.
update · plan_changeworkspaceRename and tier switch; plan_change details are { from, to }.
invite · approve_member · role_change · permissions_change · removememberMembership and per-member RBAC changes.
create · revokeapi_keyKey issuance and revocation.
retention_sweepworkspaceA sweep that removed data; details are the { runs, audit } counts.

Metrics

GET/api/metrics/overview

Dashboard aggregates computed across all runs in the workspace.

Response (excerpt)
{
  "total_runs": 412,
  "success_rate": 91.4,
  "total_cost_usd": 1.83271,
  "active_workflows": 3,
  "runs_by_day": [
    { "date": "2026-06-24", "count": 28, "cost_usd": 0.1142 }
  ],
  "eval_pass_rate": 87.5,
  "evaluated_runs": 64
}
FieldTypeDescription
total_runsintegerEvery run ever recorded in the workspace.
success_ratepercentSucceeded ÷ finished (success + failed) runs, to one decimal; 0.0 when nothing has finished.
total_cost_usdnumberSummed model + tool cost across all runs, to six decimals.
active_workflowsintegerCount of workflows with status published.
runs_by_dayarrayThe last 14 days, each { date, count, cost_usd }.
eval_pass_ratepercent | nullPassed checks ÷ total checks across every evaluated run; null when no checks have run.
evaluated_runsintegerRuns that produced at least one evaluation check.

Members & RBAC

Access is governed by ten granular capabilities. A member's role supplies a default set; per-member overrides create exceptions in either direction; the effective permission is the override if one is set, else the role default. API keys linked to a member act with that member's effective permissions.

FieldTypeDescription
viewall rolesView workflows, runs, and the audit log.
run_workflowsadmin · editorTrigger runs.
approve_gatesadmin · editorApprove or reject paused human gates.
cancel_resumeadmin · editorCancel and resume runs.
create_edit_workflowsadmin · editorCreate and edit workflows, custom tools, and schedules.
publish_workflowsadmin · editorPublish a workflow (draft to published).
delete_workflowsadminDelete workflows and their run history.
manage_membersadminInvite, approve, re-role, and remove members.
manage_api_keysadminCreate and revoke API keys.
manage_workspaceadminWorkspace settings and plan changes.
  • admin holds all ten capabilities; editor holds the six above through publish_workflows; viewer holds only view
  • Members carry a status: active, invited (holds an invite code), or pending(self-serve signup awaiting an admin's approval)
  • The last active admin is protected: you cannot demote or remove them, nor deny them manage_members or manage_workspace — promote another admin first

How requests map to capabilities

The auth middleware maps each request to a required capability and returns 403 "This action requires the 'X' permission" — when the credential lacks it.

FieldTypeDescription
GET / HEAD / OPTIONSviewAll reads, plus POST .../validate (a read-only check).
POST /api/workflows/{id}/runsrun_workflowsTriggering a run.
POST .../approveapprove_gatesApproving or rejecting a gate.
POST .../cancel · .../resumecancel_resumeCancelling or resuming a run.
POST/PUT /api/workflows · /api/templatescreate_edit_workflowsAuthoring and instantiating workflows.
DELETE /api/workflows/{id}delete_workflowsDeleting a workflow.
/api/schedules · /api/toolscreate_edit_workflowsManaging schedules and custom tools.
/api/membersmanage_membersAny member mutation.
/api/keysmanage_api_keysAny API-key mutation.
/api/workspacemanage_workspaceWorkspace and plan changes.
/api/mcpviewGateway is view-level; each tool enforces its own capability inside the MCP handler.
GET/api/members

List members, ordered by join time. Invite codes are shown only for members still in the invited state.

POST/api/members/invite

Invite a member: { name, email, role }. 400 if the email already belongs to a member. With RESEND_API_KEY set, the invite code is emailed. Returns 201.

GET/api/members/{id}

Member detail including every capability with its default, override, and effective value, plus the count of non-revoked API keys linked to them.

PUT/api/members/{id}

Change a member's role: { role }. Guarded against demoting the last active admin. Audit-logged as role_change.

PUT/api/members/{id}/permissions

Set per-member overrides: { overrides: { capability: true | false | null } }. null clears an override. Unknown capabilities return 400; last-admin denials are blocked.

Request
{ "overrides": { "delete_workflows": true, "publish_workflows": false } }
POST/api/members/{id}/approve

Approve a self-serve signup: moves a pending member to active. 400 unless the member is pending.

GET/api/members/{id}/stats

Per-member activity: runs triggered, approvals, rejections, workflows created, total cost, a 30-day activity series, and the 15 most recent actions.

DELETE/api/members/{id}

Remove a member. Guarded against removing the last active admin. Returns 204.

API keys

API keys are role-scoped bearer credentials sent in the X-API-Keyheader. A key linked to a member acts with that member's effective permissions; an unlinked key uses its own role's defaults. The full secret (prefixed aos_) is returned once at creation — only a display prefix is stored. Keys are revoked (flagged), never hard deleted, and last_used_at updates on each authenticated request.

Credential precedence in the auth middleware: X-API-Key wins over Authorization: Bearer <session token>. With neither, AUTH_MODE=open acts as the workspace owner and AUTH_MODE=required returns 401.
GET/api/keys

List keys, newest first: name, display prefix, role, linked member_id, created_at, last_used_at, revoked.

POST/api/keys

Create a key: { name, role, member_id? }. When member_id is set, the key adopts that member's role. The plaintext secret is returned once, in this response only.

Response (secret shown once)
{
  "id": "5f2ab9013c77",
  "name": "CI pipeline",
  "prefix": "aos_1a2b3c4d…",
  "role": "editor",
  "member_id": null,
  "revoked": false,
  "secret": "aos_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
}
DELETE/api/keys/{id}

Revoke a key: it stops authenticating immediately. Returns 204.

Usage

GET/api/usage

Current billing period at a glance — the same run count the 402 gate uses.

Response
{
  "plan": "free",
  "runs_used": 274,
  "runs_limit": 1000,
  "cost_usd": 0.83104,
  "period_start": "2026-07-01T00:00:00Z",
  "period_end": "2026-07-31T23:59:59Z"
}

runs_limit is null on the enterprise plan. The period spans the first and last day of the current UTC month.

Authentication modes

Auth is a single environment switch. Local development runs open; set it to required in production and hand out session tokens or API keys.

FieldTypeDescription
AUTH_MODEopen | requiredopen: no login; unauthenticated /api requests act as the workspace owner. required: every /api request needs a session token or API key, else 401.
SECRET_KEYstringSigns HMAC session tokens (7-day TTL). Unset means a random per-boot secret, so sessions do not survive a restart — set it in production.
OWNER_PASSWORDstringIn required mode, if the owner admin has no password at boot, one is set from this env var or generated and logged once.
RATE_LIMIT_PER_MINUTEintegerAbove 0, enables a per-IP request ceiling that returns 429. Off by default.
CORS_ORIGINScsvComma-separated browser origins allowed by CORS.
  • Exempt from auth even in required mode: /api/health, /api/auth/login, /api/auth/accept-invite, /api/auth/signup, and /api/hooks/*(a webhook's secret is its own credential)
  • On restart, runs left running or pending become resumable failures — their last checkpoint is intact, so POST /api/runs/{id}/resume picks up where they left off