Blocks
A @flow is a sequence of blocks. Each block is one execution unit; the runtime dispatches on a kind discriminator and the executor knows how to drive each kind. There are fourteen of them, but you'll write @rigid and @flexible for most everything. The other twelve cover control flow, external calls, and lifecycle concerns. The single most useful distinction to internalize is rigid vs flexible: rigid is deterministic and replayable, flexible is LLM-driven and not.
Computation blocks#
These produce a value (or update memory) without changing control flow.
@rigid — a deterministic sequence of statements. Variable assignments, memory reads and writes, plugin calls, conditionals, simple math. The verifier rejects an LLM call inside @rigid (error E405) and rejects non-deterministic operations (E400) because rigid blocks must be replayable from a serialized scope snapshot during checkpoint resume.
@flexible — an LLM call with a goal, optional inputs, an output type, and per-block constraints. The compiler turns the output: declaration into a structured-output schema; the runtime hands it to the model provider. The block's "result" is whatever the model returns, type-checked against the schema.
1 2 3 4 5 | |
@guarded — @flexible plus a list of post-conditions that must hold on the model's output. If any check fails, an optional on_validation_failure body runs; otherwise the runtime raises GuardViolationError. Use this when the model's structured output isn't enough — when you also need invariants like "all citations resolve" or "no claim is unsupported".
@model_call — non-text model invocations: TTS, STT, embeddings, image generation, audio generation. Same shape as @flexible (input bindings, output type) but a modality selects the call type instead of producing text.
Control flow#
These compose other blocks. Each takes a body or a list of branches.
@parallel — execute branches concurrently. Optional max_concurrent and fail_fast. Results are returned as a list in branch order. A @checkpoint inside a @parallel branch is rejected by the verifier (E403) — suspend/resume across parallel branches isn't supported.
@foreach — iterate a collection, run the body for each element. Optionally yield expressions to build a result list.
@match — pattern matching with arms. Patterns can be literals, enum symbols, type checks, bindings, or wildcards. Guards on arms are supported.
@while — bounded loop. The max_iterations is mandatory; the runtime raises WhileExceededError if you hit it without breaking.
@try_catch — error handling. try_body runs first; on a ScaiCoreRuntimeError, catch clauses are tried in order against the error's class name (or *).
External calls#
These cross a boundary — to another Core, or to async results gathered separately.
@core_call — invoke another Core via the host's CoreDispatcher. Sync by default; pass is_async = true to fire-and-forget (returning an AsyncFlowRef). Pair async calls with @await_responses to collect their results.
@await_responses — wait on a set of AsyncFlowRef values. A strategy (all, any, at_least(n), majority) controls when the await is satisfied. Timeouts and partial completion are first-class.
Lifecycle#
These don't compute a value — they shape how execution proceeds.
@checkpoint — pause execution and surface a request to a human (or a queue). When the host returns a resolution, the flow resumes with the response bound into scope. Supports presentation, options, on_response match arms, and (since v1.0.0) a hitl_target for native routing into ScaiQueue.
@budget — wraps a body in resource limits: max duration, LLM calls, plugin calls, memory writes, retries. On excess, either fail or warn.
@debug — a body of statements that compile away unless you pass --debug to scaicore compile. Use for inline logging during development.
When to pick which#
A short heuristic for the three you'll reach for most:
- Mostly deterministic work?
@rigid. Memory reads, plugin calls, arithmetic, branching, building output structures. - LLM call with a structured output?
@flexible. It enforces the shape and routes through your@llmconfig. - LLM call where the output must satisfy hard rules?
@guarded. Cheaper than re-prompting, more reliable than hoping.
Reach for control-flow blocks when sequencing doesn't capture what you mean. Reach for lifecycle blocks when you need to bound, suspend, or instrument execution — not as a first move.