workflow.toml into version control and launch it with a single command:
fabro run and fabro create resolve and package the workflow locally,
register its immutable workflow version and dependencies, and then ask the
server to admit the intent and create a run from that version. Source parsing
or packaging failures stop locally before registration; full effective-intent
validation is authoritative at server admission. Use fabro preflight for
explicit local validation without creating a run. fabro create stops with
the run in the submitted state; fabro run performs the same create operation
and then starts the run separately.
The workflow can be selected by name from the current project or user workflow
storage, by a path in another local checkout, or as a loose local file. Its
source location does not choose the execution workspace: the directory where
you invoke Fabro remains the target source. Clone-based environments derive a
GitHub target from that caller directory, while a local environment receives
the canonical caller directory directly. Fetching workflow definitions from a
remote Git URL is not part of these commands.
Minimal example
A run config needs at minimum a schema version and a goal:run.toml
Goal precedence: CLI
--goal or --goal-file > [run].goal > Graphviz graph
attribute. A CLI --goal-file is read on the invoking machine and sent as a
per-run value; a goal file referenced by workflow.toml remains immutable
workflow content.
Full example
run.toml
Sections
[run.model]
Override the default model and provider for all nodes that don’t have an explicit model assigned via a stylesheet.
run.toml
Provider values are catalog provider ID strings. Built-in IDs like
anthropic and openai work, and settings-defined IDs like proxy work after they are added under [llm.providers.<id>].
For a qualified fallback, the selector may be that provider’s canonical model ID, alias, or API ID. Fabro splits on the first : when the part before it names a known provider, so provider API IDs may contain / or additional colons:
run.toml
kimi-k3. It tries direct Moonshot AI, then OpenRouter, then Claude Opus. The OpenRouter entry could equivalently be written as "openrouter:moonshotai/kimi-k3" using its API ID. Legacy provider/model fallback references remain accepted but are normalized to provider:model.
A colon alone does not make a reference qualified. Many model IDs contain one — ollama name:tag values, Bedrock inference-profile IDs and ARNs — so Fabro treats the reference as qualified only when the text before the first : names a known provider. "llama3:8b" stays a single model ID, while "ollama:llama3:8b" pins the ollama provider and passes llama3:8b as the selector.
At run creation, Fabro resolves the primary selector, every node selector, and the fallback table against the server’s ready-provider snapshot. It persists the selected canonical model slug and provider, so resuming the run does not choose a different provider just because credentials or priorities changed. fabro validate only checks the table’s TOML shape because it is offline and has no server model catalog. Use fabro preflight for catalog and provider checks.
Each original requested model selects one fixed chain. Fabro does not jump to the chain configured for a fallback target. When a target lacks the requested reasoning level, Fabro uses the nearest supported level. It rounds equal-distance choices up.
For example, a server that selects Modal as the primary kimi-k3 offering can define these independent chains:
settings.toml
claude-fable falls back to gpt-sol, Fabro continues with claude-opus from the Fable list. It does not restart from the separate gpt-sol list.
Historical built-in provider API IDs are accepted for compatibility and normalize before this selection. For example, name = "openai/gpt-5.6-sol" is treated as the canonical gpt-5.6-sol selector; omit provider to use readiness and priority, or set provider separately to pin an offering.
[run.model.controls]
Set default model controls for all nodes that do not override them in the workflow stylesheet:
run.toml
Fallback lists with splice
Use the reserved"..." marker in one model’s list to splice in that model’s inherited list from lower-precedence layers:
run.toml
[run.prepare]
Ordered list of steps to run before the workflow starts. Use this to clone repositories, install dependencies, or prepare the environment.
run.toml
Each step must exit with status 0. If any step fails, the run aborts before the workflow starts. Prepare steps replace across layers — the higher-precedence layer wins wholesale.
Fabro substitutes
{{ vars.* }} when the server creates the run, then resolves {{ secrets.* }} from token entries in the server vault immediately before the worker executes the steps. Secret expressions remain in the persisted run definition; resolved secret values are not persisted. A missing or non-token secret aborts startup with the affected step and token named in the error.
[run.clone]
Configure whether clone-based sandboxes clone the run’s GitHub origin before execution.
run.toml
enabled = false to start Docker and Daytona runs with an empty provider workspace. Use prepare steps to clone or create any files the workflow needs.
[run.run_branch]
Configure Fabro’s managed fabro/run/<id> checkpoint branch.
run.toml
[run.meta_branch]
Configure Fabro’s managed fabro/meta/<id> metadata branch.
run.toml
[run.environment] and server-managed environments
Runs select a reusable server-managed environment by slug. For fabro run and
fabro create, use --environment <slug> to select it; omitting the flag
selects default. Configure the catalog on the server rather than relying on
the CLI machine’s settings.toml or the source checkout’s
.fabro/project.toml, because those environments tables are not transmitted
during intent creation.
server settings.toml
[run.environment.*] and apply
to the selected server environment:
When
provider = "local", Fabro runs directly in the resolved working
directory. If you want local isolation, create or enter a separate clone or Git
worktree yourself.
Environment variable values can combine literal text with server variables and token secrets:
run.toml
Missing or non-token secret references fail closed before sandbox startup.
{{ env.* }} is not supported: the process environment is not a configuration source. Use {{ vars.NAME }} for a non-sensitive value or {{ secrets.NAME }} for a credential.
[run.integrations.github.permissions]
Request a scoped GitHub App token for workflow stages that need GITHUB_TOKEN inside the sandbox. Values map directly to GitHub App permission names and access levels.
run.toml
GITHUB_TOKEN if the app is not configured or is not installed on the repository.
This table follows the workflow settings merge rules. A higher-precedence
workflow or CLI override can set permissions = {} to clear inherited
permissions and run without a GitHub token.
[run.integrations.github].additional_repositories
Declare extra GitHub repositories, beyond the implicit run origin, that the minted GITHUB_TOKEN must cover. The one permissions map applies to the origin and every declared repository.
run.toml
owner/repository slug. Every repository in the effective set must share one owner and be reachable by the origin repository’s GitHub App installation. A non-empty list requires contents = "read" or contents = "write". Malformed slugs, case-insensitive duplicates, cross-owner sets, and sets larger than 499 entries fail configuration validation with indexed error paths such as run.integrations.github.additional_repositories[1].
Unlike permissions-only configuration, declared additional repositories are a hard requirement: missing credentials, a missing origin, or an inaccessible declared repository fails preflight and run initialization with the repository named.
The higher-precedence list replaces the lower one wholesale — no union and no ... splice — and additional_repositories = [] explicitly clears an inherited list. additional_repositories and permissions resolve independently; if layering leaves repositories declared while permissions were cleared, resolution reports the invalid combination instead of dropping either field.
See Additional repositories for what works inside stages (gh, GitHub API, plain Git over HTTPS and the common SSH spellings) and for the security boundary.
[run.notifications]
Define named notification routes for run events. Slack lifecycle notifications are configured here, not in server config.
run.toml
Each enabled Slack route posts once for each matching lifecycle event. Messages include the run ID, an Open in Fabro link when available, workflow label, terminal result, duration, and pull request details when those are already present in the run event stream.
run.failed is emitted only when the run terminally fails. A failed stage that routes onward to a normal completion path produces run.completed, not run.failed.
If a Slack route’s channel is missing, empty, or contains an unsupported interpolation token, Fabro logs a warning and skips that route. Delivery failures are logged and never fail or alter the run.
[run.checkpoint]
Configure how git checkpoint commits behave.
run.toml
exclude_globs replaces across layers — the higher-precedence layer wins wholesale. skip_git_hooks and commit_timeout use normal override semantics: the highest layer that sets the field wins.
[run.inputs]
Define inputs that are rendered into final workflow string attributes. See Variables for the full reference.
run.toml
goal, root model_stylesheet, and node prompt attributes with {{ inputs.name }} syntax:
c-i.fabro
import paths, @file paths, or child workflow paths, or any full-template attribute besides prompt, goal, and the root model_stylesheet. Command script supports only simple value substitution. Other attributes such as label are literal text.
If a workflow template references an undefined input like {{ inputs.langauge }}, fabro validate reports a warning. Run-style commands promote that diagnostic to an error before creating or starting a run.
TOML [run.inputs] tables replace wholesale across layers. Unlike labels, TOML input tables do not merge by key — the highest-precedence config layer that sets inputs wins its entire map.
CLI input flags are sparse overrides on top of the resolved config inputs:
-I / --input to override multiple keys. CLI input flags have the highest precedence, merge per key, and preserve unrelated inherited inputs. Duplicate CLI keys are accepted; the last value wins.
[run.artifacts]
Configure automatic collection of test artifacts (Playwright reports, JUnit XML, screenshots, etc.) from the execution environment after each stage.
run.toml
Artifact collection is opt-in — when no
[run.artifacts] section is present, no file scanning occurs.
Artifact globs use / as the separator and have the same semantics in every sandbox:
*and?match within one path segment.- Bracket expressions such as
[abc]and[!abc]match one character. **matches across directories when used as a complete segment.- Leading dots are matched normally.
- Patterns use
/, are relative to the sandbox working directory, and are case-sensitive. Backslashes are invalid. - Absolute patterns and patterns containing a
..segment are invalid.
.ai/reports/*.md matches direct Markdown children of .ai/reports, while .ai/reports/**/*.md also matches nested reports. *.trace.zip matches only the working-directory root; use **/*.trace.zip to match at any depth. To collect date-named implementation plans, use .ai/plans/????-??-??-*.md.
Each collection reflects the post-stage workspace state rather than filesystem modification timestamps. A path with unchanged content is recorded only once per run; if its content changes, Fabro captures the new version.
Fabro resolves the configured workspace root but does not recurse through symlinks below it while collecting artifacts. Dependency, cache, and build directories such as .git, node_modules, target, .venv, .cache, and dist are pruned. A collection is limited to 100 files, 10 MB per file, and 50 MB total.
[run.agent]
Configure workflow agent behavior that is not tied to a single stage.
run.toml
fabro_tools defaults to false. Set it to true only for runs whose agents should be able to use the same Fabro run-management MCP tool catalog exposed to human MCP clients: create, search, get, interact, gather, events, and pair.
One workflow-agent exception is intentional: fabro_run_create always creates child runs parented to the current run. If an agent supplies parent_id, it must match the current run ID.
This setting is independent of [run.agent.mcps], which configures external MCP servers available to the agent.
[run.agent.mcps]
Configure MCP servers available to agent stages during the workflow run. Each server is a named TOML table under [run.agent.mcps]. All three transport types are supported: stdio, http, and sandbox.
run.toml
run.toml
Inline transport commands, URLs, env values, and headers support
{{ vars.* }} and {{ secrets.* }} interpolation. As with prepare steps, server variables resolve at run creation and token secrets resolve at launch; missing values fail closed. See MCP runtime interpolation for the standalone fabro exec difference.
The sandbox transport runs the MCP server inside the workflow’s sandbox. This is useful for tools that need access to the sandbox environment, such as browser automation with Playwright. See MCP for details.
[run.pull_request]
Automatically open a GitHub pull request when the workflow run completes successfully. Requires a GitHub App to be configured and a clone-based Docker or Daytona environment; run creation rejects enabled = true on a Local environment.
run.toml
[[run.hooks]]
Define hooks that run in response to lifecycle events. Each hook is a TOML array entry:
run.toml
Hook merge semantics: hooks with matching
id values replace in place. Hooks without an id from a higher-precedence layer append after the fully merged inherited hook list.
See Hooks for hook types beyond scripts (HTTP, prompt, agent).
Graph path resolution
The[workflow].graph path is resolved relative to the TOML file’s parent directory, not the current working directory. This means a run config and its workflow can live side by side:
Precedence
For runs created byfabro run and fabro create, the CLI transmits sparse
flags and immutable workflow content, not machine or project run defaults.
Fabro resolves workflow behavior in this order (first match wins):
Stylesheet rules on individual nodes always take priority over run config values.
Project and machine settings
fabro run and fabro create do not transmit [run] or [environments]
from .fabro/project.toml or the CLI machine’s ~/.fabro/settings.toml.
When either key is present, the CLI warns with the affected file and key names,
but never includes the values in the warning or request. Move workflow-owned
behavior into each workflow.toml, and configure placement in server-managed
environments.
In particular, automatic pull-request behavior for CLI-created runs belongs in
the workflow:
workflow.toml
Validation
Fabro validates the run config when it loads:_versioncheck — Only_version = 1(or missing, which defaults to1) is accepted. The legacy top-levelversionkey is rejected with a rename hint.- Unknown keys — Any top-level key not in
[project],[workflow],[run],[cli],[server], or_versionis rejected with a targeted rename hint pointing at the v2 replacement path. - Variable check — Undefined workflow or prompt template variables produce diagnostics.
fabro validatereports them as warnings; run-style commands treat them as errors before creating or starting a run.
fabro preflight to validate a run config without executing it: