fabro/docs/public/workflows/variables.mdx
Bryan Helmkamp f07bb4aaba
feat(cli): support sparse input overrides (#222)
## Summary
- Add repeatable `-I` / `--input KEY=VALUE` CLI overrides for workflow
run inputs on `fabro run`, `fabro create`, and `fabro preflight`. CLI
inputs are sparse per-key overrides that merge over the resolved config
inputs (preserving unrelated inherited values), unlike TOML
`[run.inputs]` which still replaces wholesale.
- Manifest bundling and graph-level goal resolution render workflow
source with the effective inputs before structural scanning, so
input-driven `@prompt`, `import`, and `stack.child_workflow` paths get
bundled correctly.
- Persist raw `KEY=VALUE` strings on `ManifestArgs.input` so server-side
replay applies the same sparse overrides on top of merged config.
- Review-driven cleanups: shared `TemplateContext::for_input_scan`
helper for the recurring "render inputs but defer goal" idiom (replaces
4 sites), `#[derive(Default)]` on `ManifestBuildInput` to drop
boilerplate, inline trivial `apply_input_overrides` wrapper, drop a
redundant clone, and tighten the parser/test helpers.

## Test plan
- [ ] `cargo nextest run -p fabro-cli -p fabro-config -p fabro-server -p
fabro-template -p fabro-workflow`
- [ ] `cargo +nightly-2026-04-14 fmt --check --all`
- [ ] `cargo +nightly-2026-04-14 clippy -p fabro-cli -p fabro-config -p
fabro-server -p fabro-template -p fabro-workflow --all-targets -- -D
warnings`
- [ ] Smoke: `fabro run <workflow> -I key=value --input other=42`
overrides those keys while preserving unrelated inherited inputs
- [ ] Smoke: `-I` accepts strings, integers, floats, booleans, empty
values; rejects arrays, inline tables, datetimes; rejects missing `=`
and empty key
- [ ] Smoke: input-driven `@prompts/{{ inputs.foo }}` and
`stack.child_workflow="{{ inputs.bar }}/workflow.fabro"` paths bundle
correctly when overridden via `-I`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:00:35 -04:00

111 lines
3.7 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 throughout the workflow as `{{ inputs.* }}`:
```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 expands templates in multiple passes:
1. Before DOT parsing, `{{ inputs.* }}` can parameterize structural parts of the graph, including imported `.fabro` files.
2. After parsing, all string graph, node, and edge attributes are rendered again with the real `{ goal, inputs }` context.
3. Agent and prompt handlers do a final runtime render pass as a safety net.
`{{ goal }}` is preserved through the pre-parse step so it can be resolved later. That means goal-dependent MiniJinja control flow such as `{% if goal %}` is not useful in structural pre-parse templates.
## Undefined variables
Fabro uses strict undefined-variable handling. If a workflow template references an unknown value such as `{{ inputs.langauge }}`, validation fails instead of passing the literal text through to the model.
## 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 |