docs/api/runs.md

Runs API

Triggering skills and workflows, polling for results, approving paused runs.

Trigger a skill

POST /api/v1/skills/:slug/runs
{
  "input": { "transcript": "...", "meeting_title": "..." },
  "idempotencyKey": "optional-client-id"
}

Returns immediately:

{
  "runId": 1234,
  "status": "running",
  "pollUrl": "/api/v1/runs/1234"
}

If the skill has requiresApproval: true, the run lands in the review queue as status: pending instead of running to completion.

Trigger a workflow

POST /api/v1/workflows/:slug/runs
{
  "input": { "transcript": "...", "prospect_name": "Acme" },
  "idempotencyKey": "..."
}

Workflows can take minutes or hours (they pause at approve gates). Use the webhook registration or poll to track progress.

Poll a run

GET /api/v1/runs/:id

Returns:

{
  "id": 987,
  "kind": "workflow",
  "slug": "discovery_followup",
  "status": "paused",
  "pausedAt": "review",
  "reviewUrl": "https://.../dashboard/review?runId=987",
  "stepResults": {
    "summary": { "output": { "prospect": "Acme", "pain": "..." } },
    "email": { "output": { "body": "...", "subject": "..." } }
  },
  "workspaceSha": "a8d1795",
  "createdAt": "2026-04-14T20:00:00Z",
  "completedAt": null
}

Possible statuses

StatusMeaning
runningCurrently executing
pausedWaiting for approval (workflow only) — check pausedAt for step name
pendingSkill output landed in review queue (requiresApproval skills)
approved / rejectedHuman acted on a pending skill run
completedFinished successfully
failedErrored — see error field
cancelledExplicitly cancelled

Approve a paused run

POST /api/v1/runs/:id/approve
{
  "reviewedBy": "jane@acme.com",
  "note": "Looks good — matches their stated budget"
}

Workflows advance to the next step; pending skill runs move to approved. Requires run:approve scope.

Reject

POST /api/v1/runs/:id/reject
{
  "reviewedBy": "jane@acme.com",
  "reason": "Tone is off — try again"
}

Cancel

POST /api/v1/runs/:id/cancel

Hard stop. Cannot be resumed.

The unified review queue ✓ live

Skills awaiting approval, paused workflows, and missions awaiting review all surface in one queue. Two endpoints — both live on tenant Bearer tokens — let an external app or inbox drive approvals without knowing which kind of run it's acting on.

List what's pending

GET /api/v1/reviews
Authorization: Bearer vcn_live_...
{
  "reviews": [
    { "kind": "mission",  "id": 42, "title": "Weekly PPC report — Daylyte", "status": "awaiting_review" },
    { "kind": "workflow", "id": 987, "title": "Workflow run #987",          "status": "paused" },
    { "kind": "skill",    "id": 1203, "title": "Skill run #1203",           "status": "pending" }
  ]
}

Decide

POST /api/v1/reviews/decide
Authorization: Bearer vcn_live_...
Content-Type: application/json
{ "kind": "mission", "id": 42, "action": "approve", "reason": "Numbers check out" }

kindskill | workflow | mission; actionapprove | reject; reason optional. The decision dispatches to the owning service (resume/cancel the workflow or mission, approve/reject the skill run) and returns the refreshed queue so your inbox stays in sync:

{ "ok": true, "reviews": [ /* what's still pending */ ] }

Deciding a review is the approve capability. The token's principal is enforced before anything dispatches — owners/PMs and client-reviewers hold it; a draft-only specialist token gets 403 FORBIDDEN. The reviewer recorded on the run is token:<id>, so the audit trail shows exactly which credential acted.

Route it to a person (team queue) ✓ live

The queue is multi-user. Each item carries an assignee; filter to a person's queue, or the unassigned/triage lane:

GET /api/v1/reviews?assignedTo=<userId>      # one person's queue
GET /api/v1/reviews?assignedTo=unassigned    # the triage lane

Each returned item includes assignedTo, snoozedUntil, and note. Route or snooze an item:

POST /api/v1/reviews/assign     { "kind": "mission", "id": 42, "assignedTo": "<userId>", "note": "SDR follow-up" }
POST /api/v1/reviews/snooze     { "kind": "workflow", "id": 987, "until": "2026-07-02T15:00:00Z" }

assignedTo: null unassigns. Snoozed items drop out of the active queue until their time (add nothing to see them; the list hides them by default). Both return the refreshed queue. Managing the queue is the same approve capability as deciding.

Streaming

Long-running workflows support server-sent events:

GET /api/v1/runs/:id/stream
Accept: text/event-stream

Events: step_started, step_completed, paused_for_review, approved, completed, failed.

Idempotency

Pass idempotencyKey (any client-chosen string) on POST to prevent duplicate runs on retry. Keys are scoped per-token and expire after 24h.

Retry with the same key + same body → same runId returned. Retry with the same key + different body → 409 CONFLICT.

Listing runs

GET /api/v1/runs?status=paused&kind=workflow&limit=50

Filters: status, kind (skill|workflow), rating (up|down), skillSlug, workflowSlug, agentSlug, createdAfter.

Returns a merged stream of skill + workflow runs for the tenant, sorted by createdAt desc:

{
  "runs": [
    {
      "kind": "skill",
      "id": 987,
      "slug": "discovery_summary",
      "status": "approved",
      "rating": "up",
      "feedbackNote": "Matched the prospect's language well",
      "workspaceSha": "a8d1795",
      "createdBy": "chris",
      "createdAt": "2026-04-15T07:22:00Z"
    }
  ]
}

Feedback

POST /api/v1/runs/:id/feedback
{ "rating": "up", "note": "Customer used this verbatim" }

Valid rating values: "up", "down", null (clears). Note is optional. Idempotent — posting the same body twice is a no-op; the feedbackAt timestamp updates on each call.

Works on any skill run regardless of status — pending, approved, rejected, or auto (skills that never hit the review queue). Useful for "was this useful?" post-hoc surveys.

See Feedback + logs for the full loop.