---
title: The rigidity spectrum
path: concepts/rigidity-spectrum
status: published
---

# The rigidity spectrum

ScaiCore — and ScaiFlow by extension — defines exactly **three** block types for steps that involve an LLM:

| Block | When to use | What it compiles to |
|---|---|---|
| `@rigid` | The step is deterministic, no LLM call (string templating, variable assignment, plugin call). | `kind: "rigid"` with statements inside. |
| `@guarded` | LLM call with pre/post conditions. A `guard` expression must evaluate truthy to enter; a `validate` expression must evaluate truthy on the output. Failure routes to `on_validation_failure` (catch block) or fails the flow. | `kind: "guarded"` with `goal`, `output_type`, `llm_role`, `guard`, `validate`, `on_validation_failure`. |
| `@flexible` | LLM call with a goal and optional output schema; the LLM has more freedom in how it satisfies the goal. | `kind: "flexible"` with `goal`, `output_type`, `llm_role`, `input_bindings`, optional `constraints` / `examples` / `on_failure`. |

There is no fourth block. ScaiCore reserves `@adaptive` as a keyword but it has **no implementation** — don't expect it to ever resolve. Adaptive-feeling behavior is composed from the other three plus `@checkpoint`.

## Why three (not many)

Each block has a contract the runtime can enforce:

- `@rigid` — fully deterministic; no token budget, no failure modes from the LLM.
- `@guarded` — bounded behavior: the guard limits what the LLM is asked to do, the validate limits what counts as a valid answer. Use this when you'd rather refuse than guess.
- `@flexible` — open-ended; you trade enforceability for capability. Use this when you trust the LLM more than the rules you could write down.

Putting an LLM step inside the right block is the single biggest reliability lever in ScaiCore. Rigid the parts that don't need an LLM; guard the parts that need one but where wrong answers are expensive; reach for flexible only when neither of the others fits.

## The adaptive approval pattern

When you want LLM-driven decisions that pause for human approval at run time, compose `@flexible` (the decision) with `@checkpoint` (the pause) via a `@logic_decision`:

```mermaid
flowchart LR
  Decide["@flexible<br/>decides"]
  Branch{"@logic_decision<br/>confidence > threshold?"}
  Continue["continue"]
  Review["@checkpoint<br/>HITL review"]
  Decide --> Branch
  Branch -->|yes| Continue
  Branch -->|no| Review
  Review --> Continue
```

This is what older versions of the spec called "adaptive approval". It's not a new block type — it's a composition pattern. ScaiFlow's `hitl_review` node compiles to exactly this shape: a single `@checkpoint` with `hitl_target: {scope, queue, hitl_spec}`. ScaiCore's runtime detects `hitl_target`, auto-publishes the review request to ScaiQueue, suspends the invocation, and auto-resolves it when ScaiQueue reports the message completed.

See [Concepts: HITL and checkpoints](./hitl-and-checkpoints) for the full picture.

## In ScaiCore source

```scaicore
// Rigid — deterministic, no LLM call.
greeting = @rigid {
    "Hello {{user_name}}, here's your update."
}

// Guarded — LLM call with pre/post conditions.
response = @guarded {
    model = "primary"
    goal = "Answer using only the retrieved docs."
    guard: kb_results.length > 0
    validate: result.citations.length > 0
}

// Flexible — goal-driven LLM call.
intent = @flexible {
    model = "fast"
    goal = "Classify the customer's intent."
    output: Classification
}
```

## On the canvas

The three rigidity levels map to three node kinds:

- `llm_rigid` → "Strict Prompt"
- `llm_guarded` → "Guarded Prompt"
- `llm_flexible` → "Flexible Prompt"

Their property panels expose only the fields each block actually needs:

- **Strict Prompt** — `template` (the literal output, with `{{variable}}` substitutions).
- **Guarded Prompt** — `goal`, `model role`, `guard`, `validate`, optional `output schema`.
- **Flexible Prompt** — `goal`, `model role`, optional `output schema`, `input bindings`.

The Model role dropdown is populated from the flow's [model registry](./models-and-registry). Adding a role in Flow → Models makes it available in every LLM node.
