Output Engine
Outputs are generated in the fourth pipeline phase, after assets, models, and compliance have been applied. Each output file (output/*.toml or output/*.jsonnet) loops over an origin_resource type and renders one artifact resource per matching node. Omit origin_resource for a singleton output that executes exactly once with an empty context (e.g. a static application config map or an ACME directory document); templates then render without origin_resource variables.
Artifact bodies can be rendered with text-based Tera templates or native Jsonnet expressions (jsonnet = '''...'''). Jsonnet is particularly powerful for complex, nested data structures (such as Kubernetes manifests or Terraform configurations) because it constructs JSON directly with variables, conditionals, list comprehensions, and sibling .libsonnet imports rather than text interpolation.
An output rule produces a new graph resource (linked to the origin via DESCRIBED_BY) and stores the rendered content. Resource names and filenames can be templated with the full traversable origin_resource context. Origin-less outputs have no DESCRIBED_BY edge.
Strict Validation: All
[[output]]blocks enforce strict key validation. Misspelled, unsupported, or misplaced keys (such astarget_idorcreate_relation) produce a parse error rather than failing silently. Supported keys are:resource_type,name,filename,mimetype,relation,match_on,depends_on,template, andjsonnet.
Filenames may be simple names (e.g. report.txt) or relative paths (e.g. .agents/skills/{{ origin_resource.name }}/SKILL.md). Absolute paths and paths containing .. are rejected when artifacts are exported to disk.
Dependency ordering with depends_on
An output rule can declare that its artifact depends on another output artifact. After all output rules stabilize, rescile resolves each declaration into a DEPENDS_ON edge from the dependent output to the dependency output. Tools that export artifacts (such as rescile-ce save --outputs or rescile-ee serve --write-outputs) use these edges to write dependencies before dependents, and to detect dependency cycles.
[[output]]
resource_type = "agent_skill"
name = "skill-{{ origin_resource.name }}"
filename = ".agents/skills/{{ origin_resource.name }}/SKILL.md"
depends_on = [
{ resource_type = "developer_source_artifact", name = "as-{{ origin_resource.name }}" }
]
template = "..."
Both resource_type and name are rendered with the same Tera context as the output itself, so they can reference origin_resource and relation. If the named dependency does not exist, a warning is emitted and the edge is skipped.
Relation-bound outputs
An output file (or a single output rule) can be bound to a relation. When relation is set at file level, it applies to every [[output]] rule in the file unless a rule sets its own relation.
What relation means here: it is a filter and expansion over edges that already exist in the graph. It does not create or declare a relation between the output artifact and anything else — every output artifact is automatically linked to its origin resource via a DESCRIBED_BY edge, regardless of relation. You bind a rule to a relation only when the artifact content itself depends on data stored on the edge or on the specific node at the other end of the edge.
How a rule with relation renders: for each origin node, rescile collects the outgoing edges (the origin node must be the start of the edge) whose label matches the configured relation. The rule then expands once per matching edge, and the current edge is exposed as a top-level relation variable. A node with three matching outgoing edges produces three artifacts; a node with none produces none.
origin_resource = "compute"
relation = "DEPLOYED_AS"
[[output]]
resource_type = "deployment_target"
name = "target-{{ origin_resource.name }}-{{ relation.target.properties.name }}"
filename = "{{ origin_resource.name }}-{{ relation.target.properties.name }}.json"
template = '''
{
"instance_type": "{{ relation.properties.instance_type }}",
"encrypted": {{ relation.properties.encrypted | default(value=false) }},
"provider": "{{ relation.target.properties.name }}"
}
'''
Available fields:
relation.label— the relation label, e.g.DEPLOYED_AS.relation.properties.<name>— any property stored on the edge.relation.target.labelandrelation.target.properties.<name>— the node on the other end of the relation.
The relation variable is also available when rendering name, filename, and depends_on values. (mimetype is stored verbatim and cannot be templated.)
When relation is omitted, the rule renders a single artifact per matching origin node, regardless of any edges.
Silent-skip pitfall
If an origin node has no outgoing edge with the configured label, it is skipped silently — no warning, no artifact. The phase log then shows the file as processed with nothing applied:
▶ Phase 4: Applying Output Definitions
→ Processing Output Iteration 1/8: +0 resources, +0 defined relations, +0 properties set
✓ Phase 4: Outputs Applied (1 files, +0 resources, ...)
This is the most common reason an output rule “does nothing”. Check the edge direction: only edges leaving the origin node count. If your graph models the relation in the other direction (something --DEPLOYED_AS--> origin), the rule never matches.
Two rules of thumb:
- If your template never reads
relation.*, you almost certainly do not want therelationbinding — delete it and render once per origin node. - If you do bind to a relation, the matching edges must be created before Phase 4 by a model rule (
link_resources), an asset convention, or compliance.
To see which origin nodes a rule actually evaluates, run with debug selectors for the output file: --debug-build=my-output prints Evaluating origin_resource '<type>:<name>' per candidate node.
relation vs match_on
Both keys filter over existing graph state and both skip non-matching nodes silently — but they do different jobs:
match_on |
relation |
|
|---|---|---|
| Question asked | Does this origin node qualify? | Which outgoing <LABEL> edges does it have? |
| Effect on rendering | Gate: one artifact per node when it passes, none when it fails | Loop: one artifact per matching edge (0 edges → 0 artifacts) |
| Added template data | None | The relation variable: relation.properties.* (edge data) and relation.target.* (the node at the other end) |
match_on cannot express per-edge rendering: with three DEPLOYED_AS edges, a match_on-only rule still renders exactly one artifact for the node. relation renders three, each with its own edge and target. Conversely, relation is a poor substitute for match_on — it has no value comparison, it only checks for edge existence by label.
The two compose: match_on first filters the candidate nodes, then relation expands each surviving node once per matching edge. Mental model: origin_resource is the loop over node types, match_on is a WHERE clause on the node, relation is an inner for each edge loop that also hands you the edge.
Example: what only relation can do
A compute node is deployed in two regions, and each deployment edge carries its own edge data. The rule renders one artifact per deployment — edge properties and target node included:
origin_resource = "compute"
relation = "DEPLOYED_AS"
[[output]]
resource_type = "deployment_target"
name = "target-{{ origin_resource.name }}-{{ relation.target.properties.name }}"
filename = "{{ origin_resource.name }}/{{ relation.target.properties.name }}.json"
template = '''
{
"host": "{{ origin_resource.name }}",
"provider": "{{ relation.target.properties.name }}",
"instance_type": "{{ relation.properties.instance_type }}"
}
'''
Given one compute node web-1 with two outgoing DEPLOYED_AS edges:
web-1 --DEPLOYED_AS--> aws-eu, edge propertyinstance_type = "t3.medium"web-1 --DEPLOYED_AS--> exoscale-ch, edge propertyinstance_type = "standard.small"
this produces two artifacts:
deployment_targettarget-web-1-aws-eu→{ "host": "web-1", "provider": "aws-eu", "instance_type": "t3.medium" }deployment_targettarget-web-1-exoscale-ch→{ "host": "web-1", "provider": "exoscale-ch", "instance_type": "standard.small" }
match_on alone cannot produce this. Even a relation-aware match_on condition such as
{ relation = "DEPLOYED_AS", property = "instance_type", value = "t3.medium" } only answers “does this node have any edge whose instance_type is t3.medium?” — it is a gate, not a loop:
- it renders at most one artifact for
web-1, not one per deployment; - the template has no
relationvariable, so it cannot read which edge matched, the edge’s own properties, or the target of that specific edge —{{ origin_resource.DEPLOYED_AS }}is an array of target nodes and does not carry the edge instance either.
Use match_on when the question is which nodes qualify; use relation when the question is what exists per edge.
Scoping precedence
A file-level relation applies to every [[output]] rule in the file. If a rule sets its own relation, the rule-level value wins. If neither is set, the rule renders one artifact per matching origin node as usual.
origin_resource = "compute"
relation = "DEPLOYED_AS" # default for every rule below
[[output]]
resource_type = "node_manifest" # uses DEPLOYED_AS
name = "node-{{ origin_resource.name }}"
...
[[output]]
relation = "BACKED_UP_TO" # overrides the file-level default
resource_type = "backup_manifest"
...