The flow graph
A ScaiFlow flow is a JSON document conforming to the v2 flow schema. Conceptually it's a directed graph of nodes connected by edges, with some flow-level metadata.
Shape#
{
"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#
{
"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 for the per-kind config shapes.
Edges#
{
"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
conditionexpression 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.
Variables#
{ "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#
{
"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 (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 for what comes out the other end.