API reference

Reference 04

Workflows And Automation API

app-internal workflowsblueprintsschedulesnotificationspermissionsworkshop

Who This Is For

This group is for a developer automating multi-step work in a local Relay instance. It covers workflows and the way they run, resume, and stop; blueprints that turn a template into a workflow; schedules that fire tasks on a cron; the notifications that carry approvals and alerts; the permission rules that govern what agents may do without asking; and the built-in Operator Workshop run lifecycle. Read this if you are launching workflow runs, scripting scheduled tasks, polling for pending approvals, managing an agent’s permission allow-list, or integrating the local workshop experience.

Stability

app-internal

These routes power the Relay web app. They are documented so you can build against a local instance, but their request and response shapes are tied to the app’s own screens and may change between releases. Branch on HTTP status codes rather than on the internal structure of an error body, and treat every response field as additive.

Local Access Model

Relay API routes run inside the local Relay app. Use http://127.0.0.1:<port> in examples, where <port> is the port your instance is bound to (3000 by default).

Do not expose these routes on a public network without your own access controls. These routes read and write the local SQLite database, run agent work in the background, and read and write blueprint files on disk.

Conventions For This Group

These behaviors hold across the routes below, so they are stated once here rather than repeated per endpoint:

  • Workflow status vocabulary. A workflow’s top-level status is one of draft, active, paused, completed, failed. A run in progress is active, not “running”. paused covers two cases: a delay step waiting for its time (the workflow carries a resumeAt) and a step waiting for human input. There is no separate stopped or cancelled status; a stopped workflow becomes failed.
  • Background execution. execute, resume, and step retry return 202 and run the work in the background. A 202 means the run was claimed and dispatched, not that it finished or even that it will succeed. Poll the status route for the outcome. The stop route is the exception: it acts synchronously and returns 200.
  • Timestamps. createdAt, updatedAt, and similar fields serialize to ISO date strings on the wire.
  • Malformed request bodies. Most write routes read the JSON body without a guard. A malformed or empty body throws before validation and surfaces as an unhandled 500, not a structured 400. Send well-formed JSON with Content-Type: application/json.
  • Validation. These routes validate by hand or with plain validator functions rather than a shared schema. Error bodies are { "error": "<message>" } with the specific message noted per route.

Endpoint Families

  • blueprints
  • workflows
  • schedules
  • notifications
  • permissions
  • workshop

Endpoints

Method(s)PathStabilitySource
GET, POST/api/blueprintsapp-internalsrc/app/api/blueprints/route.ts
DELETE, GET/api/blueprints/{id}app-internalsrc/app/api/blueprints/[id]/route.ts
POST/api/blueprints/{id}/instantiateapp-internalsrc/app/api/blueprints/[id]/instantiate/route.ts
POST/api/blueprints/importapp-internalsrc/app/api/blueprints/import/route.ts
GET, POST/api/workflowsapp-internalsrc/app/api/workflows/route.ts
DELETE, PATCH/api/workflows/{id}app-internalsrc/app/api/workflows/[id]/route.ts
GET/api/workflows/{id}/debugapp-internalsrc/app/api/workflows/[id]/debug/route.ts
DELETE, GET, POST, PUT/api/workflows/{id}/documentsapp-internalsrc/app/api/workflows/[id]/documents/route.ts
POST/api/workflows/{id}/executeapp-internalsrc/app/api/workflows/[id]/execute/route.ts
POST/api/workflows/{id}/resumeapp-internalsrc/app/api/workflows/[id]/resume/route.ts
GET/api/workflows/{id}/statusapp-internalsrc/app/api/workflows/[id]/status/route.ts
GET/api/workflows/{id}/targetapp-internalsrc/app/api/workflows/[id]/target/route.ts
POST/api/workflows/{id}/steps/{stepId}/retryapp-internalsrc/app/api/workflows/[id]/steps/[stepId]/retry/route.ts
POST/api/workflows/{id}/stopapp-internalsrc/app/api/workflows/[id]/stop/route.ts
POST/api/workflows/from-assistapp-internalsrc/app/api/workflows/from-assist/route.ts
POST/api/workflows/optimizeapp-internalsrc/app/api/workflows/optimize/route.ts
GET/api/notificationsapp-internalsrc/app/api/notifications/route.ts
DELETE, PATCH/api/notifications/{id}app-internalsrc/app/api/notifications/[id]/route.ts
PATCH/api/notifications/mark-all-readapp-internalsrc/app/api/notifications/mark-all-read/route.ts
GET/api/notifications/pending-approvalsapp-internalsrc/app/api/notifications/pending-approvals/route.ts
GET/api/notifications/pending-approvals/streamapp-internalsrc/app/api/notifications/pending-approvals/stream/route.ts
DELETE, GET, POST/api/permissionsapp-internalsrc/app/api/permissions/route.ts
DELETE, GET, POST/api/permissions/presetsapp-internalsrc/app/api/permissions/presets/route.ts
GET, POST/api/schedulesapp-internalsrc/app/api/schedules/route.ts
DELETE, GET, PATCH/api/schedules/{id}app-internalsrc/app/api/schedules/[id]/route.ts
DELETE, GET, PUT/api/schedules/{id}/budgetapp-internalsrc/app/api/schedules/[id]/budget/route.ts
POST/api/schedules/{id}/executeapp-internalsrc/app/api/schedules/[id]/execute/route.ts
GET/api/schedules/{id}/heartbeat-historyapp-internalsrc/app/api/schedules/[id]/heartbeat-history/route.ts
POST/api/schedules/parseapp-internalsrc/app/api/schedules/parse/route.ts
GET, POST/api/workshop/{id}app-internalsrc/app/api/workshop/[id]/route.ts
GET/api/workshop/{id}/exportapp-internalsrc/app/api/workshop/[id]/export/route.ts
POST/api/workshop/{id}/fallbackapp-internalsrc/app/api/workshop/[id]/fallback/route.ts
GET/api/workshop/handoffapp-internalsrc/app/api/workshop/handoff/route.ts
GET/api/workshop/preflightapp-internalsrc/app/api/workshop/preflight/route.ts
POST/api/workshop/startapp-internalsrc/app/api/workshop/start/route.ts

Endpoint Reference

GET /api/blueprints

Lists all workflow blueprints: built-in, user-installed, and any provided by plugins.

  • Request: none.
  • Response 200: a JSON array of blueprints. Each element:
    JSON
    {
      "id": "string",
      "name": "string",
      "description": "string",
      "version": "string",
      "domain": "work",
      "tags": ["string"],
      "pattern": "sequence",
      "variables": [ { "id": "string", "type": "text", "label": "string", "required": true } ],
      "steps": [ { "name": "string", "requiresApproval": false } ],
      "author": "string",
      "source": "string",
      "estimatedDuration": "string",
      "difficulty": "beginner",
      "isBuiltin": false
    }
    domain is one of work, personal. pattern is one of sequence, planner-executor, checkpoint. difficulty is one of beginner, intermediate, advanced.
  • Errors: none returned explicitly. A filesystem failure propagates unhandled.
  • Side effects: reads only.

POST /api/blueprints

Creates a user blueprint from a supplied YAML document.

  • Request body (JSON):
FieldTypeRequiredNotes
yamlstringyesThe blueprint YAML. Parsed and validated after the request is received; needs a pattern and at least one step.
  • Response 201: the created blueprint (same shape as a GET /api/blueprints element, with isBuiltin false).
  • Errors:
    • 400 when yaml is missing: { "error": "yaml field is required" }.
    • 400 on an invalid blueprint: { "error": "Invalid blueprint: <issues>" }.
    • 400 when the id already exists: { "error": "Blueprint \"<id>\" already exists" }, or another failure message.
  • Side effects: writes the blueprint file to the user blueprints directory and refreshes the blueprint cache.

GET /api/blueprints/{id}

Fetches a single blueprint by id.

  • Request: none.
  • Response 200: the blueprint object.
  • Errors: 404 when not found: { "error": "Blueprint not found" }.
  • Side effects: reads only.

DELETE /api/blueprints/{id}

Deletes a user blueprint file by id. Built-in blueprints cannot be deleted.

  • Request: none.
  • Response 200: { "ok": true }.
  • Errors:
    • 403 when the blueprint is built-in: { "error": "Cannot delete built-in blueprints" }.
    • 400 when the user blueprint is not found: { "error": "Blueprint \"<id>\" not found" }, or { "error": "Failed to delete blueprint" }.
  • Side effects: removes the blueprint file and refreshes the cache.

POST /api/blueprints/{id}/instantiate

Instantiates a blueprint into a new draft workflow, resolving its variables and skipping steps whose conditions are not met.

  • Request body (JSON):
FieldTypeRequiredNotes
variablesobjectyesA map of blueprint variable id to value. Required variables must be provided.
projectIdstringnoLinks the new workflow to a project.
  • Response 201:
    JSON
    { "workflowId": "string", "name": "string", "stepsCount": 0, "skippedSteps": ["string"] }
  • Errors:
    • 400 when variables is missing or not an object: { "error": "variables object is required" }.
    • 400 when the blueprint is not found: { "error": "Blueprint \"<id>\" not found" }.
    • 400 when a required variable is missing: { "error": "Missing required variables: ..." }.
    • 400 when every step is skipped by its conditions: { "error": "All steps were skipped by conditions ..." }, or another failure message.
  • Side effects: inserts one workflow row in draft status. It does not create tasks or start a run.

POST /api/blueprints/import

Imports a blueprint by fetching a YAML file from a GitHub URL.

  • Request body (JSON):
FieldTypeRequiredNotes
urlstringyesA GitHub URL to the blueprint YAML.
  • Response 201: { "ok": true, "id": "string", "name": "string" }.
  • Errors:
    • 400 when url is missing: { "error": "url is required" }.
    • 400 on a non-GitHub URL: { "error": "Only GitHub URLs are supported" }.
    • 400 when the fetch fails: { "error": "Failed to fetch blueprint: <status>" }.
    • 400 on any other failure, including a duplicate id: { "error": "<message>" }, or { "error": "Import failed" }.
  • Side effects: fetches from GitHub and writes a new user blueprint on disk.

GET /api/workflows

Lists all workflows with per-workflow task and document rollups.

  • Request: none.
  • Response 200: a JSON array, newest first. Each element:
    JSON
    {
      "id": "string",
      "name": "string",
      "projectId": "string | null",
      "definition": "string",
      "status": "draft",
      "runNumber": 0,
      "createdAt": "string",
      "updatedAt": "string",
      "taskCount": 0,
      "liveTaskCount": 0,
      "outputDocCount": 0
    }
    status is one of draft, active, paused, completed, failed. definition is a raw JSON string. liveTaskCount counts running and queued tasks.
  • Errors: none returned explicitly. A database failure propagates unhandled.
  • Side effects: reads only.

POST /api/workflows

Creates a workflow in draft status from a name and a definition.

  • Request body (JSON):
FieldTypeRequiredNotes
namestringyesNon-empty.
definitionobjectyesA workflow definition with a pattern and at least one step.
projectIdstring | nullnoMust resolve to an existing project when non-null.

The pattern is one of sequence, planner-executor, checkpoint, loop, parallel, swarm, each with its own step-shape rules.

  • Response 201: the created workflow row (status is draft, runNumber is 0).
  • Errors:
    • 400 when name is missing: { "error": "Name is required" }.
    • 400 when the definition is invalid: { "error": "Definition must include pattern and at least one step" }, or a specific validation message.
    • 400 when a step’s runtime and profile are incompatible: { "error": "<message>" }.
    • 404 when projectId does not resolve: { "error": "Project not found: <id>" }.
  • Side effects: inserts one workflow row. It does not create tasks or start a run.

PATCH /api/workflows/{id}

Edits a workflow’s name, definition, project, or success criteria, or pauses it. Editing is allowed only for draft, completed, or failed workflows.

  • Request body (JSON):
FieldTypeNotes
namestringEdit mode. Non-empty.
definitionobjectEdit mode. Re-validated.
projectIdstring | nullEdit mode. A non-null id must resolve; null clears the link; omission leaves it unchanged.
successCriteriaarrayEdit mode. Replaces the workflow’s run criteria.
statusstringTransition mode. Only paused is a real transition.
  • Response 200: in edit mode, the updated workflow row; in pause mode, { "id": "string", "status": "paused" }.
  • Errors:
    • 404 when not found: { "error": "Workflow not found" }.
    • 404 when projectId does not resolve: { "error": "Project not found: <id>" }.
    • 409 when editing an active or paused workflow: { "error": "Cannot edit active or paused workflows" }.
    • 400 when nothing to change: { "error": "status, name, projectId, definition, or successCriteria is required" }.
    • 409 when pausing a non-active workflow: { "error": "Can only pause an active workflow" }.
    • 400 when asked to set active: { "error": "Use POST /api/workflows/[id]/execute to resume a workflow" }.
    • 400 on an unsupported transition: { "error": "Invalid status transition: <status>" }.
  • Side effects: updates the workflow. Editing a completed or failed workflow resets it to draft and discards its execution state. Pausing flips the status but does not cancel running tasks.

DELETE /api/workflows/{id}

Deletes a workflow and cascade-deletes its tasks, documents, logs, and usage records.

  • Request: none.
  • Response 200: { "deleted": true }.
  • Errors:
    • 404 when not found: { "error": "Workflow not found" }.
    • 409 when the workflow is active with live tasks: { "error": "Cannot delete an active workflow ..." }.
    • 500 on failure: { "error": "<message>" }, or { "error": "Delete failed" }.
  • Side effects: cascade-deletes the workflow’s dependent records, then the workflow row.

GET /api/workflows/{id}/debug

Returns a failure analysis for a workflow: a root cause, a timeline, fix suggestions, and per-step errors.

  • Request: none.
  • Response 200:
    JSON
    {
      "rootCause": { "type": "unknown", "summary": "string" },
      "timeline": [ { "timestamp": "string", "event": "string", "severity": "error", "details": "string", "stepId": "string" } ],
      "suggestions": [ { "tier": "quick", "title": "string", "description": "string", "action": "string" } ],
      "stepErrors": [ { "stepId": "string", "stepName": "string", "error": "string" } ]
    }
    rootCause.type is one of budget_exceeded, timeout, transient, unknown. timeline[].severity is one of success, warning, error. suggestions[].tier is one of quick, better, best.
  • Errors: 500 on failure, including a workflow that does not exist: { "error": "<message>" }, or { "error": "Analysis failed" }.
  • Side effects: reads only.

GET /api/workflows/{id}/documents

Lists a workflow’s document bindings with the attached document metadata. A workflow with no bindings, and a workflow that does not exist, both return an empty array.

  • Request: none.
  • Response 200: a JSON array:
    JSON
    [
      {
        "bindingId": "string",
        "documentId": "string",
        "stepId": "string | null",
        "createdAt": "string",
        "document": { "id": "string", "originalName": "string", "filename": "string", "mimeType": "string", "size": 0, "direction": "output", "status": "ready", "category": "string | null" }
      }
    ]
    document is null when the bound document row is missing.
  • Errors: 500 on failure: { "error": "Failed to fetch workflow documents" }.
  • Side effects: reads only.

POST /api/workflows/{id}/documents

Attaches existing documents to a workflow, optionally scoped to a step.

  • Request body (JSON):
FieldTypeRequiredNotes
documentIdsstring[]yesNon-empty.
stepIdstringnoScopes the bindings to one step.
  • Response 201: { "attached": 0, "workflowId": "string", "stepId": "string | null" }. attached is the number of ids requested; duplicates are skipped.
  • Errors:
    • 400 when the request body is not valid JSON: { "error": "Invalid JSON body" }.
    • 400 when documentIds is empty: { "error": "documentIds must be a non-empty array" }.
    • 404 when the workflow is not found: { "error": "Workflow not found" }.
    • 404 when a document is not found: { "error": "Documents not found: <ids>" }.
    • 500 on other failure: { "error": "Failed to attach documents" }.
  • Side effects: inserts one document binding per id, skipping duplicates.

DELETE /api/workflows/{id}/documents

Removes document bindings from a workflow. With no body, removes all of them.

  • Request body (JSON), optional:
FieldTypeNotes
documentIdsstring[]The bindings to remove. Omit to remove all.
stepIdstringLimits removal to one step.
  • Response 200: { "ok": true }.
  • Errors: 500 on failure: { "error": "Failed to remove document bindings" }. A workflow that does not exist still returns { "ok": true }.
  • Side effects: removes the matching bindings, or all bindings when no ids are given.

PUT /api/workflows/{id}/documents

Atomically replaces the workflow-level document context while retaining any step-scoped bindings. This is the save contract used by the workflow form.

  • Request body (JSON):
FieldTypeRequiredNotes
documentIdsstring[]yesFull replacement set. An empty array clears workflow-level bindings.
  • Response 200: { "updated": 0, "workflowId": "string" }.
  • Errors:
    • 400 when the request body is not valid JSON: { "error": "Invalid JSON body" }.
    • 400 when the body is invalid or includes stepId: { "error": "documentIds must be an array; stepId is not accepted for replacement" }.
    • 404 when the workflow does not exist.
    • 404 when any document does not exist: { "error": "Documents not found: <ids>" }.
    • 500 on transaction failure: { "error": "Failed to replace workflow documents" }.
  • Side effects: validates every document first, then deletes and recreates workflow-level bindings in one SQLite transaction. Failed validation preserves the previous set.

POST /api/workflows/{id}/execute

Starts or re-runs a workflow. It atomically claims the workflow into active and dispatches execution in the background.

  • Request: none.
  • Response 202: { "status": "started", "workflowId": "string" }.
  • Errors:
    • 404 when not found: { "error": "Workflow not found" }.
    • 409 when the workflow is already running: { "error": "Workflow is already running" }.
    • 500 when the pre-run reset fails: { "error": "Failed to reset workflow state" }.
  • Side effects: for a completed, failed, or crashed run, cancels orphaned tasks and resets the workflow to draft, then claims it into active (the guard against double-execution) and runs it in the background. The run later moves the workflow to completed, failed, or paused.

POST /api/workflows/{id}/resume

Resumes a delay-paused workflow now, rather than waiting for its resumeAt. This supports the sequence pattern.

  • Request: none.
  • Response 202: { "status": "resuming", "workflowId": "string" }.
  • Errors:
    • 404 when not found: { "error": "Workflow not found" }.
    • 409 when the workflow is not paused: { "error": "Workflow is not paused (current status: <status>)", "status": "<status>" }.
  • Side effects: clears the pause and continues the run in the background from the next step.

GET /api/workflows/{id}/status

Returns a workflow’s full status: its per-step state, run history, and document lists. The shape depends on the workflow pattern.

  • Request: none.
  • Response 200: an object keyed on pattern. Common fields include id, name, status, projectId, projectName, definition, liveTaskCount, runNumber, runHistory, stepDocuments, and parentDocuments. projectName is the current linked name or null. The non-loop patterns add a resumeAt (epoch milliseconds or null), per-step state, and a workflowState; the loop pattern adds loopState and loopConfig. runHistory entries are { runNumber, taskCount, completedCount, failedCount }.
  • Errors: 404 when not found: { "error": "Workflow not found" }.
  • Side effects: reads only.

POST /api/workflows/{id}/steps/{stepId}/retry

Retries a single failed step after validating the workflow and step state, then dispatches the continued run in the background.

  • Request: none.
  • Response 202: { "status": "retry_started", "workflowId": "string", "stepId": "string" }.
  • Errors:
    • 404 when the workflow or step is not found.
    • 409 when the workflow is active or the step is not failed.
    • 500 for an unclassified retry failure.
  • Side effects: when the step is failed and the workflow is not active, resets the step, moves the workflow to active, and re-runs the step in the background.

POST /api/workflows/{id}/stop

Stops a running workflow. It cancels the workflow’s live tasks and marks the workflow and its state failed. This route acts synchronously.

  • Request: none.
  • Response 200: { "status": "stopped", "workflowId": "string", "cancelledTasks": 0 }.
  • Errors:
    • 404 when not found: { "error": "Workflow not found" }.
    • 409 when the workflow is not running: { "error": "Workflow is not running (current status: <status>)" }.
    • 500 when the workflow state cannot be parsed: { "error": "Failed to parse workflow state" }.
  • Side effects: cancels each live task, marks running and waiting steps failed, and sets the workflow status to failed. There is no stopped status.

GET /api/workflows/{id}/target

Previews every effective execution target Relay would use for the workflow without starting it.

  • Request: none.
  • Response 200: { "kind": "workflow", "ready": true, "targets": [ ], "context": { ... }, "error": null }, with one target per executable step. context.cell contains only vocabularyVersion and instanceId; the context also identifies the workflow project’s effective working directory (or launch-workspace fallback). Data-directory and database paths remain restricted to the Settings instance response. Neither field creates another customer data or credential boundary.
  • Errors:
    • 404 when not found: { "error": "Workflow not found" }.
    • 409 when any target cannot be resolved: { "kind": "workflow", "ready": false, "targets": [], "context": { ... } | null, "error": { } }.
  • Side effects: reads workflow, profile, routing, and runtime configuration only.

POST /api/workflows/from-assist

Creates a workflow and its per-step tasks together, and optionally starts the run immediately.

  • Request body (JSON):
FieldTypeRequiredNotes
namestringyesNon-empty.
definitionobjectyesA workflow definition, validated as in POST /api/workflows.
projectIdstringno
prioritynumbernoTask priority. Defaults to 2.
assignedAgentstringnoFallback runtime for steps that do not set their own.
executeImmediatelybooleannoWhen true, creates the workflow active and starts it.
  • Response 201:
    JSON
    { "workflow": { }, "taskIds": ["string"], "parentTaskId": null, "status": "created" }
    status is started when executeImmediately is set, otherwise created. workflow is the full workflow row.
  • Errors:
    • 400 when name is missing: { "error": "Name is required" }.
    • 400 when the definition is missing or invalid: { "error": "Definition must include pattern and at least one step" }, or a specific message.
    • 400 when a step’s runtime and profile are incompatible: { "error": "<message>" }.
    • 500 on failure: { "error": "Failed to create workflow and tasks" }.
  • Side effects: in one transaction, inserts the workflow and one planned task per step. When executeImmediately is set, also starts the run in the background.

POST /api/workflows/optimize

Returns advisory optimization suggestions for a workflow definition. Suggestions are advisory; the route changes nothing.

  • Request body (JSON):
FieldTypeRequiredNotes
definitionobjectyesThe workflow definition to analyze.
workflowIdstringnoEnables a budget estimate from past runs.
  • Response 200:
    JSON
    {
      "suggestions": [
        { "type": "budget_estimate", "title": "string", "description": "string", "data": {}, "action": { "label": "string", "type": "string", "payload": {} } }
      ]
    }
    type is one of document_binding, budget_estimate, runtime_recommendation, pattern_insight. The list may be empty.
  • Errors:
    • 400 when definition is missing: { "error": "definition is required" }.
    • 500 on failure: { "error": "<message>" }, or { "error": "Optimization failed" }.
  • Side effects: reads only.

GET /api/notifications

Lists up to 100 notifications, newest first, or just a count.

  • Request query parameters, all optional:
ParameterTypeNotes
unread"true"Returns only unread notifications.
typestringFilters by notification type.
countOnly"true"Returns { "count": 0 } instead of the rows.
  • Response 200: { "count": 0 } when countOnly is set, otherwise a JSON array of notification rows:
    JSON
    [
      {
        "id": "string",
        "taskId": "string | null",
        "type": "permission_required",
        "title": "string",
        "body": "string | null",
        "read": false,
        "toolName": "string | null",
        "toolInput": "string | null",
        "response": "string | null",
        "respondedAt": "string | null",
        "createdAt": "string"
      }
    ]
    type is one of permission_required, task_completed, task_failed, agent_message, budget_alert, context_proposal, context_proposal_batch, tier_limit. Resolved learning-proposal notifications are hidden from this list.
  • Errors: none returned explicitly. A database failure propagates unhandled.
  • Side effects: reads only.

PATCH /api/notifications/{id}

Sets a notification’s read flag and returns the updated row.

  • Request body (JSON):
FieldTypeRequiredNotes
readbooleannoDefaults to true.
  • Response 200: the updated notification row.
  • Errors: 404 when not found: { "error": "Not found" }.
  • Side effects: updates the notification’s read flag.

DELETE /api/notifications/{id}

Deletes a notification by id.

  • Request: none.
  • Response 200: { "success": true }.
  • Errors: none returned. Deleting an id that does not exist still returns { "success": true }.
  • Side effects: deletes the notification row.

PATCH /api/notifications/mark-all-read

Marks every unread notification as read.

  • Request: none.
  • Response 200: { "success": true }.
  • Errors: none returned explicitly.
  • Side effects: sets all unread notifications to read.

GET /api/notifications/pending-approvals

Returns the current pending approval and learning notifications as enriched payloads for an inbox.

  • Request: none.
  • Response 200: a JSON array. Each element includes channel, notificationId, taskId, workflowId, toolName, permissionLabel, compactSummary, deepLink, supportedActionIds, title, body, taskTitle, workflowName, toolInput, createdAt, read, and notificationType. supportedActionIds lists the actions the client may offer, such as allow_once, always_allow, deny, open_inbox.
  • Errors: none returned explicitly. A database failure propagates unhandled.
  • Side effects: reads only.

GET /api/notifications/pending-approvals/stream

Streams the pending-approvals list as Server-Sent Events, pushing a new frame whenever the list changes.

  • Request: none. The stream ends when the client disconnects.
  • Response 200, streaming, with headers Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive. Each data frame is the same pending-approvals array as the non-streaming route:
    Code
    data: [ ... ]
    The first frame is always sent, and a new frame follows only when the list changes. A keepalive comment (: keepalive) is sent every 15 seconds. The list is polled about every 0.75 seconds.
  • Errors: none as an HTTP status; the route always returns the stream. A database poll failure is retried silently and does not produce a client frame.
  • Side effects: reads only.

GET /api/permissions

Returns all saved permission allow-patterns.

  • Request: none.
  • Response 200: { "permissions": ["string"] }. Each entry is a pattern string such as Read or Bash(command:git *).
  • Errors: none returned explicitly.
  • Side effects: reads only.

POST /api/permissions

Adds a permission pattern to the allow-list. Adding a pattern that already exists is a no-op.

  • Request body (JSON):
FieldTypeRequiredNotes
patternstringyesNon-empty.
  • Response 200: { "success": true }.
  • Errors: 400 when pattern is missing: { "error": "pattern (string) is required" }.
  • Side effects: appends the pattern to the allow-list.

DELETE /api/permissions

Removes a permission pattern from the allow-list. The pattern to remove is given in the body. Removing a pattern that is not present still succeeds.

  • Request body (JSON):
FieldTypeRequiredNotes
patternstringyesThe exact pattern to remove.
  • Response 200: { "success": true }.
  • Errors: 400 when pattern is missing: { "error": "pattern (string) is required" }.
  • Side effects: removes the pattern from the allow-list.

GET /api/permissions/presets

Lists the built-in permission presets, each marked with whether it is currently fully active.

  • Request: none.
  • Response 200:
    JSON
    {
      "presets": [
        { "id": "string", "name": "string", "description": "string", "risk": "low", "patterns": ["string"], "active": false }
      ]
    }
    risk is one of low, medium, high. A preset is active when all of its patterns are on the allow-list.
  • Errors: none returned explicitly.
  • Side effects: reads only.

POST /api/permissions/presets

Enables a preset by adding all of its patterns to the allow-list. This is additive and skips patterns already present.

  • Request body (JSON):
FieldTypeRequiredNotes
presetIdstringyesThe preset to enable.
  • Response 200: { "success": true }.
  • Errors:
    • 400 when presetId is missing: { "error": "presetId (string) is required" }.
    • 404 when the preset is unknown: { "error": "Unknown preset: <presetId>" }.
  • Side effects: adds the preset’s patterns to the allow-list.

DELETE /api/permissions/presets

Disables a preset by removing the patterns unique to it. Patterns shared with another still-active preset are kept.

  • Request body (JSON):
FieldTypeRequiredNotes
presetIdstringyesThe preset to disable.
  • Response 200: { "success": true }.
  • Errors:
    • 400 when presetId is missing: { "error": "presetId (string) is required" }.
    • 404 when the preset is unknown: { "error": "Unknown preset: <presetId>" }.
  • Side effects: removes the preset’s unique patterns from the allow-list.

GET /api/schedules

Lists all schedules, newest first.

  • Request: none.
  • Response 200: a JSON array of schedule rows. Key fields include id, projectId, name, prompt, cronExpression, assignedAgent, agentProfile, recurs, status, maxFirings, firingCount, expiresAt, lastFiredAt, nextFireAt, type, heartbeatChecklist, createdAt, updatedAt. status is one of active, paused, completed, expired. type is one of scheduled, heartbeat.
  • Errors: none returned explicitly. A database failure propagates unhandled.
  • Side effects: reads only.

POST /api/schedules

Creates a scheduled or heartbeat schedule, turning the interval into a cron expression and staggering it to avoid collisions with other schedules in the same project.

  • Request body (JSON):
FieldTypeRequiredNotes
namestringyesNon-empty.
intervalstringyesA natural-language or cron interval.
promptstringfor scheduled typeThe task prompt.
type"scheduled" | "heartbeat"noDefaults to scheduled.
projectIdstringno
assignedAgentstringno
agentProfilestringnoChecked for runtime compatibility.
recursbooleannoDefaults to true.
maxFiringsnumberno
expiresInHoursnumbernoSets an expiry.
heartbeatChecklistarrayfor heartbeat typeAt least one { id, instruction, priority } item.
activeHoursStartnumbernoHeartbeat only. 0 to 23.
activeHoursEndnumbernoHeartbeat only. 0 to 23.
activeTimezonestringnoDefaults to UTC.
heartbeatBudgetPerDaynumberno
documentIdsstring[]noDocuments to link to the schedule.
  • Response 201:
    JSON
    {
      "schedule": { },
      "warnings": [ { "type": "cron_collision", "overlappingSchedules": ["string"], "overlappingMinutes": [0], "estimatedConcurrentSteps": 0 } ]
    }
    schedule is the created schedule row. warnings is empty unless the new schedule overlaps a busy schedule in the same project.
  • Errors:
    • 400 when name is missing: { "error": "Name is required" }.
    • 400 when interval is missing: { "error": "Interval is required" }.
    • 400 when a scheduled schedule has no prompt: { "error": "Prompt is required" }.
    • 400 when a heartbeat schedule has no checklist: { "error": "Heartbeat schedules require at least one checklist item" }.
    • 400 on out-of-range active hours: { "error": "Active hours start must be 0-23" }, or the end equivalent.
    • 400 when the interval cannot be parsed, or a profile is incompatible: { "error": "<message>" }.
  • Side effects: inserts one schedule row, staggering its cron minute to avoid collisions, and links any documents. It does not fire the schedule.

GET /api/schedules/{id}

Fetches one schedule with its firing history.

  • Request: none.
  • Response 200: the schedule row, with heartbeatChecklist parsed to an array (or null), plus a firingHistory array of the tasks this schedule has fired, newest first.
  • Errors: 404 when not found: { "error": "Schedule not found" }.
  • Side effects: reads only.

PATCH /api/schedules/{id}

Updates a schedule, including pausing and resuming it.

  • Request body (JSON), all fields optional:
FieldTypeNotes
status"paused" | "active"Pause or resume.
namestringNon-empty when given.
promptstringNon-empty when given.
intervalstringRe-parsed to a new cron expression.
assignedAgentstring
agentProfilestring
heartbeatChecklistarray
activeHoursStartnumber | null
activeHoursEndnumber | null
activeTimezonestring
heartbeatBudgetPerDaynumber | null
  • Response 200:
    JSON
    { "schedule": { }, "warnings": [ ] }
    schedule is the updated row; warnings follows the same collision shape as POST /api/schedules.
  • Errors:
    • 404 when not found: { "error": "Schedule not found" }.
    • 409 when pausing a non-active schedule: { "error": "Can only pause an active schedule" }.
    • 409 when resuming a non-paused schedule: { "error": "Can only resume a paused schedule" }.
    • 400 on an invalid status: { "error": "Invalid status: <status>" }.
    • 400 when a provided name or prompt is blank: { "error": "Name cannot be empty" }, or the prompt equivalent.
    • 400 when the interval cannot be parsed, or a profile is incompatible: { "error": "<message>" }.
  • Side effects: updates the schedule. Pausing clears the next fire time; resuming recomputes it. It does not fire the schedule.

DELETE /api/schedules/{id}

Deletes a schedule by id.

  • Request: none.
  • Response 200: { "deleted": true }.
  • Errors: 404 when not found: { "error": "Schedule not found" }.
  • Side effects: deletes the schedule row.

POST /api/schedules/{id}/execute

Fires a schedule now: it creates a queued task, atomically claims a concurrency slot, and dispatches the task in the background. Add ?force=true to bypass the concurrency cap.

  • Request query parameter:
ParameterTypeNotes
force"true"Bypasses the concurrency cap and records an audit entry.
  • Response 200: { "taskId": "string", "forced": false }.
  • Errors:
    • 404 when not found: { "error": "schedule_not_found" }.
    • 429 when the concurrency cap is full and force is not set: { "error": "capacity_full", "message": "Swarm at capacity (<running>/<cap>). Retry in ~60s or add ?force=true to bypass.", "slotEtaSec": 60 }.
  • Side effects: creates a queued task, claims a run slot, and runs the task in the background. When the slot cannot be claimed, the task is deleted again. This route does not advance the schedule’s own firing count or next fire time.

GET /api/schedules/{id}/heartbeat-history

Returns recent heartbeat evaluation entries and summary stats for a heartbeat schedule.

  • Request: none.
  • Response 200:
    JSON
    {
      "history": [ { "id": "string", "taskId": "string | null", "event": "heartbeat_action", "payload": "string | null", "timestamp": "string" } ],
      "stats": { "suppressionCount": 0, "lastActionAt": "string | null", "firingCount": 0, "heartbeatSpentToday": 0, "heartbeatBudgetPerDay": "number | null" }
    }
    event is one of heartbeat_action, heartbeat_suppressed.
  • Errors:
    • 404 when not found: { "error": "Schedule not found" }.
    • 400 when the schedule is not a heartbeat: { "error": "Not a heartbeat schedule" }.
  • Side effects: reads only.

GET /api/schedules/{id}/budget

Returns the schedule’s usage-budget policy and current usage snapshot.

  • Request: none.
  • Response 200: the schedule budget snapshot.
  • Errors: 404 when the schedule is not found; 500 for snapshot failures.
  • Side effects: reads the schedule, budget policy, and usage records only.

PUT /api/schedules/{id}/budget

Creates or replaces the schedule’s usage-budget policy.

  • Request: a budget policy accepted by Relay’s usage-budget schema.
  • Response 200: the updated schedule budget snapshot.
  • Errors: 400 with issues for an invalid policy, 404 when the schedule is not found, and 400 for other policy errors.
  • Side effects: writes the policy for this schedule.

DELETE /api/schedules/{id}/budget

Removes the schedule’s explicit usage-budget policy.

  • Request: none.
  • Response 200: { "success": true, "removed": true | false }.
  • Errors: none returned explicitly.
  • Side effects: removes the matching policy when present.

POST /api/schedules/parse

Parses a natural-language or cron interval into a cron expression, a description, the next fire times, and a confidence score. It creates nothing.

  • Request body (JSON):
FieldTypeRequiredNotes
expressionstringyesThe interval to parse.
  • Response 200:
    JSON
    {
      "cronExpression": "string",
      "description": "string",
      "nextFireTimes": ["string"],
      "confidence": 1.0
    }
    nextFireTimes holds three ISO timestamps.
  • Errors:
    • 400 when expression is missing: { "error": "Expression is required" }.
    • 400 when it cannot be parsed: { "error": "Could not parse \"<input>\". Try expressions like \"every weekday at 9am\", \"hourly\", \"5m\", or a cron expression." }.
  • Side effects: reads only.

GET /api/workshop/preflight

Checks whether the built-in Relay Operator Workshop can start in the current local instance.

  • Request: none.
  • Response 200: a preflight object with ready, edition identity and hash, Relay version compatibility, data-directory writability, configured runtimes, deterministic-fallback availability, fixture integrity, and a failures array. Each failure includes code, message, and recovery.
  • Errors: 500 with the standard workshop error payload when preflight fails unexpectedly.
  • Side effects: reads local release, data-directory, runtime, and built-in fixture state.

POST /api/workshop/start

Installs or reuses the content-addressed built-in workshop starter.

  • Request body (JSON), strict: { "editionId": "relay-operator-workshop", "confirmInstall": true }.
  • Response 201: a workshop run view containing edition identity, run status, linked project, app, workflow, and receipt ids, fallback state, checkpoint results, completion counts, errors, and timestamps.
  • Errors:
    • 400 when explicit installation confirmation is missing or invalid.
    • 409 on an installation conflict.
    • 412 when the workshop edition is incompatible with the Relay version.
    • 422 for another named workshop failure.
  • Side effects: creates the local workshop app, table, project, governed workflow, and persisted workshop run. The operation is idempotent for the built-in edition and does not overwrite conflicting user work.

GET /api/workshop/{id}

Returns the current evaluated state for one workshop run.

  • Request: none.
  • Response 200: the workshop run view, including five checkpoint results. Each checkpoint includes its source route, status, detail, and optional recovery.
  • Errors: 404 with { "error", "code", "recovery" } when the run does not exist.
  • Side effects: reads the run and evaluates its linked app, table, workflow, and Operations Receipt.

POST /api/workshop/{id}

Re-evaluates checkpoint state and clears a recoverable stored workshop error.

  • Request: none.
  • Response 200: the refreshed workshop run view.
  • Errors: 422 with the standard workshop error payload.
  • Side effects: updates the run status, error fields, and timestamp after evaluating current evidence.

POST /api/workshop/{id}/fallback

Runs the explicitly selected deterministic workshop rehearsal without a model or provider call.

  • Request: none.
  • Response 200: the updated workshop run view. Repeated calls return the existing terminal evidence when a receipt is already present.
  • Errors: 422 with a named workshop error and recovery instruction.
  • Side effects: creates deterministic task and document evidence, completes the governed workflow, composes an Operations Receipt, and marks the run as having used fallback.

GET /api/workshop/{id}/export

Builds and downloads the redacted workshop completion bundle.

  • Request: none.
  • Response 200: a ZIP attachment named relay-workshop-completion-<id>.zip, with Cache-Control: no-store.
  • Errors: 422 when terminal evidence is missing, redaction fails, or another named workshop export error occurs.
  • Side effects: builds the learner-owned app Pack and selected completion evidence, then marks the retain checkpoint passed after successful bundle construction.

GET /api/workshop/handoff

Downloads the versioned production handoff used by Relay, Website, and Motion workshop surfaces.

  • Request: none.
  • Response 200: a JSON attachment named relay-operator-workshop-production-handoff.json, with Cache-Control: no-store.
  • Errors: none returned explicitly.
  • Side effects: reads only.

Examples

Instantiate a blueprint into a draft workflow, then run it:

Terminal
curl -X POST http://127.0.0.1:3000/api/blueprints/<blueprintId>/instantiate \
  -H "Content-Type: application/json" \
  -d '{ "variables": { "topic": "Q3 launch" } }'

curl -X POST http://127.0.0.1:3000/api/workflows/<workflowId>/execute

Poll a running workflow’s status:

Terminal
curl http://127.0.0.1:3000/api/workflows/<workflowId>/status

Stream pending approvals into an inbox:

Terminal
curl -N http://127.0.0.1:3000/api/notifications/pending-approvals/stream

The -N flag keeps curl from buffering, so you see each frame as it arrives.

Check what a schedule interval resolves to before creating it:

Terminal
curl -X POST http://127.0.0.1:3000/api/schedules/parse \
  -H "Content-Type: application/json" \
  -d '{ "expression": "every weekday at 9am" }'

Do Not Depend On

  • A workflow’s top-level status has no running, stopped, or cancelled value. A run in progress is active; a stopped workflow is failed. paused covers both a delay wait and a human-input wait; tell them apart by the presence of resumeAt and the per-step state, not the top-level status.
  • execute, resume, and step retry return 202 before the work runs. A 202 means the run was claimed and dispatched, not that it succeeded. Poll the status route.
  • POST /api/workflows/{id}/steps/{stepId}/retry returns 202 before it validates anything, so an invalid retry looks identical to a valid one over HTTP. Confirm the outcome by polling the status route.
  • GET /api/workflows/{id}/debug returns 500, not 404, for a workflow that does not exist, because the analysis throws on a missing workflow.
  • GET and DELETE on /api/workflows/{id}/documents do not check that the workflow exists; a missing workflow yields an empty list or a success. POST and replace-all PUT validate the workflow.
  • Pausing a workflow through PATCH /api/workflows/{id} flips the status but does not stop tasks that are already running. Use POST /api/workflows/{id}/stop to cancel live tasks.
  • The POST /api/schedules/{id}/execute not-found body is { "error": "schedule_not_found" }, while the other schedule routes use { "error": "Schedule not found" }. Do not match on one spelling across all schedule routes.
  • Permission mutations return only { "success": true }; they do not echo the resulting allow-list. Re-read GET /api/permissions to observe the new state.

Orionfold Relay

Run your AI team from one place.

The engine is free and open. A license unlocks the premium packs you own and keeps them yours, offline. Founding seats from $349.

Explore Orionfold Relay