---
title: Flow schema
path: reference/flow-schema
status: published
---

# Flow schema

ScaiFlow flows conform to **JSON Schema draft 2020-12** at `https://scailabs.com/schemas/scaiflow/v2.json`.

The schema is hand-mirrored in three places (all kept in lockstep — CI fails if they drift):

- **JSON Schema** — `packages/flow-schema/schema/v2.json` — the canonical reference.
- **Pydantic** — `packages/flow-schema/python/scaiflow_schema/models.py` — server-side validation.
- **TypeScript** — `packages/flow-schema/typescript/src/index.ts` — canvas + client typing.

## Top-level shape

```jsonc
{
  "$schema": "https://scailabs.com/schemas/scaiflow/v2.json",
  "id": "flow_<random>",                  // required, opaque string
  "name": "Untitled Flow",                // required
  "version": "1.0.0",                     // required, semver
  "scaicore_target": "0.2",               // ScaiCore language version
  "metadata": { /* see below */ },
  "nodes": [ /* see Node */ ],
  "edges": [ /* see Edge */ ],
  "variables": [],                        // flow-level state slots
  "config": { /* see FlowConfig */ },
  "tests": []                             // optional test fixtures
}
```

## `metadata`

```jsonc
{
  "author": "alice@acme.example",
  "created": "2026-04-01T...",            // ISO-8601
  "description": "What this flow does."
}
```

All fields optional.

## `Node`

```jsonc
{
  "id": "node_<random>",                  // required
  "type": "llm_flexible",                 // required, NodeKind enum
  "label": "Classify Intent",             // required
  "position": { "x": 400, "y": 200 },     // required
  "config": {},                           // per-kind shape; see Node kinds
  "inputs": [ /* Port[] */ ],
  "outputs": [ /* Port[] */ ]
}
```

### NodeKind enum (closed)

```text
entry_api, entry_webhook, entry_schedule,
llm_rigid, llm_guarded, llm_flexible,
logic_decision, logic_loop, logic_parallel,
tool_plugin, tool_http,
data_variable_set,
checkpoint,
queue_publish, queue_consume,
hitl_review, hitl_decision, queue_escalation,
compute_provision, compute_exec, compute_file_upload, compute_file_download, compute_destroy,
subflow_call
```

See [Node kinds reference](./node-kinds) for the per-kind `config` shape.

## `Port`

```jsonc
{ "id": "in_message",
  "label": "message",
  "type": "string" }
```

`type` is free-form ScaiCore type syntax: `string`, `int`, `float`, `bool`, `object`, `enum`, or a user-declared type name.

## `Edge`

```jsonc
{
  "id": "edge_<random>",                  // required
  "type": "sequential",                   // required, EdgeKind enum
  "source": { "node": "node_a", "port": "out" },     // required
  "target": { "node": "node_b", "port": "in" },      // required
  "condition": "confidence > 0.7"        // required when type === "conditional"
}
```

EdgeKind enum: `sequential`, `conditional`, `data`.

## `Variable`

```jsonc
{ "name": "intent",
  "type": "string",
  "default": null }
```

`name` and `type` required. Flow-level state slots — currently advisory (the compiler emits them; runtime enforcement TBD).

## `FlowConfig`

```jsonc
{
  "core_identity": { /* required */
    "name": "My Core",
    "description": "Optional",
    "models": [ /* ModelDecl[] — minItems: 1 */ ]
  },
  "plugins": [],                          // string[]; plugin names this flow uses
  "capabilities": [],                     // string[] OR object[]; capability flags + structured bunker block
  "bunker": { /* optional BunkerCapability */ },
  "scaiqueue": { /* optional ScaiQueueConfig */ },
  "hitl_mode": "explicit_publish",        // legacy, deprecated; M3 uses single @checkpoint with hitl_target
  "publish_as_model": false,
  "model_visibility_group_ids": []
}
```

### `ModelDecl`

```jsonc
{
  "role": "primary",                      // ^[a-z][a-z0-9_]*$, unique within models[]
  "ref": "scaigrid",                      // provider key
  "model": "openai/gpt-4o",               // model identifier
  "temperature": 0.5,                     // 0.0–2.0, optional
  "modalities": ["text", "structured_output"],     // default ["text"]
  "system_context": "...",                // optional
  "fallback": [                           // optional
    { "ref": "openai", "model": "gpt-3.5-turbo" }
  ]
}
```

First entry is the implicit `primary` default for any LLM node that doesn't specify `llm_role`.

### `ScaiQueueConfig`

```jsonc
{
  "scope": "support",                     // required, slug
  "queues": [
    {
      "slug": "review",                   // required
      "display_name": "Review queue",
      "description": "Where reviews go",
      "consumer_mode": "fifo",            // fifo | priority | deadline | broadcast
      "max_retries": 5,                   // 0–100
      "purpose": "review"                 // intake | processing | review | escalation | completed | dead-letter | custom
    }
  ]
}
```

Queue/HITL nodes that omit `scope` inherit from `flow.config.scaiqueue.scope` at compile time.

### `BunkerCapability`

```jsonc
{
  "lifecycle": "ephemeral",               // ephemeral | session | persistent | appliance
  "image": "python-3.12",                 // required
  "cpu_millicores": 1000,                 // 100–64000, default 1000
  "memory_mb": 1024,                      // 128–131072, default 1024
  "disk_mb": 2048,                        // 512–524288, default 2048
  "gpu_count": 0,                         // 0–8
  "gpu_type": "...",                      // optional
  "network_profile": "registry",          // isolated | registry | allowlisted | unrestricted | transit
  "network_allowlist": ["*.example.com"]  // used when network_profile=allowlisted
}
```

## `TestCase`

```jsonc
{
  "name": "Refund request",               // required
  "description": "Optional",
  "input": { "message": "I want a refund" },     // required
  "expect": { "intent": "complaint" },           // optional shallow == match
  "expect_expressions": []                       // optional free-form assertions
}
```

## Strictness

- `additionalProperties: false` at most levels — unknown keys are rejected on save.
- `$schema` at the top level is allowed (editor pointer convention).
- Pydantic + JSON Schema + TypeScript are kept in lockstep by `packages/flow-schema/python/tests/test_models.py` — drift fails CI.

## Validation

```bash
scaiflow validate my-flow.flow.json
```

Or via the REST API: `POST /v1/flows/validate` — see [Preview router](./rest-api/preview).
