Workflows API
List orchestrations, run them synchronously or as a stream, read a run back after its stream is gone, resume runs that paused at a human gate, and cancel them.
The reference has a page for each of these endpoints, with every field and answer and samples in cURL, JavaScript, Python and Go, starting at Run a workflow; /api/v2's runs start at Start a run.
GET /api/v1/orchestrations
List the workspace's workflows. Synapse AI's own is not listed, and v1 does not serve, run or read it, nor its runs — each is a 404.
curl http://localhost:3000/api/v1/orchestrations \
-H "Authorization: Bearer sk-syn-..."[
{
"id": "orch_k3j9x2a",
"name": "Research → Report",
"description": "Research a topic and generate a report",
"steps": 5
}
]steps is the number of steps.
GET /api/v1/orchestrations/{orch_id}
Get one workflow's definition: its steps and how they connect, the entry step, the state schema and the guardrails. Returns 404 if there is no workflow with that ID.
curl http://localhost:3000/api/v1/orchestrations/orch_k3j9x2a \
-H "Authorization: Bearer sk-syn-..."{
"id": "orch_k3j9x2a",
"name": "Research → Report",
"description": "Research a topic and generate a report",
"entry_step_id": "step_research",
"state_schema": {
"research": { "type": "str", "default": "", "description": "Research notes" }
},
"steps": [
{
"id": "step_research",
"name": "Research",
"type": "agent",
"agent_id": "agent_1726412345678",
"prompt_template": "Research {state.user_input}",
"output_key": "research",
"next_step_id": "step_write",
"...": "..."
}
],
"max_total_turns": 100,
"timeout_minutes": 30,
"...": "..."
}Every step carries all of its fields, including those left at their defaults, so a step is much longer than shown here. Step fields are described in Step Types.
POST /api/v1/orchestrations/{orch_id}/run
Start a workflow and wait for it to finish or pause (synchronous).
Request
POST /api/v1/orchestrations/orch_k3j9x2a/run
Authorization: Bearer sk-syn-...
Content-Type: application/json{
"message": "SynapseOrch platform",
"initial_state": { "audience": "engineers" }
}message is the run's input. It is stored in shared state as user_input (and user_query) — steps read it from there, and an Agent or LLM step with no prompt template uses it as its task.
initial_state (optional) is merged into the run's shared state before the first step, so steps read state.audience and your state schema's keys can be set per run. A key that starts with _ is the engine's own and is refused with a 422, as is any field the endpoint does not take.
The run keeps going if the client disconnects; it is recorded in Runs › Logs either way.
Response — completed
{
"status": "completed",
"run_id": "run_orch_k3j9x2a_1734567890123_4f2a9c",
"response": "# SynapseOrch platform\n\nSynapseOrch is…",
"shared_state": {
"user_input": "SynapseOrch platform",
"user_query": "SynapseOrch platform",
"research": "...",
"article": "..."
},
"step_history": [
{
"step_id": "step_research",
"step_name": "Research",
"step_type": "agent",
"status": "completed",
"started_at": "2026-09-16T10:00:00Z",
"ended_at": "2026-09-16T10:00:12Z",
"duration_seconds": 12.4,
"output_key": "research"
}
]
}run_id is run_<orch_id>_<milliseconds>_<6 hex>. response is what the run made: the value of the workflow's Result key (Workflow settings → Guardrails) when the run wrote it, otherwise the output of the last step that produced content — a person's answer, an Evaluator's label and an End are how a run got somewhere, not what it made. A run that made nothing answers The workflow finished without producing an answer. Cost and token counts are not part of the response; see Usage.
Response — paused (human gate)
{
"status": "paused",
"run_id": "run_orch_k3j9x2a_1734567890123_4f2a9c",
"human_input_required": {
"step_id": "step_approve",
"prompt": "Review the draft and approve or reject it.",
"fields": [
{ "name": "decision", "type": "string", "label": "Decision" }
],
"agent_context": "The draft article text…"
}
}A run also pauses when an agent step reaches a tool call its Tool Policy wants confirmed. That pause is answered the same way, with {"response": {"approve": true}} — or false to refuse.
A Team step can wait on several questions at once, one per member. Its paused run adds questions, each with its own question_id; answer each with that question_id beside response on resume. Without one, the answer goes to the only or first question.
Response — failed
{
"status": "failed",
"run_id": "run_orch_k3j9x2a_1734567890123_4f2a9c",
"response": "Orchestration failed. Error: TimeoutError: Timed out after 300s",
"shared_state": { "...": "..." },
"step_history": [ "..." ]
}POST /api/v1/orchestrations/{orch_id}/run/stream
Start a workflow and receive every event as Server-Sent Events. The request body is the same as /run.
curl -X POST http://localhost:3000/api/v1/orchestrations/orch_k3j9x2a/run/stream \
-H "Authorization: Bearer sk-syn-..." \
-H "Content-Type: application/json" \
-N \
-d '{"message": "SynapseOrch platform"}'SSE event types
The stream carries the engine's events as they happen, as data: lines, with a : ping comment line after 10 seconds without one. The ones most clients use:
| Event | When | Fields |
|---|---|---|
orchestration_start | The run starts | run_id, … |
step_start | A step starts | orch_step_id, step_name, step_type |
step_complete | A step finishes | orch_step_id, step_name, duration_seconds |
step_error / step_warning | A step reports a problem | orch_step_id, error and the step's own reason / message |
routing_decision | An Evaluator, If / Else or Switch step chose a path | step_type, decision, target_step_id |
thinking, tool_execution, tool_result, llm_thought | Inside Agent steps | as in the Chat API |
human_input_required | The run paused for a person | run_id, orch_step_id, prompt, fields, agent_context, channel_id — and kind: "tool_approval" for a tool confirmation |
orchestration_complete | The run ended | run_id, status, final_state |
final | The run's result | response, data.step_history, data.shared_state |
orchestration_error | The run failed | error |
done | The stream is closing | — |
Container steps add their own events — parallel_start, branch_start, parallel_complete, loop_iteration, loop_complete, merge_complete, transform_result — and agents that plan add tasks_updated. Ignore events you don't use.
When the run pauses, the stream sends human_input_required, then done, and closes. Resume with one of the endpoints below.
A run belongs to the server, not to the stream: a caller that stops reading leaves it running to the end. To see how it ended, read it back; to stop it, cancel it.
GET /api/v1/orchestrations/runs/{run_id}
A run's state, and its result once it has ended. This is the way back for a caller whose /run/stream or /resume/stream connection dropped: the run carried on, and this returns what the stream would have ended with.
curl http://localhost:3000/api/v1/orchestrations/runs/run_orch_k3j9x2a_1734567890123_4f2a9c \
-H "Authorization: Bearer sk-syn-..."The answer has the shapes of /run:
status | Also carries |
|---|---|
completed, failed, cancelled | response, shared_state, step_history — the same fields /run returns for that status |
paused | human_input_required with step_id, prompt and fields — also for a question asked inside a nested workflow. Answer it with /resume |
running | shared_state and step_history so far, and no response yet. A run still inside its first step returns only status and run_id |
queued | SynapseOrch Cloud only: queue_position, the run's place in line. It is waiting for a slot in the plan's runs at once and starts as soon as one frees; a run accepted and not yet picked up still reads running |
A run that did not complete also carries error, saying why: the step that failed and its reason, the run's time budget or turn limit, a restart, or a parent run that stopped. A cancelled run's response is Orchestration cancelled.
shared_state leaves out the engine's own bookkeeping keys, as the run page does. An unknown run_id returns 404. Poll it rather than holding a connection: a run that is running has nothing more to say until its next step ends.
GET /api/v1/orchestrations/runs
The workspace's recent runs, newest first. Runs of Synapse AI's own workflow, and of workflows deleted since, are left out, and so is a nested run — it is part of the run that started it.
curl "http://localhost:3000/api/v1/orchestrations/runs?status=paused&limit=10" \
-H "Authorization: Bearer sk-syn-..."| Query | Default | Description |
|---|---|---|
limit | 20 | How many runs, 1–200 |
status | — | Only runs with this status: running, paused, completed, failed or cancelled, and on SynapseOrch Cloud queued |
orchestration_id | — | Only this workflow's runs |
Each run carries run_id, orchestration_id, status, started_at, ended_at, current_step_id, steps_completed, last_step_name, waiting_for_human, total_cost_usd and, for a run that did not complete, error.
GET /api/v1/orchestrations/runs/{run_id}/events
The run's recorded events, oldest first — the run log, as JSON. Pass after (an event id) to read only what came after it; the answer's last_id is the value to pass next time.
curl "http://localhost:3000/api/v1/orchestrations/runs/run_orch_k3j9x2a_1734567890123_4f2a9c/events?after=0" \
-H "Authorization: Bearer sk-syn-..."{
"events": [ { "id": 1, "type": "orchestration_start", "...": "..." } ],
"last_id": 42,
"run": { "status": "completed", "...": "..." }
}limit caps the page (default and most 5000). An unknown run_id returns 404.
GET /api/v1/orchestrations/runs/{run_id}/events/stream
The same events as a stream: everything after after (or the Last-Event-ID header), then the run followed live. Each frame carries an id: line, so a client that drops can reconnect from the last one it saw. stream_complete ends the stream once the run has ended. Open it at any point in a run's life, as often as you like — it does not affect the run.
Read it with fetch and the Authorization header, as in the Chat API's JavaScript example. A browser EventSource cannot send that header, and an API key in the URL (?token=) is refused with a 401.
Follow a run from a React app
A hook that follows a run live through /api/v2/orchestrations/runs/{run_id}/stream and reconnects where it dropped, sending the last id: it saw as Last-Event-ID. Read streams with fetch, not EventSource: the key goes in the Authorization header, which EventSource cannot send, and a key in the URL is refused. Keep the key on a server — a browser app calls your own backend, which adds it.
import { useState, useEffect } from "react";
// Point BASE at your own server route that adds the key: a browser app never ships it.
const BASE = "https://api.synapseorch.com/api/v2";
const HEADERS = { Authorization: "Bearer YOUR_API_KEY" };
function useOrchestrationRun(runId: string | null) {
const [events, setEvents] = useState<any[]>([]);
const [status, setStatus] = useState<"idle" | "running" | "done" | "refused">("idle");
useEffect(() => {
if (!runId) return;
const stop = new AbortController();
setStatus("running");
(async () => {
let lastId = ""; // resend it to replay what was missed
while (!stop.signal.aborted) {
try {
const res = await fetch(`${BASE}/orchestrations/runs/${runId}/stream`, {
headers: lastId ? { ...HEADERS, "Last-Event-ID": lastId } : HEADERS,
signal: stop.signal,
});
if (!res.ok) { setStatus("refused"); return; } // 401: the key; 404: the run
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buf = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break; // dropped: reconnect below
buf += decoder.decode(value, { stream: true });
const frames = buf.split("\n\n");
buf = frames.pop()!;
for (const frame of frames) {
for (const line of frame.split("\n")) {
if (line.startsWith("id: ")) lastId = line.slice(4);
if (!line.startsWith("data: ")) continue;
const event = JSON.parse(line.slice(6));
if (event.type === "done" || event.type === "stream_complete") {
setStatus("done");
stop.abort();
return;
}
setEvents((prev) => [...prev, event]);
}
}
}
} catch {
if (stop.signal.aborted) return;
}
await new Promise((r) => setTimeout(r, 2000));
}
})();
return () => stop.abort();
}, [runId]);
return { events, status };
}
// In your component:
// const { events, status } = useOrchestrationRun(runId);
// events.filter(e => e.type === "step_start").map(e => <StepCard key={e.orch_step_id} {...e} />)Self-hosted, BASE is your install's address with /api/v2, served in scale mode.
POST /api/v1/orchestrations/runs/{run_id}/resume
Submit the human's answer and continue a paused run (synchronous).
Request
POST /api/v1/orchestrations/runs/run_orch_k3j9x2a_1734567890123_4f2a9c/resume
Authorization: Bearer sk-syn-...
Content-Type: application/json{
"response": {
"decision": "approve",
"comments": "Looks great, publish it."
}
}response is required: an object or a plain string. question_id names which of a Team step's questions it answers; an id the run is not asking is refused. A Human step with no fields holds a plain answer as its text — "yes" and {"response": "yes"} both store yes — and a step with fields holds the object as sent, keyed by the fields' names. It is written to the Human step's output key and to human_response — see Human Gates.
An answer that says nothing — blank text, or {} — is refused with a 422: The answer is empty. Send text, or an object keyed by the question's field names. An object whose fields are blank is an answer. A field the endpoint does not take is a 422 that names it.
Response
The same shapes as /run: completed, paused (another gate), or failed — or cancelled, for a run that was cancelled while resuming. An unknown run_id returns 404. A run that is not waiting for an answer — it finished, or an answer is already being processed — returns 409, so a run is answered once. A run that a scale mode worker is running answers {"status": "resuming", "run_id": "…"} at once; read it back for the rest.
POST /api/v1/orchestrations/runs/{run_id}/resume/stream
Resume with an SSE stream instead of a synchronous response. Same request body as /resume, same events as /run/stream.
curl -X POST http://localhost:3000/api/v1/orchestrations/runs/run_orch_k3j9x2a_1734567890123_4f2a9c/resume/stream \
-H "Authorization: Bearer sk-syn-..." \
-H "Content-Type: application/json" \
-N \
-d '{"response": {"decision": "approve"}}'POST /api/v1/orchestrations/runs/{run_id}/cancel
Stop a run.
curl -X POST http://localhost:3000/api/v1/orchestrations/runs/run_orch_k3j9x2a_1734567890123_4f2a9c/cancel \
-H "Authorization: Bearer sk-syn-..."{"status": "cancellation_requested", "run_id": "run_orch_k3j9x2a_1734567890123_4f2a9c", "stopped": true}stopped is false for a run that had already finished; it keeps the status it finished with. An unknown run_id returns 404.
Complete example: Run → Human gate → Resume
import requests
BASE = "http://localhost:3000/api/v1"
HEADERS = {"Authorization": "Bearer sk-syn-..."}
# 1. Start the orchestration
resp = requests.post(
f"{BASE}/orchestrations/orch_k3j9x2a/run",
headers=HEADERS,
json={"message": "AI orchestration trends 2026"},
)
data = resp.json()
if data["status"] == "paused":
run_id = data["run_id"]
human_prompt = data["human_input_required"]["prompt"]
print(f"Human approval needed: {human_prompt}")
# 2. Collect human input (e.g. from your UI)
decision = input("Approve? (yes/no): ")
# 3. Resume the orchestration
resume_resp = requests.post(
f"{BASE}/orchestrations/runs/{run_id}/resume",
headers=HEADERS,
json={"response": {"decision": decision, "notes": "Approved via API"}},
)
final = resume_resp.json()
print(f"Status: {final['status']}")
print(f"Article: {final['shared_state'].get('article', '')[:200]}...")
elif data["status"] == "completed":
print(data["response"])Chat API
Chat with an agent over the REST API, as one synchronous request or a server-sent event stream, and list the agents you can call.
Knowledge API
Create knowledge bases, upload files, send records and web pages, follow each document, and search with filters and citations — the same paths on self-hosted and Cloud.