Instruction Tags¶
All instruction tags recognized during composition. Tags are processed on YAML mapping keys and removed from the final tree (unless noted otherwise).
Processing Order¶
!set_default/!define?-- soft variable definitions!define-- hard variable definitions!import-- bulk name bindings from a module or vocab!each-- loop expansion!if-- conditional inclusion
After all instructions:
!require-- checked for unsatisfied requirements!assert-- evaluated in a separate pass
!include, !deferred, !raw, !noconstruct, and !unset are handled by the composition pipeline directly, not by the instruction registry.
!define¶
Define a variable and add it to the context of sibling and descendant nodes. The definition node is removed from the tree.
The value can be:
- A scalar (string, number, bool)
- An interpolation expression (
${...}) -- evaluated at composition time - A mapping or sequence -- constructed immediately
- A
!fncallable -- stored as aCallableSymbolof kind'template' - A
!pipepipeline -- stored as aCallableSymbolof kind'pipe' - A tagged mapping (e.g.
!MyModel) -- lazily constructed asLazyConstructable
Variable names must be valid Python identifiers.
!define:type¶
Coerce the value to a specific type after evaluation.
Supported types: int, float, str, bool, list, dict.
!set_default / !define?¶
Soft definition. Sets the variable only if it does not already exist in the context. Values from !define or CLI ++ override soft defaults.
Also supports typed coercion for primitives: !set_default:int, !define?:float, etc.
For arbitrary type names (resolved through the active scope), the :Type
suffix records type metadata on the surrounding template's InterfaceSpec
without coercing the value:
The Gate annotation surfaces in mk.interface().params[0].annotation_name
(and as the resolved type in .annotation when Gate is in scope).
Mapping body (CLI metadata)¶
A top-level !set_default can carry CLI metadata via a mapping body. When the
config is loaded by a @dracon_program CLI, these directives surface as real
argparse flags — --name value, --help-visible, with optional short alias.
Allowed body keys for !set_default: default, help, short, hidden.
| key | type | meaning |
|---|---|---|
default |
any | the default value (coerced to :Type if typed) |
help |
str | help text shown in --help |
short |
str (-x) |
short alias (single char, must not collide with model) |
hidden |
bool | omit from --help |
The scalar form (!set_default workers: 4) is still valid: it is sugar for
{default: 4} with no CLI metadata. Inner-scope directives (inside !fn,
!deferred, or !if branches) are not exposed as CLI flags — they remain
contracts of the template they belong to.
See CLI flags from config layers
for the precedence rules and the ++ fallback.
!import¶
Bulk-bind names from a Python module or YAML vocab into the current document scope. Equivalent to many !defines collapsed into a single line.
Bare paths default to the py: scheme. Explicit schemes work the same as !include (py:, pkg:, file:).
Wildcard form¶
!import <ref>: (null body) imports the module's public surface — __all__ if defined, otherwise non-_-prefixed attributes.
Selective form¶
A sequence of names imports only those — and surfaces a clear error listing the available names if any are missing.
Alias form¶
A {src: alias} mapping renames as it imports.
Collision rule¶
!import is purely additive: a name already bound (by an earlier !define, !set_default, or another !import) wins. To force-rebind, use !define after the import.
Propagation through !include¶
Names imported in a vocab file flow through <<(<): like !defines do:
# vocab.yaml
!import mypkg.types: [Model, Optimizer]
# job.yaml
<<(<): !include file:vocab.yaml
m: !Model { ... }
YAML-vocab imports¶
For pkg: / file: sources, the imported document is composed in isolation; its top-level !define-bound names are pulled into the importing document.
When to use what¶
!include py:mod@Namefor one symbol.!define X: !include py:mod@Xfor one symbol that needs an explicit!defineceremony (e.g., for round-trip stability).!import mod: [a, b, c]for several symbols. Selective form is the recommended path — wildcard is for curated vocabulary packages.- Fully-qualified tags
!mod.path.Classstill work via the dynamic-import fallback.
!require¶
Declares that a variable must be provided by some outer scope (a !define, a !set_default, CLI ++, CLI --flag, or programmatic context). Checked after all instructions have run.
If the variable is not satisfied by end of composition, raises CompositionError with the hint message. Removed from the tree (pure validation).
Mapping body (CLI metadata)¶
A top-level !require can carry CLI metadata to surface as a real argparse
flag in @dracon_program CLIs. The same grammar as !set_default, minus
default (a required variable has no default by definition):
Allowed body keys for !require: help, short, hidden. Passing
default raises a CompositionError.
Typed !require:Type¶
Add a type-annotation suffix to record the parameter's type on the
surrounding template's InterfaceSpec:
!define MakePlot: !fn
!require:list[Event] events: "events to plot"
!require:Gate gate: "active gate"
!returns:PlotData _:
kind: derive
The annotation is metadata only — Dracon does not perform runtime type
checking beyond what construction or callable invocation already does.
Type is resolved through the active scope (just like a tag), so a class
made visible by !include vocab.yaml or by context_types=[...] becomes
the live annotation on the param. Strings that don't resolve stay in
annotation_name for documentation and JSON output. Untyped !require
remains valid.
!returns¶
Pure metadata marker for !fn and !deferred bodies that records the
return type on the symbol's InterfaceSpec. Removed from the final tree.
Two YAML-friendly forms are accepted: !returns:Type _: (type in the tag,
empty key) and !returns _: Type (type in the value). Both produce the
same return_annotation_name on the resulting InterfaceSpec.
!assert¶
Validate an invariant on the composed tree. Evaluated after all other instructions.
The key must be an interpolation expression that evaluates to a truthy/falsy value. If falsy, raises CompositionError with the message. Removed from the tree.
!if¶
Conditional inclusion. The key is an expression evaluated for truthiness.
Shorthand form¶
Include the content if the condition is true, otherwise remove the node entirely:
Then/else form¶
If the else key is absent and the condition is false, the node is removed.
The condition value can be a plain scalar (true/false/0/1) or an interpolation.
!each¶
Loop expansion. Duplicates the template body for each item in a list-like expression.
The variable (name in this example) is available in ${...} expressions inside the template.
Sequence value (generates list items)¶
When all keys in a mapping are !each instructions with sequence values, they are expanded and spliced into the parent sequence.
Key expression¶
The key must be an interpolation expression that evaluates to an iterable. Plain scalars are not valid.
Nested instructions¶
!each can contain other instructions (!define, !if, etc.) in its body. They are expanded per-iteration.
!fn¶
Define a callable YAML template. Three forms:
File reference¶
Loads the file as a template. Each call deepcopies the template, injects kwargs as context, and runs composition + construction.
Inline mapping¶
!define greeting: !fn
!require name: "name is required"
!set_default greeting: "Hello"
message: "${greeting}, ${name}!"
The !fn tag goes on the value node, not as a separate key. The mapping body becomes the template. Use !require for mandatory parameters and !set_default for optional ones.
Expression lambda¶
The value is an interpolation expression. Parameters come from the caller's kwargs.
Return marker (!fn :)¶
Inside a mapping template body, tagging a key with !fn marks it as the return value. The callable returns only that value instead of the whole mapping.
!define compute: !fn
!require x: "input"
intermediate: ${x * 2}
!fn : ${intermediate + 1}
# compute(x=5) returns 11, not the whole mapping
Invocation¶
Callables defined by !fn are invoked via ${fn_name(key=value)} in expressions or programmatically as regular Python callables.
!fn:path (Partial Application)¶
Creates a CallableSymbol of kind 'partial' -- a partial application of a Python callable with pre-filled kwargs.
The dotted path is resolved as a Python import. The mapping body provides default kwargs. At call time, runtime kwargs override the defaults.
If the path has no dots, it is looked up in the current context instead of imported.
!pipe¶
Function composition. Chains a sequence of callables where each stage's output feeds into the next.
!define pipeline: !pipe
- load_data
- clean_data:
remove_nulls: true
- ${custom_transform}
- !fn:mymodule.save
path: output.csv
Stage types¶
- Bare name: resolved from context
- Name with kwargs:
name: {kwargs}-- pre-fills kwargs for that stage - Interpolation:
${expr}-- resolved at definition time - Tagged node:
!fn:path {kwargs}-- constructed via the loader
Value threading¶
- If a stage returns a mapping, it is unpacked as
**kwargsinto the next stage - If a stage returns a non-mapping, it is passed as the single unfilled
!requireparameter of the next stage
Nested pipe instances are flattened automatically.
!include¶
Include content from an external source. See Include Schemes for the full list of schemes.
database: !include file:db.yaml
settings: !include pkg:mypackage:defaults.yaml
api_key: !include env:API_KEY
Selector¶
Append @keypath to extract a subtree:
Internal references¶
- Absolute path:
!include /some.path-- reference within the current document - Relative path:
!include .siblingor!include ..parent.key - Anchor:
!include anchor_nameor!include anchor_name.sub.key
!include?¶
Optional include. If the source is not found (e.g. missing file), the node is silently removed instead of raising FileNotFoundError.
!deferred¶
Pause composition at this node. The subtree is wrapped in a DeferredNode and not processed further until explicitly composed/constructed at runtime.
Extended syntax¶
!deferred:Type¶
Specify the target type for construction:
!deferred::query_params¶
Query-string style parameters:
clear_ctx=true-- clear the inherited context before constructingreroot=true-- re-root the composition at this node
!deferred::query:Type¶
Combine query params and a type:
!live¶
Late-bind specific ${...} leaves against per-call runtime context without wrapping the whole subtree in !deferred. !live shifts the pause-unit from "subtree" to "variable": only ${...} leaves whose free names intersect the declared scope stay lazy, the rest resolves normally at load time.
Multiple names can be declared with commas: !live component, theme:.
import dracon
cfg = dracon.load("theme.yaml")
cfg["theme"]["width"] # LazyInterpolable, not resolved
cfg["theme"]["width"].invoke(component=c) # resolves with this binding
dracon.resolve_all_lazy(cfg, except_for={"component"}) # resolve everything *but* live lazies
The lazy leaves carry their matched scope params on _scope_params. resolve_all_lazy skips any lazy whose scope params are non-empty (unless their names are in except_for). dump re-emits !live groups by inspecting _scope_params, so loads(dump(cfg)) is a fixed point.
!live is the variable-axis dual of !deferred:
!deferredpauses a subtree for later construction!livepauses specific leaves so the surrounding structure stays usable
!cascade:NAME¶
Predicate-keyed mapping dialects. One tag, two flavors discriminated by the registered CascadeStrategy's input_params:
Inherit-mode (empty input_params)¶
Keys with shared semantics flow into descendants at compose time. Built-in strip_suffix(SUFFIX) is the most common case:
!cascade:strip_suffix:params smooth:
smooth_params: { sigma: 1.0 }
smooth_2d:
smooth_2d_params: { sigma: 2.0 } # merges with smooth_params via the strip_suffix dialect
strip_suffix:SUFFIX is a parametric built-in: the dialect collapses <name>_<SUFFIX> and <name> onto the same target. Compose-time result is a plain dict — no runtime dispatch.
For inherit-mode without a YAML tag, use dracon.cascade_inherit(tree, key_normalize=...) directly.
Select-mode (non-empty input_params)¶
Predicate keys dispatch on a runtime value. Compose-time output is a CallableSymbol of kind 'match'; invoke(**input_params) collects matching keys, sorts by specificity, and merges. Each input_params name implicitly opens a !live scope inside the body, so ${name.x} leaves stay callable against the per-invocation binding.
from dracon import CascadeStrategy, register_cascade_strategy
register_cascade_strategy(CascadeStrategy(
name="css-like",
input_params=("component",),
parse=parse_selector,
matches=lambda sel, c: sel.matches(c),
specificity=lambda sel: sel.specificity,
))
style: !cascade:css-like
"button": { color: blue }
"button.primary": { color: "${component.theme.accent}" }
Strategy fields: name, input_params, apply (inherit-mode), parse/matches/specificity/merge (select-mode), recursive. Register parametric builders via the same path used internally by strip_suffix; resolve_cascade_strategy(name, arg) looks up NAME or NAME:ARG.
!raw¶
Mark a scalar value as opaque to all Dracon phases. The string is carried through composition, construction, and lazy resolution without any interpretation. Downstream systems (runtimes, template engines, shells) can evaluate the contents however they like.
!raw is the scalar dual of !deferred:
!deferredpauses a subtree for later construction by Dracon!rawmarks a scalar that Dracon will never evaluate
The phase boundary is on the value, not the template. A !raw value flows through !fn invocations untouched:
!define make_job: !fn
!require cmd: "command expression"
!fn :
run: ${cmd}
job: !make_job
cmd: !raw "runtime.dispatch('task')"
# job.run is a RawExpression, not an interpolated string
When to use !raw vs escaping¶
Use !raw when a value is meant for a different evaluator entirely. Use $${} escaping when you just need a literal ${...} in the output. The key difference: !raw survives any number of !fn nesting levels without counting escape layers.
Python type¶
RawExpression is a str subclass. It works anywhere a string does and round-trips through dump/loads preserving the !raw tag.
from dracon import RawExpression
expr = RawExpression("channels.messages('bugs')")
isinstance(expr, str) # True
In Pydantic models, type the field as RawExpression | str to accept both regular strings and raw expressions.
!noconstruct¶
When used as a tag on a mapping key, !noconstruct causes the entire key-value pair to be skipped during construction. The pair does not appear in the constructed output at all.
!unset¶
Mark a key for deletion during merge processing. Used to remove inherited keys.
After merges are processed, any key with !unset as its value is deleted from the parent mapping.
Custom Instructions¶
Register your own instruction tags:
from dracon import register_instruction, Instruction
class MyInstruction(Instruction):
@staticmethod
def match(value):
if value == '!mytag':
return MyInstruction()
return None
def process(self, comp_res, path, loader):
# modify comp_res and return it
return comp_res
register_instruction('!mytag', MyInstruction)