Skip to content

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

  1. !set_default / !define? -- soft variable definitions
  2. !define -- hard variable definitions
  3. !import -- bulk name bindings from a module or vocab
  4. !each -- loop expansion
  5. !if -- conditional inclusion

After all instructions:

  1. !require -- checked for unsatisfied requirements
  2. !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.

!define base_url: "https://api.example.com"

endpoint: "${base_url}/users"

The value can be:

  • A scalar (string, number, bool)
  • An interpolation expression (${...}) -- evaluated at composition time
  • A mapping or sequence -- constructed immediately
  • A !fn callable -- stored as a CallableSymbol of kind 'template'
  • A !pipe pipeline -- stored as a CallableSymbol of kind 'pipe'
  • A tagged mapping (e.g. !MyModel) -- lazily constructed as LazyConstructable

Variable names must be valid Python identifiers.

!define:type

Coerce the value to a specific type after evaluation.

!define:int port: "8080"
!define:float rate: "0.5"

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.

!set_default env: "development"
# or equivalently:
!define? env: "development"

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:

!define mk: !fn
  !set_default:Gate gate: "default-gate"
  ok: 1

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.

!set_default:int workers:
  default: 4
  help: "worker count"
  short: -w

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.

!import dracon.tests.test_py_scheme_helper:

result: ${add(a=2, b=3)}
h: !Helper { n: 1, label: x }

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.

!import mypkg.vocab:

Selective form

A sequence of names imports only those — and surfaces a clear error listing the available names if any are missing.

!import mypkg.vocab: [CircuitPanel, MVPNetworkPanel, build_row]

Alias form

A {src: alias} mapping renames as it imports.

!import mypkg.vocab:
  CircuitPanel: BioCircuit
  NetworkDiagramPanel: NetDiagram

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.

!import file:vocab.yaml:

When to use what

  • !include py:mod@Name for one symbol.
  • !define X: !include py:mod@X for one symbol that needs an explicit !define ceremony (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.Class still 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.

!require api_key: "API key needed. Set via ++api_key=... or --api-key"

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):

!require port:
  help: "bind port"
  short: -p

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.

!define mk: !fn
  !returns:PlotData _:
  !fn :
    rows: 3

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.

!assert ${port > 0 and port < 65536}: "port must be between 1 and 65535"

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:

!if ${enable_debug}:
  log_level: DEBUG
  log_file: debug.log

Then/else form

!if ${env == 'production'}:
  then:
    log_level: WARNING
  else:
    log_level: DEBUG

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.

!each(name) ${['alice', 'bob', 'charlie']}:
  user_${name}:
    email: "${name}@example.com"

The variable (name in this example) is available in ${...} expressions inside the template.

Sequence value (generates list items)

items:
  !each(i) ${range(3)}:
    - "item_${i}"
# result: items: [item_0, item_1, item_2]

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

!define processor: !fn file:processor.yaml

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

!define double: !fn ${x * 2}

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.

!define my_loader: !fn:mymodule.load_data
  format: csv
  encoding: utf-8

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 **kwargs into the next stage
  • If a stage returns a non-mapping, it is passed as the single unfilled !require parameter 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:

db_host: !include file:config.yaml@database.host

Internal references

  • Absolute path: !include /some.path -- reference within the current document
  • Relative path: !include .sibling or !include ..parent.key
  • Anchor: !include anchor_name or !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.

overrides: !include? file:local-overrides.yaml

!deferred

Pause composition at this node. The subtree is wrapped in a DeferredNode and not processed further until explicitly composed/constructed at runtime.

template: !deferred
  greeting: "Hello, ${name}!"

Extended syntax

!deferred:Type

Specify the target type for construction:

model: !deferred:MyModel
  field1: value

!deferred::query_params

Query-string style parameters:

model: !deferred::clear_ctx=true:MyModel
  field1: value
  • clear_ctx=true -- clear the inherited context before constructing
  • reroot=true -- re-root the composition at this node

!deferred::query:Type

Combine query params and a type:

config: !deferred::reroot=true:ServerConfig
  host: localhost

!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.

theme: !live component:
  width: "${component.x * 2}"
  label: "${component.id}"
  static: "fixed"

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:

  • !deferred pauses a subtree for later construction
  • !live pauses 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}" }
cfg["style"].invoke(component=button)  # merged property dict for that component

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.

env:
  HUNT_KNOWN_BUGS: !raw "channels.messages('known_bugs')"
  SHELL_HOME: !raw "${HOME}/.config"

!raw is the scalar dual of !deferred:

  • !deferred pauses a subtree for later construction by Dracon
  • !raw marks 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.

!noconstruct raw_template:
  key: ${expr}  # this entire entry is removed from the constructed output

!unset

Mark a key for deletion during merge processing. Used to remove inherited keys.

<<: !include file:base.yaml
unwanted_key: !unset

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)