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:
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 for the full picture.
In ScaiCore source#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
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, optionaloutput schema. - Flexible Prompt —
goal,model role, optionaloutput schema,input bindings.
The Model role dropdown is populated from the flow's model registry. Adding a role in Flow → Models makes it available in every LLM node.