mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-26 01:11:21 +00:00
## Summary
`fabro validate` had inconsistent behavior for undefined template
variables depending on whether the prompt was inline or loaded via an
`@file` reference. Inline `{{ inputs.foo }}` produced a warning and
validation passed; the same expression inside a `@file`-imported prompt
produced a hard validation error.
Fixes #286.
## Root cause
Two template-rendering passes with different strictness, applied to
disjoint inputs:
1. **DOT-source pass**
(`lib/crates/fabro-workflow/src/operations/create.rs`) honored
`RenderMode::Structural` for `fabro validate` — undefined variables
downgraded to a `Severity::Warning` diagnostic, then lenient render
finished the job.
2. **Per-attribute pass**
(`lib/crates/fabro-workflow/src/transforms/variable_expansion.rs`)
inside `TemplateTransform` was always strict and had no `RenderMode`
awareness. Because `FileInliningTransform` runs *before*
`TemplateTransform`, expressions inside `@file` content only ever
encountered the strict pass.
## Fix
- Plumb `RenderMode` through `TransformOptions` into
`TemplateTransform`.
- In `RenderMode::Structural`, the transform catches
`TemplateError::UndefinedVariable` per attribute, emits a warning
diagnostic, and falls back to `render_lenient`.
- Diagnostics flow through a new `Transformed.diagnostics` field into
`Validated` alongside lint output.
- Diagnostics now include `node_id` when the undefined variable was
found inside a node attribute, which is more useful than the previous
"at line 1" location.
- `RenderMode` and the shared `template_undefined_variable_diagnostic`
helper moved to `pipeline/types.rs` so the transform layer can reach
them without a circular dep.
Strict mode (`fabro run`, preflight) is unchanged — undefined inputs
still hard-fail before a run is created.
## Behavior
Illustrative output shapes (variable names and line numbers depend on
the fixture):
Inline prompt (unchanged):
```
warning: undefined template variable `inputs.<name>` at line <n> (template_undefined_variable)
Validation: OK
```
`@file`-imported prompt (previously a hard error, now matches inline —
node-attributed instead of line-attributed):
```
warning [node: <id>]: undefined template variable `inputs.<name>` in node `<id>` (template_undefined_variable)
Validation: OK
```
## Test plan
- [x] `cargo nextest run --workspace` — 5773/5773 passing
- [x] `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings` clean
- [x] `cargo +nightly-2026-04-14 fmt --check --all` clean
- [x] New regression test
`bare_fabro_with_unbound_inputs_in_imported_prompt_validates_structurally_with_warning`
in `lib/crates/fabro-cli/tests/it/cmd/validate.rs` against new fixture
`test/templated_unbound_imported/`
- [x] Existing
`bare_fabro_with_unbound_inputs_validates_structurally_with_warning` and
`strict_render_hard_fails_on_unbound_inputs` still pass — verifies
inline structural and run-start strict behavior are both preserved
- [x] Manual reproduction of the exact inputs from the issue now
succeeds with a warning
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Aleksi Asikainen <1086393+salieri@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
113 lines
4 KiB
Text
113 lines
4 KiB
Text
---
|
|
title: "Variables"
|
|
description: "Using templates in workflows"
|
|
---
|
|
|
|
Fabro uses `{{ ... }}` templates for workflow strings and prompts.
|
|
|
|
## Template context
|
|
|
|
Workflow and prompt templates can reference:
|
|
|
|
| Expression | Resolves to |
|
|
|---|---|
|
|
| `{{ goal }}` | The workflow goal |
|
|
| `{{ inputs.name }}` | A value from `[run.inputs]`, optionally overridden by CLI input flags |
|
|
|
|
Environment variables are **not** available in workflow or prompt templates. Use `{{ env.NAME }}` only in config strings and HTTP hook headers.
|
|
|
|
## Run config inputs
|
|
|
|
Define typed inputs in `[run.inputs]`:
|
|
|
|
```toml title="run.toml"
|
|
_version = 1
|
|
|
|
[workflow]
|
|
graph = "check.fabro"
|
|
|
|
[run]
|
|
goal = "Run repository checks"
|
|
|
|
[run.inputs]
|
|
repo_name = "fabro"
|
|
repo_url = "https://github.com/fabro-sh/fabro"
|
|
language = "rust"
|
|
```
|
|
|
|
These values are available in final string attributes such as `goal`, `prompt`, `script`, `label`, and edge labels:
|
|
|
|
```dot title="check.fabro"
|
|
digraph Check {
|
|
graph [goal="Run tests for {{ inputs.repo_name }}"]
|
|
|
|
start [shape=Mdiamond, label="Start"]
|
|
exit [shape=Msquare, label="Exit"]
|
|
|
|
clone [label="Clone", shape=parallelogram, script="git clone {{ inputs.repo_url }} repo"]
|
|
test [label="Test", prompt="Run the {{ inputs.language }} test suite in the repo/ directory."]
|
|
|
|
start -> clone -> test -> exit
|
|
}
|
|
```
|
|
|
|
Override individual inputs at run time with repeatable `-I` / `--input` flags:
|
|
|
|
```bash
|
|
fabro run .fabro/workflows/check/workflow.toml -I repo_name=fabro-2 --input language=rust
|
|
```
|
|
|
|
CLI input values use TOML scalar parsing when possible. Quoted strings, booleans, integers, and floats keep their typed values; unquoted bare text falls back to a string. Empty values such as `foo=` are accepted as empty strings. Arrays, inline tables, and datetimes are rejected.
|
|
|
|
## `goal`
|
|
|
|
Agent and prompt nodes also receive the workflow goal at runtime:
|
|
|
|
```dot title="example.fabro"
|
|
digraph Example {
|
|
graph [goal="Implement the login feature"]
|
|
|
|
plan [label="Plan", prompt="Create a plan for: {{ goal }}"]
|
|
}
|
|
```
|
|
|
|
That prompt becomes `Create a plan for: Implement the login feature`.
|
|
|
|
## Expansion timing
|
|
|
|
Fabro keeps workflow structure static and renders workflow templates once:
|
|
|
|
1. Fabro parses the root DOT and imported `.fabro` files without rendering them.
|
|
2. Literal `import`, `@file`, graph-goal file, and child-workflow references are resolved.
|
|
3. Final graph, node, and edge string attributes are rendered with the `{ goal, inputs }` context.
|
|
|
|
Templates are not supported in graph syntax, node IDs, edge structure, `import` paths, `@file` paths, child workflow paths, or other file references.
|
|
|
|
Fabro renders the graph `goal` first and stores the rendered value back onto the graph. Other attributes that use `{{ goal }}` receive that rendered value.
|
|
|
|
## Undefined variables
|
|
|
|
Fabro renders undefined workflow variables as empty text and records a `template_undefined_variable` diagnostic. `fabro validate` reports that diagnostic as a warning so you can validate workflow structure before all inputs are known. Run-style commands such as `fabro run`, `fabro create`, and preflight promote the same diagnostic to an error before proceeding.
|
|
|
|
## Escaping
|
|
|
|
To emit literal template syntax, use MiniJinja escaping:
|
|
|
|
```dot
|
|
test [prompt="{% raw %}{{ goal }}{% endraw %}"]
|
|
```
|
|
|
|
You can also emit literal braces with expressions such as `{{ '{{' }}` when needed.
|
|
|
|
## Input merging
|
|
|
|
TOML `[run.inputs]` tables intentionally replace the inherited map wholesale rather than merging by key. Whichever TOML layer has the highest precedence and sets `[run.inputs]` wins its entire map.
|
|
|
|
CLI input flags are different: they are sparse per-key overrides applied after config resolution, so unrelated inherited inputs remain available. If a key is repeated on the CLI, the last value wins.
|
|
|
|
| Source | Priority |
|
|
|---|---|
|
|
| CLI flags (`-I key=value` / `--input key=value`, repeated; per-key merge) | Highest |
|
|
| `workflow.toml` `[run.inputs]` | |
|
|
| `.fabro/project.toml` `[run.inputs]` | |
|
|
| `~/.fabro/settings.toml` `[run.inputs]` | Lowest |
|