zoryn flow¶
zoryn flow runs a flow — a user-defined sequence of steps that turns a routine package update into a single command. A flow can update and build the package, run your own checks over ssh or inside a devenv, submit the result to gyle, and — when a step fails — run a recovery flow (up to and including an AI agent that reads the build log) and retry.
Nothing in a flow ever prompts: it is meant to run from cron, a CI job or a terminal you are not watching.
Experimental
zoryn flow is experimental. Step types, config keys and state format may change in future releases.
Synopsis¶
zoryn flow [NAME] [--continue] [--restart] [--abort] [--dry-run] [-C DIR] [-d] [-v]
zoryn flow run [NAME] [--continue] [--restart] [--abort] [--dry-run] [-C DIR] [-d] [-v]
zoryn flow list [-C DIR]
zoryn flow show NAME [-C DIR]
zoryn flow NAME and zoryn flow run NAME are the same thing — the plain form is rewritten to run before cmdliner sees it. Only a flow literally named run, list, show or help needs the explicit zoryn flow run <name> form.
Run the command from inside a gear repository; zoryn switches to the repository root first, so config layers, .gear/version-up, {pkg} and the state file all refer to the same package regardless of the subdirectory you started in.
zoryn flow run¶
Runs the flow. Without NAME the flow is chosen by [flow] name in .gear/version-up, and failing that by default in the [flows] table of the machine config; with --continue and no NAME the saved state decides.
Options¶
NAME— flow to run. Without it the flow comes from[flow] namein.gear/version-up, else from[flows] default.--continue— resume the interrupted flow from the step that failed.--restart— discard the saved state and run the flow from its first step.--abort— discard the saved state of an interrupted flow.--dry-run— print the resolved steps and exit without running anything.-C DIR— change to directoryDIRbefore running.-d,--debug— enable debug output (also traces the commands zoryn spawns).-v,--verbose— show build output on screen forbuildsteps, aszoryn build -vdoes (the initial build inside anupstep always goes to its log file).
--continue, --restart and --abort are mutually exclusive, and --continue cannot be combined with --dry-run.
zoryn flow # run the selected flow
zoryn flow update # run the flow named 'update'
zoryn flow update --dry-run # print the resolved steps only
zoryn flow --continue # resume after fixing a failed step
zoryn flow --abort # discard the saved state
zoryn flow list¶
Lists the flows visible from the current package, one per line, each followed by the config file that defines it. Where the same name is defined in two layers, only the winning definition is listed.
zoryn flow show¶
Prints the steps of NAME in the same numbered form --dry-run uses, with each step's if, on-failure and non-default max-retries below it. Variables produced by later steps are not known yet, so a line still containing {new_version} or {task} is printed verbatim.
$ zoryn flow show update
Flow update
1. up
on-failure: fix-build
max-retries: 3
2. devenv: make smoke
3. ssh stand1: run-checks {pkg} {new_version}
4. submit -B sisyphus --run
Where flows are defined¶
Flows live in the machine-local configuration only, in two layers:
~/.config/zoryn/projects.d/<project>.toml— per package, wins;~/.zoryn— global.
A package repository can not define steps. Steps carry shell commands and ssh hosts, so a cloned repository must never be able to inject them — the most a repository may do is select a flow by name:
If the selected name is not defined in your own config, the run fails with unknown flow '<name>'; nothing from the repository is executed.
Configuration format¶
Each flow is a [flows.<name>] table with a steps array of inline tables. The [flows] table itself holds only default, the flow used when nothing else selects one.
[flows]
default = "update"
[flows.update]
steps = [
{ run = "up", on-failure = "fix-build", max-retries = 3 },
{ run = "devenv", cmd = "make smoke" },
{ run = "ssh", host = "stand1", cmd = "run-checks {pkg} {new_version}" },
{ run = "submit", branch = "sisyphus", task-run = true },
]
[flows.fix-build]
steps = [ { run = "agent" } ]
[flows.batch-update]
steps = [
{ run = "batch", config = "qt6.toml" },
{ run = "bash", cmd = "notify-send 'batch {pkg} done'" },
]
# multi-repo submit: one submit creates a task per branch; {tasks} addresses them all
[flows.kernel]
steps = [
{ run = "submit", branch = "sisyphus,p11", task-run = false },
{ run = "zoryn", args = ["task", "add", "{tasks}", "rebuild", "--dependent-on", "kernel-image-for-vm"] },
{ run = "zoryn", args = ["task", "approve", "{tasks}", "all", "-m", "sign"] },
{ run = "zoryn", args = ["task", "run", "{tasks}", "--commit"] },
]
Step types¶
run | Keys | What the step does |
|---|---|---|
up | build options + no-build | the full zoryn up update pipeline (detect … up-hooks) followed by a build of the new version — or, with no-build = true, the update alone. Publishes {old_version} and {new_version} |
build | build options | a zoryn build |
submit | submit options | zoryn submit. The commit message is never opened in an editor, and a tag conflict cancels the step instead of asking. Publishes {task} |
task-add | task (default {task}), args, dependent-on | zoryn task add: adds a subtask to task — args is the action tail (packages, rebuild, a branch), dependent-on maps to --dependent-on. All values are templated, so the default acts on the task the submit step created |
task-approve | task (default {task}), subtask (default all), message | zoryn task approve: approves subtask (a number, package, pkg.git=tag or all) of task, with message as the -m comment |
task-run | task (default {task}), commit, message | zoryn task run: starts the build of task; commit = true adds --commit, message the -m comment |
batch | config (required) | zoryn task batch <config> --no-edit-commit; when the previous attempt left batch state behind, the retry adds --continue so it resumes that batch instead of being refused. The child never asks anything: its stdin is /dev/null, so the resume takes the default answer (do not retry the packages that failed inside the batch) |
bash | cmd (required) | runs cmd through the system shell (/bin/sh -c) in the package directory |
ssh | host, cmd (both required) | runs cmd on host over the configured {ssh} command; both arguments are shell-quoted |
devenv | cmd (required), profile | zoryn devenv [--profile <profile>] -- bash -lc <cmd> — the command runs inside the dev environment, so it does have bash there |
zoryn | args (required, array of strings) | runs zoryn with those arguments verbatim, e.g. args = ["check", "spec"] |
agent | cli, prompt | opens a devenv with the agent's feature enabled and runs the AI CLI non-interactively on the newest .gear/build.*.log (see AI-agent repair). Off unless [agent] flow_steps = true |
An unknown run value, a missing required key, a steps value that is not an array of inline tables, or a missing steps array is a configuration error naming the flow (and, where it applies, the step number).
Common step keys¶
| Key | Type | Meaning |
|---|---|---|
name | string | label printed after the step in show/--dry-run, and the step's ref in qualified flags (--<name>.<key>) — give steps of the same type distinct names to address them individually |
if | string | shell condition; the step runs only when the command exits 0. A skipped step is not a failure |
on-failure | string | name of a recovery flow to run when the step fails, before retrying it |
max-retries | integer, default 1 | how many recovery-and-retry attempts the step gets. Has no effect without on-failure |
if conditions are executed by the same shell as a bash step, so {git}-style command templates and flow variables both work: if = "test -f .gear/{pkg}.spec".
Step options¶
The up, build, submit, task-* and agent steps take typed option keys that map to the corresponding command's flags. Every option is optional; when unset, the step behaves exactly like the bare command with that flag absent — including reading your config. For example, an up/build step with no parallel key builds in parallel when [builders] parallel = "on", just like zoryn up; a submit step with no commit/task-run publishes and runs according to your [submit] config.
up and build (the build phase; no-build is up-only):
| Key | Type | Effect |
|---|---|---|
builder | string | build on the named builder (else the configured/default builder) |
arch | string | target architecture filter |
branch | string | build target branch |
rebuild | bool | use hsh-rebuild |
skip-check | bool | skip checks — equivalent to a bare zoryn build --skip-check (rpmbuild,all) |
parallel | bool | force parallel (true) or sequential (false) multi-builder builds; unset follows [builders] parallel |
no-python-auto-deps | bool | disable the Python auto-deps update-and-retry |
no-build | bool | up only: update (version bump, spec, gear tags, up-hooks) and stop, without building — the step still publishes {old_version}/{new_version}, so a later build step does the build. On a build step the key is a config error. Its CLI negation is --build, not --no-no-build |
submit:
| Key | Type | Effect |
|---|---|---|
branch | string | target repository (-B); unset lets the pipeline resolve it |
task-run | bool | true starts the task (--run), false forces --no-run; unset follows [submit] run |
commit | bool | true forces a real submit; unset follows [submit] (test-only vs publish) |
test-only | bool | true forces a test-only task |
no-deps | bool | submit without dependencies |
skip-check | bool | skip the pre-submit spec check |
allow-overwrite-tag | bool | allow overwriting an existing tag |
message | string | commit/tag message (no editor) |
with | string | add a package to the task (--with) |
replace | string | replace a subtask in an existing task (--replace) |
Setting both commit and test-only is a conflict and fails the step, exactly as zoryn submit rejects it.
task-add:
| Key | Type | Effect |
|---|---|---|
task | string | task to add to; defaults to {task}, the task the submit step created |
args | array of strings | action tail passed verbatim: packages, rebuild, a branch |
dependent-on | string | --dependent-on PKG |
task-approve:
| Key | Type | Effect |
|---|---|---|
task | string | task to approve; defaults to {task} |
subtask | string | a number, package, pkg.git=tag or all; defaults to all |
message | string | -m comment |
task-run:
| Key | Type | Effect |
|---|---|---|
task | string | task to run; defaults to {task} |
commit | bool | --commit — commit the task into the repository on success |
message | string | -m comment |
agent:
| Key | Type | Effect |
|---|---|---|
cli | string | the AI CLI to run (claude, codex or opencode); defaults to [agent] cli |
prompt | string | replaces the built-in repair prompt; templated, and a step with its own prompt no longer requires a build log to exist |
Options as flags (implicit by name)¶
Every option key of the selected flow's steps is also a command-line flag on zoryn flow <NAME>, so you can override a step's config value for one run without editing the flow:
- a bool key
k→--k(sets ittrue) or--no-k(sets itfalse); - a value key
k→--k VALUEor--k=VALUE; branchalso answers to zoryn's usual short form-B(-B sisyphus,p11).
A CLI value overrides the step's config value for the step(s) that declare it; unset on both CLI and config still means "command default". --dry-run prints each step with its effective options, so you can see what will run.
If a key is declared by only one step, the bare --k is enough. If more than one step declares it (for instance both up and submit accept skip-check), the bare form is ambiguous and rejected; qualify it as --<step>.<key>, where <step> is the step's name if set, otherwise its run type (up, submit, …). The qualified form is always accepted.
An unknown flag lists the options the flow accepts; an ambiguous one lists the concrete qualified forms. Bash TAB completion offers the selected flow's flags and completes branch names after -B/--branch.
zoryn flow release --commit # override the submit step's commit
zoryn flow release --no-commit # force a test-only submit for this run
zoryn flow update --submit.skip-check # qualify a key shared by two steps
zoryn flow update --builder arm-01 # send the up/build to a specific builder
Substitutions¶
The tokens {pkg}, {old_version}, {new_version}, {tasks} and {task} are replaced in a step's cmd, host, args elements, prompt and if value:
| Token | Value | Available from |
|---|---|---|
{pkg} | package directory name (the git repository root) | the first step |
{old_version} | version before the update | after a successful up step |
{new_version} | version after the update | after a successful up step |
{tasks} | all ids of the gyle tasks just created, comma-separated in submission order (e.g. 123,456 for a multi-repo submit) | after a successful submit step |
{task} | the first created task id (single-repo back-compat) | after a successful submit step |
Prefer {tasks} — it works for one repo (just 123) or many, and zoryn task run/add/approve all accept the comma-separated list. Use {task} only when you deliberately want the first id alone.
Using a token whose value no earlier step has produced fails the step with flow variable '<name>' is not available yet (produced by a later step) — it is a config mistake, not a runtime accident.
Every other {...} token is left alone and reaches the usual command templates — {git}, {ssh} and friends keep working inside cmd values.
A substituted value crosses into /bin/sh, and some values come from the package repository ({old_version} is the raw Version: field). To keep a hostile repository from smuggling shell syntax through them, a value is only substituted when it consists solely of the safe set A-Za-z0-9.+~_,- (which covers any real package name, version, task id and the comma-joined {tasks} list); anything else fails the step with flow variable '<name>' has an unsafe value … rather than reaching the shell.
The other keys (branch, config, profile, cli, name) are used as written; they are not templated.
Branching, retries and recovery¶
A failing step is not automatically fatal. When it has an on-failure flow and attempts are left:
- the recovery flow is looked up in the same layers and run to completion;
- the failed step is retried;
max-retriescounts these attempts (default1, i.e. one recovery plus one retry).
The recovery flow is a normal flow — it can itself have steps with on-failure. Nesting is capped at depth 5, so a flow that recovers into itself and always fails still terminates with a failure instead of looping. A recovery flow's own progress lives in memory only and is never written to the state file.
If the recovery flow fails, or its name is not defined, the original step fails with both messages joined (build failed; recovery flow 'fix-build' also failed: …).
The up step and a pending build¶
up bumps and commits the version before it builds. Once the commit lands, a naive retry would find the package up to date and stop the flow as "nothing to do", never building the committed tree and never reaching submit. zoryn closes that in both directions. The bumped version and a pending-build marker are written to the state file before the build starts, so even a hard kill during the build (Ctrl-C, kill, OOM, reboot) leaves resumable state. And if the build merely fails, the same marker is recorded with the versions the attempt computed. Either way the retry — in the same process after a recovery flow, or a later zoryn flow --continue, even from a fresh process — rebuilds the version that is already there, publishes {old_version} and {new_version} as a successful up would, and the flow carries on to the following steps.
An up step that finds no new version is not a failure: it stops the flow successfully (exit 0). This is the normal outcome of a nightly cron run.
AI-agent repair¶
An agent step opens a devenv with [agent] cli (default claude, also codex or opencode) enabled as a devenv feature, and runs that CLI non-interactively — claude -p, codex exec, opencode run — with a prompt naming the newest .gear/build.*.log and pointing the CLI at the build-repair workflow of the bundled zoryn Agent Skill (references/repair.md: read the log, fix the packaging, verify with gear-rpm inside the devenv, leave the changes uncommitted for the flow's next steps). The devenv bind-mounts the bundled skill directory read-only at its host path, so the pointer installed by zoryn agent skill install resolves inside it. If there is no build log, the step fails; an unsupported cli fails the step too.
Both halves can be set per step: the cli key overrides the [agent] cli machine value, and the prompt key replaces the built-in repair prompt entirely — { run = "agent", prompt = "run the tests of {pkg} and fix what fails, then exit" }. A step prompt is templated like a cmd and stands on its own, so it does not require a build log to exist; making the CLI exit when done stays the prompt's responsibility, as it is for the built-in one.
The agent runs with your real credentials, mounted writable
The devenv the agent runs in mounts your host ~/.claude and ~/.claude.json (and the codex/opencode equivalents) read-write — the CLI needs them to authenticate. The "change only this repository" rule lives in the skill only; nothing in the sandbox enforces it, and a prompt-injected agent (build logs are attacker-influenced text) could write to those files, e.g. installing hooks that run on your next session. Agent steps are therefore off by default: enable them with [agent] flow_steps = true in ~/.zoryn, and only for flows and repositories you trust. Without it, an agent step fails with a message explaining this.
Combined with on-failure and max-retries this gives the "build broke, let the agent try, build again" loop:
[flows.update]
steps = [ { run = "up", on-failure = "fix-build", max-retries = 3 } ]
[flows.fix-build]
steps = [ { run = "agent" } ]
[agent]
cli = "claude"
flow_steps = true
State and resuming¶
A flow run keeps its position in ~/.local/state/zoryn/flow-<project>-state.json ($XDG_STATE_HOME is honoured). The state holds the flow name, the next step, the variables collected so far and the used attempts of the current step; it is written after every completed or skipped step and cleared when the flow finishes or stops cleanly.
- A failed step leaves the state in place and prints how to resume. Fix the problem, then
zoryn flow --continueretries exactly that step with the variables it already had. - Starting a flow while an unfinished one is saved is refused — choose
--continue,--restartor--abort. --continueand--restartwithoutNAMEact on whatever the state names, even if the config would now select a different flow. Given aNAMEthat does not match the saved flow, both are refused.- The state file is keyed by the package directory name, so two checkouts of one package (e.g. a
sisyphusand ap11clone both in a directory calledfoo) would share it. The saved state records its checkout path, and a--continue/--restartfrom the other checkout is refused rather than resuming the wrong tree. --abortonly needs the file to exist, so it also rescues a state file too damaged to load; a corrupt-state error on any command names--abortas the way out.--dry-runprints the plan before any of this: an interrupted flow neither blocks it nor is discarded by--restart.
Because a flow with an up or build step depends on a working builder, the builders are resolved and their mountpoints validated before the first step runs — a misconfigured builder fails the flow immediately instead of halfway through.
Exit codes¶
| Code | Meaning |
|---|---|
0 | the flow finished, or an up step found no new version |
1 | a step failed (state saved), or the flow could not be started: unknown flow, no flow selected, unparsable config, incompatible flags, no usable builder |
Related¶
zoryn up— the update pipeline anupstep runszoryn submit— what asubmitstep doeszoryn devenv— the environmentdevenvandagentsteps use- Configuration —
[flows],[agent]and.gear/version-up