SynapseOrch 2.0 is a release candidate; the stable release is 1.9.3. How to try 2.0

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:

EventWhenFields
orchestration_startThe run startsrun_id, …
step_startA step startsorch_step_id, step_name, step_type
step_completeA step finishesorch_step_id, step_name, duration_seconds
step_error / step_warningA step reports a problemorch_step_id, error and the step's own reason / message
routing_decisionAn Evaluator, If / Else or Switch step chose a pathstep_type, decision, target_step_id
thinking, tool_execution, tool_result, llm_thoughtInside Agent stepsas in the Chat API
human_input_requiredThe run paused for a personrun_id, orch_step_id, prompt, fields, agent_context, channel_id — and kind: "tool_approval" for a tool confirmation
orchestration_completeThe run endedrun_id, status, final_state
finalThe run's resultresponse, data.step_history, data.shared_state
orchestration_errorThe run failederror
doneThe 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:

statusAlso carries
completed, failed, cancelledresponse, shared_state, step_history — the same fields /run returns for that status
pausedhuman_input_required with step_id, prompt and fields — also for a question asked inside a nested workflow. Answer it with /resume
runningshared_state and step_history so far, and no response yet. A run still inside its first step returns only status and run_id
queuedSynapseOrch 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-..."
QueryDefaultDescription
limit20How 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"])

On this page