oxo-flow lint#
Run best-practice linting checks on a .oxoflow file.
Usage#
Arguments#
| Argument | Description |
|---|---|
<WORKFLOW> |
Path to the .oxoflow workflow file |
Options#
| Option | Short | Default | Description |
|---|---|---|---|
--strict |
— | — | Treat warnings as errors (non-zero exit) |
--ai |
— | — | Enable AI-powered semantic linting |
--verbose |
-v |
— | Enable verbose (debug-level) logging |
--quiet |
— | — | Global log-level flag: suppresses info-level log lines and the banner. Lint diagnostics and the summary are printed regardless |
--no-color |
— | — | Disable colored output |
--json |
— | — | Output machine-readable JSON to stdout |
Examples#
Run standard linting#
Run strict linting#
Output#
oxo-flow v0.25.0 — Rust-native bioinformatics pipeline engine
warning [W003]: rule has no description (rule: bwa_align)
hint: add description = "Brief one-line description of what bwa_align does" to this rule
info [W004]: rule has a shell command but no log file specified (rule: bwa_align)
hint: add log = "logs/bwa_align.log"
info [W007]: leaf rule (no dependents) could be marked as target = true (rule: fastqc)
hint: add target = true to this rule
info [W025]: rule uses deprecated rule-level threads/memory (rule: bwa_align)
hint: replace `threads = N` / `memory = "8G"` with `resources.threads = N` / `resources.memory = "8G"` under this rule
Summary: 0 errors, 1 warning, 3 info
Each diagnostic prints a hint: line (when a suggestion exists) showing
the fix, matching the style of validate and run output. W007
suggests target = true: a rule marked as a target is built by default
when oxo-flow run is invoked without an explicit -t, so marking the
final leaf rules (like fastqc above) makes them part of the default
run — see Workflow Format: Priority and Targeting.
W025 flags the deprecated rule-level threads = N / memory = "8G"
fields (superseded by [rules.resources] in v0.4) so old workflows
surface the migration instead of silently keeping their old settings —
see Workflow Format: Rule resources.
W031 flags a consumer rule that reads the output of a when-gated
producer without a when gate of its own: when the producer's gate is
off, its files never appear and the consumer's inputs cannot be resolved
at plan time (dry-run input ✗) or the run fails outright. Wildcard
templates match after canonicalizing every {...} placeholder, and
fully literal paths match literally — a literal input of a gated
producer is the same hazard. The repair is either a matching when gate
on the consumer or splitting it into when-gated variants (the
multiqc/multiqc_pseudo idiom). Consumers that declare their
tolerance for a missing producer are not flagged: optional = true /
"any" rules, input_groups disk-discovery fallbacks, and consumers
that already carry any when gate.
W033 flags two rules that write the same output path — including
differently-named wildcards over the same template (variants/{smp}.vcf
vs variants/{sample}.vcf) and identical literal paths. The engine does
not refuse to run: both rules execute and the second to finish silently
overwrites the first (verified live in v0.25.0 with a run reporting
success while both colliding rules executed), so downstream rules consume
the wrong file. The repair is distinct output directories per writer —
or a when gate when the two rules are mutually exclusive alternatives
(the wgs_coverage/wes_coverage idiom). Multi-producer outputs remain
a supported feature, so this is a warning, not an error. {config.*}
placeholders are resolved against the parsed config before the templates
are compared, so {config.out_dir}/{sample}.vcf collides with its literal
counterpart results/{sample}.vcf when out_dir = "results" — and does
not collide with {config.alt_dir}/{sample}.vcf when the values differ.
When both writers carry meaningful when gates that are complementary
empty-string checks on the same expression — X != '' on one and
X == '' on the other (issue #860; the paired/tumor-only
mutect2_paired/mutect2_tumoronly shape) — each rule runs exactly when
the other does not, so the pair can never race and the collision is not
reachable at runtime. W033 then downgrades to an info note naming the
shared expression ("... their when gates are complementary on 'X' —
collision is not reachable at runtime") instead of the warning. The
detection is deliberately narrow: a single top-level ==/!= whose
right side is the empty-string literal '' or "" and whose left side
is the same bare config.<name> or wildcard.<name> reference on both
sides. These are the only left-hand forms the executor compares with
exact complement semantics — compound conditions (&&/||),
comparison operators
other than ==/!=, function-call or unqualified-identifier left
sides, different expressions, non-empty or unquoted
right-hand sides, and pairs where either writer is ungated all keep the
plain warning — a syntactic complement there would not prove a runtime
one (a shared disjunct short-circuits both gates to true; anything the
executor cannot parse as a reference falls through to its default-true
verdict).
See
Troubleshooting → Output collisions.
W034 flags an {input[N]} / {output[N]} placeholder whose index is out
of range, or a named {input.key} / {output.key} whose key the rule
does not declare. The substitution loop only renders indices 0..len
(list-form: declaration order; map-form: keys in sorted order; dir-form:
the single entry), and only renders named references for declared keys —
anything else survives rendering as literal brace text, so the tool is
invoked with a path like {input[5]} and fails with an unrelated
"No such file" error that hides the real mistake. The repair depends on
the input shape: use an in-range index (map-form indices follow the
rule's sorted keys, not the text order), switch to the bare {input} /
{output} form to interpolate all entries, or — preferred for
role-bearing IO — declare a map and use named access (tumor = "..." +
{input.tumor}), which also survives reordering. Lines that are comments
in the rendered script are exempt, and script paths are checked the
same way as inline shell.
W032 flags a [config] key whose name looks like a secret
(token, secret, passwd, password, credential, api_key,
access_key, private_key, ssh_key, key — matched at word/segment
boundaries, so names that merely contain these as a substring, like
tokenize or password_length_hint, are not flagged) but is not
declared sensitive: its value then lands in plaintext in command
records, stderr tails, and the checkpoint — credential leakage on shared
clusters and in CI artifacts. The repair is the declaration itself:
Declared-sensitive keys are env-routed and masked on disk, so W032
never flags them — see
[config] — Configuration Variables.
Notes#
lintis a strict superset ofvalidate: it runs every validate check (parsing, DAG, input existence) plus style linting and secret scanning — runningvalidateseparately afterlintadds nothing- Linting checks for common mistakes, missing metadata, and potential performance issues
- Rules are checked for valid input/output patterns and environment declarations
- Use
--strictto ensure high-quality workflow definitions in production environments
Every finding carries a stable code (E0xx = validation error, W0xx =
lint warning). The full code index with one-line meanings lives in the
Glossary → Diagnostic Codes.