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
| Status | Meaning |
|---|---|
running | Currently executing |
paused | Waiting for approval (workflow only) — check pausedAt for step name |
pending | Skill output landed in review queue (requiresApproval skills) |
approved / rejected | Human acted on a pending skill run |
completed | Finished successfully |
failed | Errored — see error field |
cancelled | Explicitly 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" }
kind ∈ skill | workflow | mission; action ∈ approve | 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.