---
title: 'REST API: Flows'
path: reference/rest-api/flows
status: published
---

# 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:**

```jsonc
{
  "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:**

```jsonc
{
  "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:**

```jsonc
{
  "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](../../concepts/architecture#deploy) for what happens.

**Body:**

```jsonc
{
  "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:**

```jsonc
{
  "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:**

```jsonc
{
  "core_id": "core_xxx",
  "name_filter": "Refund"          // optional substring filter
}
```

**Response:**

```jsonc
{
  "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:**

```jsonc
[
  { "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:**

```jsonc
{ "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:**

```jsonc
{
  "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:**

```jsonc
[
  { "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.
