---
title: Node kinds
path: concepts/node-kinds
status: published
---

# Node kinds

ScaiFlow has 24 node kinds across 9 categories. Each kind maps to a specific ScaiCore construct — the compiler refuses to invent anything that doesn't exist upstream.

This page is conceptual; the [node-kinds reference](../reference/node-kinds) has the per-kind config shape, ports, and IR mapping.

## Categories (visual color cue on the canvas)

| Category | Color | Kinds |
|---|---|---|
| **Entry** | Green | `entry_api`, `entry_webhook`, `entry_schedule` |
| **LLM** | Blue | `llm_rigid`, `llm_guarded`, `llm_flexible` |
| **Logic** | Orange | `logic_decision`, `logic_loop`, `logic_parallel` |
| **Tool** | Purple | `tool_plugin`, `tool_http` |
| **Data** | Teal | `data_variable_set` |
| **Checkpoint** | Yellow | `checkpoint` |
| **Queue / HITL** | Amber | `queue_publish`, `queue_consume`, `hitl_review`, `hitl_decision`, `queue_escalation` |
| **Compute** | Indigo | `compute_provision`, `compute_exec`, `compute_file_upload`, `compute_file_download`, `compute_destroy` |
| **Sub-flow** | Grey | `subflow_call` |

## Entry points

How an invocation starts. Each entry kind compiles to a **top-level `IRTrigger`** declaration on `IRModule.triggers` — NOT to a block in the flow body. The closed set of trigger kinds is `{api, webhook, schedule}`.

There is no `entry_chat` — chat reachability is a publish-time concern, handled by `flow.config.publish_as_model` (see [Publish as model](./publish-as-model)).

- **`entry_api`** — HTTP-style invocation. The deployed Core exposes it at `POST /v1/modules/scaicore/cores/{id}/invoke`. Config: `method`, `path`.
- **`entry_webhook`** — same wire shape but typically called from an external system. Config: `method`, `path`, `auth`.
- **`entry_schedule`** — cron-driven trigger. Config: `cron` (5-field), `timezone`.

## LLM blocks

The three-tier rigidity spectrum — see [Rigidity](./rigidity-spectrum).

- **`llm_rigid`** — deterministic template, no model call.
- **`llm_guarded`** — bounded LLM call with `guard` (pre) and `validate` (post) expressions.
- **`llm_flexible`** — goal-driven LLM call with an optional output schema.

All three pick a model from the flow's [registry](./models-and-registry) via `llm_role`.

## Logic

- **`logic_decision`** — branches to one of N targets based on per-edge conditions. Compiles to a ScaiCore `match` block; the IR closed set has no separate `if` kind. Style is `if` (one or two branches) or `switch` (multi-branch on a shared subject); both compile identically — `switch` is the convention where every branch condition references the same subject.
- **`logic_loop`** — `for` (iterate a collection) or `while` (loop until condition false; `max_iterations` is mandatory).
- **`logic_parallel`** — fan-out branches; `max_concurrent` + `fail_fast` controls.

## Tool calls

- **`tool_plugin`** — invoke a registered ScaiGrid plugin. Config: `plugin`, `method`, `args`, optional `result_binding`. Compiles to a `plugin_call` statement (NOT a block — sits inside the parent `rigid` block).
- **`tool_http`** — placeholder for direct HTTP. ScaiCore forbids direct HTTP from Core code; this compiles to a `plugin_call` against an `http` plugin name (the canonical name isn't finalized in ScaiCore yet — see [Troubleshooting](../troubleshooting)).

## Data

- **`data_variable_set`** — bind an expression to a variable name. Compiles to `kind: "assign"` per the IR doc.

## Checkpoint

- **`checkpoint`** — pause execution. Useful as a standalone deterministic gate or as the underlying block kind for HITL Review.

## Queue / HITL

ScaiQueue is the asynchronous message bus + human-review surface. See [Queues](./queues) and [HITL](./hitl-and-checkpoints).

- **`queue_publish`** — `scaiqueue.publish(scope, queue, message_type, payload)`.
- **`queue_consume`** — `scaiqueue.consume(scope, queue, consumer_id)`.
- **`hitl_review`** — single `@checkpoint` with `hitl_target: {scope, queue, hitl_spec}`. Runtime auto-publishes the review request and auto-resolves on completion.
- **`hitl_decision`** — lightweight `@checkpoint` of type `"approval"`, no preceding queue publish. For cases where the agent already routed to a HITL surface.
- **`queue_escalation`** — `scaiqueue.escalate(...)`.

## Compute (ScaiBunker)

Sandboxed execution environments. See [Sandboxes](./sandboxes-and-bunkers).

- **`compute_provision`** — `scaibunker.create(image, lifecycle)`. Returns a bunker handle.
- **`compute_exec`** — `scaibunker.exec(command, timeout_s)`.
- **`compute_file_upload`** — `scaibunker.write_file(path, content)`.
- **`compute_file_download`** — `scaibunker.read_file(path)`.
- **`compute_destroy`** — `scaibunker.destroy()`.

A flow with any `compute_*` node auto-adds `scaibunker` to its `plugins` list and lifts a default `bunker` capability onto the manifest if the flow's `config.bunker` is unset.

## Sub-flow

- **`subflow_call`** — invoke another Core. Config: `target` (a `core://{tenant_slug}/{slug}` URI), `input_bindings`, `is_async`. Compiles to `IRCoreCallBlock` (`kind: "core_call"`).

## Adding a node kind

Extending the kind set isn't a casual change. You'd need to:

1. Add it to the JSON Schema's `NodeKind` enum (`packages/flow-schema/schema/v2.json`).
2. Add it to the Pydantic + TS enums.
3. Add a category + label + color entry.
4. Add a `_emit_*` function in `apps/compiler/scaiflow_compiler/codegen.py` and register it in `_DISPATCH`.
5. Add a default-config entry in `apps/canvas/src/canvas/nodeDefaults.ts`.

Skipping any one of these will compile but break at runtime.
