REST API: Flows
Flow CRUD, deploy, tests, versions, presence, deployment history.
All endpoints under /api/v1/flows. All require scaicore:view (read) or scaicore:manage (write) plus per-flow ACLs (the caller must hold the requested access level on the specific flow).
GET /v1/flows#
List flows visible to the caller (filtered by ACLs).
Query params:
search— substring filter on name.limit— page size, default 100.
Response:
{
"items": [
{
"id": "flow_abc123",
"name": "Customer Support",
"description": "Triage + KB + HITL",
"owner_id": "usr_xxx",
"tenant_id": "tnt_acme",
"version": "1.0.2",
"created_at": "2026-04-01T...",
"updated_at": "2026-04-29T..."
}
],
"total": 1
}
POST /v1/flows#
Create a flow.
Body:
{
"name": "My Flow",
"description": "Optional",
"content": { /* full FlowGraph JSON */ }
}
Response: 201 Created with the full FlowDetail (summary + content).
Owner is the calling principal, implicit admin ACL.
GET /v1/flows/{flow_id}#
Read one flow with its content.
Response: FlowDetail — summary fields + the content (full FlowGraph JSON).
scaicore:view + view ACL on this flow.
PUT /v1/flows/{flow_id}#
Update a flow. Partial body — any field omitted is left unchanged.
Body:
{
"name": "Renamed flow",
"description": "New description",
"content": { /* new FlowGraph */ } // optional; bumps version + writes a new storage key
}
If content is provided, the version is patch-bumped (1.0.2 → 1.0.3) and the new content is written to object storage at flows/{tenant}/{flow}/v{new_version}.flow.json. The old version's content stays in object storage (enables version history + diff).
If only metadata fields change, the version stays.
scaicore:manage + edit ACL.
DELETE /v1/flows/{flow_id}#
Soft-delete a flow. Sets deleted_at; content stays in object storage for audit/restore.
scaicore:manage + admin ACL.
POST /v1/flows/{flow_id}/deploy#
Compile the flow + deploy to ScaiGrid + optionally publish as chat model. See Concepts: Architecture for what happens.
Body:
{
"publish_as_model": true, // overrides flow.config.publish_as_model if set
"group_ids": ["grp_xxx"] // overrides flow.config.model_visibility_group_ids
}
Both fields are optional; defaults come from the flow's config.
Response:
{
"core_id": "core_xxx",
"published_slug": "scaicore/acme/my-flow", // null if not published
"published_model_id": "model_xxx", // null if not published
"scaiqueue_provisioning": { // null if no scaiqueue topology
"scope_id": "sc_xxx",
"scope_slug": "support",
"queue_ids": { "review": "q_xxx" }
}
}
scaicore:manage + deploy ACL.
Errors:
- 412
missing_scaigrid_credentials— no ScaiGrid token available (caller has no JWT and no per-tenant API key is set). - 502
scaicore_compile_failed— flow doesn't compile. - 502
scaiqueue_provision_failed— pre-deploy ScaiQueue provisioning failed. - 502
scaicore_create_failed— ScaiGrid rejected the manifest.
POST /v1/flows/{flow_id}/run-tests#
Run the flow's tests[] fixtures against a deployed Core.
Body:
{
"core_id": "core_xxx",
"name_filter": "Refund" // optional substring filter
}
Response:
{
"results": [
{
"name": "Refund request",
"ok": true,
"expect": { "intent": "complaint" },
"output": { "intent": "complaint", "urgency": "medium", ... },
"error": null
}
],
"passed": 1,
"failed": 0
}
scaicore:manage + deploy ACL.
GET /v1/flows/{flow_id}/versions#
List every historical version of the flow.
Response:
[
{ "version": "1.0.3", "is_current": true, "last_modified": "2026-04-29..." },
{ "version": "1.0.2", "is_current": false, "last_modified": "2026-04-28..." },
{ "version": "1.0.1", "is_current": false, "last_modified": "..." }
]
scaicore:view + view ACL.
GET /v1/flows/{flow_id}/versions/{version}#
Fetch a specific historical version's content.
Response: FlowDetail with the historical content.
POST /v1/flows/{flow_id}/presence/heartbeat#
Used by the canvas to announce "I'm editing this flow right now". Stored in-memory with a 30-second TTL. The toolbar's presence-avatar row reads from GET /v1/flows/{id}/presence.
Body:
{ "display_name": "Alice", "color": "#a4ff32" }
Response: 204 No Content.
GET /v1/flows/{flow_id}/presence#
List currently-active editors (heartbeat within the last 30 s), excluding the calling user.
Response:
{
"peers": [
{ "user_id": "usr_xxx", "display_name": "Bob",
"color": "#3284ff", "last_seen_ago_s": 4 }
]
}
DELETE /v1/flows/{flow_id}/presence#
Explicit "I'm leaving" — fires on beforeunload from the canvas. 204 No Content.
GET /v1/flows/{flow_id}/deployments#
List historical deploys of this flow.
Response:
[
{ "core_id": "core_xxx",
"deployed_at": "2026-04-29...",
"deployed_by": "usr_xxx",
"published_slug": "scaicore/acme/my-flow",
"flow_version": "1.0.2" }
]
Used by the canvas Live Runs panel to map a flow → its core_id(s) so it can poll events for them.
WebSocket: /v1/flows/{flow_id}/sync#
CRDT relay. Binary Yjs protocol messages shuttled between connected peers in the same flow. Authenticated via the same access token (passed as a query param or subprotocol header — see the canvas implementation for the wire format).
Out of scope for typical REST consumers; the canvas uses it for real-time collaborative editing.