---
title: Compilation targets
path: concepts/ir-and-compilation-targets
status: published
---

# Compilation targets

The compiler can emit three formats from the same flow JSON. The right format depends on what's downstream.

| Format | When to use | Wire shape |
|---|---|---|
| **`yaml`** (default) | Deploy to ScaiGrid. | YAML manifest accepted by `POST /v1/modules/scaicore/cores`. |
| **`ir`** | Verification, debug, direct-runtime experiments. | SCIR MessagePack binary bundle — identical to `scaicore.compiler.serializer.IRSerializer`. |
| **`scai`** | Human inspection only. | `.scai` text source. Deprecated. |

Set via the CLI's `--format` flag (`scaiflow compile flow.json --format ir`) or via `compile_flow(..., output_format="ir")` from Python.

## YAML — the deploy artifact

```yaml
core:
  name: customer-support-agent
  mode: stateless
  runtime_version: "0.2"
  identity:
    provider: scaikey
    persona: "Customer Support Agent"
  model: scaigrid/openai/gpt-4o          # implicit primary, single-string
  plugins:
    - alias: scaidrive
      endpoint: plugin://scaidrive
  capabilities:
    - bunker:
        lifecycle: ephemeral
        image: python-3.12
  scaiqueue:
    scope: support
    queues: [...]
  flows:
    - name: customer-support-agent
      trigger: { type: api }
      steps:
        - llm_turn: "Classify the customer intent."
          meta: { node_id: node_classify, node_kind: llm_flexible, ... }
        - plugin_call: { alias: scaidrive, method: search, args: {...} }
          meta: { node_id: node_kb, ... }
        - llm_turn: "Generate a response grounded in the retrieved docs."
          guarded: true
          guard: "confidence > 0.7"
          meta: { node_id: node_respond, ... }
        - checkpoint:
            type: hitl_review
            scope: support
            queue: review
          meta: { node_id: node_review, ... }
  scaiflow_meta:
    source_id: flow_abc123
    source_version: 1.0.0
    scaicore_target: "0.2"
    publish_as_model: true
    model_visibility_group_ids: [grp_acme_eng]
    models:                                   # full multi-model registry
      - role: primary
        provider: scaigrid
        model: openai/gpt-4o
        modalities: [text, structured_output]
      - role: fast
        provider: scaigrid
        model: openai/gpt-4o-mini
        modalities: [text]
```

### Why `scaiflow_meta`

ScaiCore's YAML parser is intentionally permissive about unknown keys — it loads the body as `dict[str, Any]` and walks it into IR. ScaiFlow uses this to round-trip fidelity that the documented YAML keywords don't carry:

- The full **multi-model registry** (`scaiflow_meta.models`) — the documented YAML only has `core.model: "<provider>/<model>"` (single string).
- **Source linkage** — `source_id`, `source_version`, `scaicore_target` so the YAML can be reverse-engineered into a canvas flow.
- **Per-step `meta`** — `{node_id, node_kind, node_label, node_config}` so YAML → canvas import is lossless for ScaiFlow-emitted manifests.
- **Deploy intents** — `publish_as_model`, `model_visibility_group_ids` so the deployer can pick them up alongside the artifact.

Hand-edited YAML (authored outside ScaiFlow) won't have `meta` on steps; those steps land under `metadata.unknown_steps` after import. A heuristic mapper is a non-goal until a customer hits the wall.

## IR — the binary form

`output_format="ir"` produces a SCIR MessagePack bundle starting with the magic bytes `SCIR`. Wire-identical to what ScaiCore's `IRSerializer().serialize(module)` produces, so the upstream `IRSerializer().deserialize(bytes)` + `Verifier().verify(module)` round-trip end-to-end against ScaiFlow's emit.

When to use:

- **Verifying** — pass `--verify` (default for `ir` format) to run ScaiCore's own verifier against the bundle. Catches IR shape mismatches the YAML loader silently tolerates.
- **Direct runtime targets** — if a future deploy module accepts SCIR directly (today everything is YAML-via-ScaiGrid).
- **Debugging** — the IR is the canonical form; reading it as JSON via `deserialize_ir(payload)` from `scaiflow_compiler._scaicore` shows exactly what the runtime will execute.

## `.scai` text — deprecated

A best-effort emission of a `.scai`-syntax source file. Not what ScaiCore's parser canonically reads (the IR is); kept for inspection and as documentation when ScaiCore's grammar examples drift from what ScaiFlow's compiler actually produces.

## The shared analyze stage

All three emitters share the analyze pass that runs first:

1. Schema-validates the flow JSON.
2. Builds a `nodes_by_id` map.
3. Detects dangling edges (source/target node doesn't exist), missing ports.
4. Verifies entry-point presence (at least one of `entry_api`/`entry_webhook`/`entry_schedule`).
5. Detects control cycles.
6. Builds a topological order over control edges.

Failures here surface as `CompileError(message, errors=[...])` with one string per problem. The canvas's live preview pane renders them inline; CLI prints them to stderr.

## Round-trip

```mermaid
flowchart LR
  Flow1["flow JSON"] -->|compile yaml| Yaml["YAML"]
  Yaml -->|import yaml_import| Flow2["flow JSON ≈"]
```

Lossless for ScaiFlow-emitted YAML (every step has `meta` carrying its `node_id`/`node_kind`/`node_label`/`node_config`). Partial for hand-edited YAML (unknown steps preserved under `metadata.unknown_steps`).

```mermaid
flowchart LR
  Flow["flow JSON"] -->|compile ir| SCIR["SCIR bytes"]
  SCIR -->|deserialize| IR["IRModule dict"]
```

The IR direction is one-way at the moment — no IR → flow JSON importer.
