---
title: The flow graph
path: concepts/flow-graph-model
status: published
---

# The flow graph

A ScaiFlow flow is a JSON document conforming to the [v2 flow schema](../reference/flow-schema). Conceptually it's a directed graph of nodes connected by edges, with some flow-level metadata.

## Shape

```jsonc
{
  "id": "flow_abc123",
  "name": "Customer Support Agent",
  "version": "1.0.0",
  "scaicore_target": "0.2",
  "metadata": { "author": "...", "description": "..." },
  "nodes": [ /* see Node, below */ ],
  "edges": [ /* see Edge, below */ ],
  "variables": [],            // flow-level named state slots
  "config": {                 // flow-level identity + capabilities
    "core_identity": {
      "name": "Customer Support",
      "models": [
        { "role": "primary", "ref": "scaigrid", "model": "openai/gpt-4o",
          "modalities": ["text", "structured_output"] }
      ]
    },
    "plugins": [],
    "capabilities": [],
    "bunker": { /* optional ScaiBunker sandbox spec */ },
    "scaiqueue": { /* optional queue topology */ },
    "publish_as_model": false,
    "model_visibility_group_ids": []
  },
  "tests": [ /* optional test fixtures */ ]
}
```

`id` is opaque (UUID-like) and stable across saves. `version` follows semver patch-bumping on every content update (managed server-side). `scaicore_target` pins the ScaiCore language version the compiler targets.

## Nodes

```jsonc
{
  "id": "node_<random>",
  "type": "llm_flexible",      // one of the closed node-kind enum
  "label": "Classify Intent",  // user-visible name on the canvas
  "position": { "x": 400, "y": 200 },
  "config": { /* per-kind shape; see Node kinds reference */ },
  "inputs":  [ { "id": "in", "label": "message", "type": "object" } ],
  "outputs": [ { "id": "out", "label": "result", "type": "object" } ]
}
```

Every node carries a kind, a position, a configuration object whose shape depends on the kind, and zero-to-many input/output ports. Ports are the attachment points for edges; the `type` field is free-form ScaiCore type syntax (`string`, `int`, `object`, or a user-declared type name).

The closed list of kinds:

- **Entry**: `entry_api`, `entry_webhook`, `entry_schedule`
- **LLM**: `llm_rigid`, `llm_guarded`, `llm_flexible`
- **Logic**: `logic_decision`, `logic_loop`, `logic_parallel`
- **Tool**: `tool_plugin`, `tool_http`
- **Data**: `data_variable_set`
- **Checkpoint**: `checkpoint`
- **Queue/HITL**: `queue_publish`, `queue_consume`, `hitl_review`, `hitl_decision`, `queue_escalation`
- **Compute**: `compute_provision`, `compute_exec`, `compute_file_upload`, `compute_file_download`, `compute_destroy`
- **Sub-flow**: `subflow_call`

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

## Edges

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

Three edge kinds:

- **sequential** — unconditional next-step.
- **conditional** — has a `condition` expression that must evaluate truthy for control to flow this way. Decision nodes use these for branching.
- **data** — pure data dependency without control transfer (rare; reserved for cases where a later block needs a value from an earlier block that isn't on the control path).

Conditional edges are how `logic_decision` nodes branch — there's no per-branch output port; you attach N conditional edges to the single output, each with its own boolean expression. See [Decision](../concepts/node-kinds#logic_decision).

## Variables

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

Flow-level named state slots. Currently advisory — the compiler emits them on the manifest but doesn't enforce typing. Use them to document the "variables in scope" for downstream readers.

## Tests

```jsonc
{
  "name": "Classifies a refund request",
  "description": "Inputs '{message: \"I want a refund\"}', expects intent='complaint'.",
  "input": { "message": "I want a refund" },
  "expect": { "intent": "complaint" },
  "expect_expressions": []
}
```

Run via [`scaiflow test`](../reference/cli#test) (compile-only) or `POST /v1/flows/{id}/run-tests` with a deployed `core_id` (assertion against live output).

## URLs and identity

A flow is uniquely addressed by `(tenant_id, flow_id)`. The flow's URL on the canvas is `<canvas-host>/flows/<flow_id>`. Object storage keys follow `flows/{tenant_id}/{flow_id}/v{version}.flow.json` — every version is preserved (never updated in place), so history + diff just enumerate object-storage keys.

## Compilation

The canvas can serialize the flow at any time and ship it to `/v1/flows/compile` for live preview, or `/v1/flows/{id}/deploy` for actual deployment. See [Compilation targets](./ir-and-compilation-targets) for what comes out the other end.
