Specification
Validation
Every stilt config is validated strictly before a single LLM call is made. Invalid configs fail fast with clear errors.
Validation
Every stilt config is validated strictly before a single LLM call is made. Invalid configs fail fast with clear errors.
How Validation Works
Validation happens in two phases. First, schema validation checks the raw structure — required fields present, correct types, no unexpected properties. This catches typos, missing keys, and malformed YAML. Second, semantic validation checks cross-reference integrity — every step id referenced in a field actually exists, every knob reference resolves, constraints like "only one recursion step" are enforced.
If a config passes validation, it runs without runtime surprises from structural issues. Every possible error is caught before execution starts.
Validation runs when a stilt is saved in Foundry, when the API is called with a stilt parameter, and when the programmatic parseConfig / validateConfig functions are used.
Validation Rules
Top-level rules. The config must have a name and an exit field. The exit value must match an existing step id. The allowedTargets strategy must be universal or constrained — if constrained, providers and models arrays must be non-empty and "*" cannot be mixed with explicit entries.
Step identity rules. Step ids must be unique — no two steps can share the same id. A non-sequential step can't ingest from itself with loopRef: "current" (only sequential steps can self-reference, since their nodes run in order). Steps inside a group can't ingest from each other with loopRef: "current" — they run simultaneously, so outputs aren't available yet.
Field reference rules. Every stepId in an ingest or multi_ingest source must exist in the config. Using loopRef: "accumulate" or nodeRef: "accumulate" on an ingest field is invalid — accumulate produces arrays and only multi_ingest can render them. A text field's from must be a string, not an object. nodeInfo fields must not have a from property. knobInfo.from must reference a knob that exists in the knobs section. skipFirstNode: true is only meaningful on ingest fields in a sequential context.
Knob rules. At most one knob of type loops. At most one knob of type recursion. Slider knobs must have 3 to 5 steps with exactly one marked as default: true. Numerical knobs must satisfy min <= default <= max.
Group rules. A group must contain at least 2 children. Groups cannot be nested inside other groups. A group cannot define fields, nodes, systemPrompt, continueIf, or recursion — groups are containers, not executors. Cloning fields from a group step is invalid.
Gate rules. Using pruned: true on nodes.from requires the source step to have a continueIf gate — there's nothing to prune against without one.
Recursion rules. At most one step can have a recursion block. The recursion block cannot go on a group step. If maxDepth is a string, it must reference an existing knob.
Timeline rules. At most one step can have timeline: "init". The init step cannot be the same step as the exit step. The exit step can have nodes only if it is a sequential step — normal steps with nodes cannot be the exit because all nodes run simultaneously and there is no deterministic "last" node.
Failure Examples
Recursion on a group. Groups are containers that hold sibling steps — they don't execute LLM calls themselves, so there's nothing to recurse on. Move the recursion block onto a normal or sequential step inside the group, or on a step outside the group.
Forward reference with loopRef: "current". Step A ingests from step B with loopRef: "current", but step B comes after step A in the steps list. Steps execute in order — when step A runs, step B hasn't produced output yet. Reorder the steps, or use loopRef: "previous" to read the output from the previous loop's run of step B.
Accumulate on an ingest field. Setting loopRef: "accumulate" on an ingest field. Accumulate collects outputs from all previous loops, producing an array. ingest resolves to a single value — it cannot render an array. Switch to multi_ingest if all previous outputs are needed, or use a specific loop index / previous if just one is needed.
Pruned without a gate. A step uses nodes.from with pruned: true, but the source step has no continueIf gate. Pruning means "count only the nodes that survived the gate." Without a gate, all nodes survive, making pruned meaningless. Add continueIf to the source step, or remove pruned: true.
Validation API
LukiScript ships programmatic validation functions for integrating config checks into external tooling.
parseConfig(raw) takes raw YAML or JSON, parses it into the typed config structure, and throws if the input is malformed or missing required fields.
validateConfig(config) takes a parsed config and returns a structured result: { valid: boolean, errors: string[] }. If valid is false, the errors array contains human-readable messages describing every rule violation.
Typical integration:
import { parseConfig, validateConfig } from '@redeo/lukiscript';
const parsed = parseConfig(rawConfig);
const result = validateConfig(parsed);
if (!result.valid) {
result.errors.forEach((error) => console.error(error));
}Validation errors include the field path, the violated rule, and a description of the fix.