Skip to content

Create a Workflow#

This guide covers the complete process of authoring an .oxoflow workflow file, from project scaffolding to production-ready pipelines.


Scaffold a new project#

oxo-flow init my-pipeline
cd my-pipeline

This generates a project directory with a starter .oxoflow file, envs/ and scripts/ directories, an example data/input.txt, and a .gitignore.


Workflow file structure#

Every .oxoflow file is TOML with four core top-level sections (additional sections like [report], [[sample_groups]], [[pairs]], and [[include]] are available for advanced use):

[workflow]      # Required: name
[config]        # Optional: user-defined variables
[defaults]      # Optional: default settings for all rules
[[rules]]       # Required: one or more pipeline steps

The [workflow] section#

[workflow]
name = "my-pipeline"
version = "1.0.0"
description = "Short description of what this pipeline does"
author = "Shixiang Wang <w_shixiang@163.com>"
Field Required Description
name Yes Pipeline name (used in reports and logs)
version No Semantic version (defaults to "0.1.0")
description No Human-readable description
author No Author name or organization

The [config] section#

Define variables that are referenced throughout the workflow:

[config]
reference = "/data/ref/hg38.fa"
samples_dir = "raw_data"
results_dir = "results"
min_quality = "30"

Reference them in rule fields with {config.variable_name}:

shell = "bwa mem {config.reference} {input} > {output}"

The [defaults] section#

Set default values applied to all rules unless overridden:

[defaults]
threads = 4
memory = "8G"
environment = { conda = "envs/base.yaml" }

Defining rules#

Each [[rules]] entry defines one step in the pipeline:

[[rules]]
name = "step_name"
input = ["path/to/input1.txt", "path/to/input2.txt"]
output = ["path/to/output.txt"]
environment = { conda = "envs/tools.yaml" }
shell = "my-tool --threads {threads} {input} > {output}"

[rules.resources]
threads = 8
memory = "16G"

Rule fields#

Field Required Type Description
name Yes String Unique rule identifier
shell Conditional String Shell command to execute; required whenever the rule declares output (a script or transform may replace it)
input No Array Input file paths (may contain wildcards; recommended for file-based dependency inference)
output No Array Output file paths (may contain wildcards; recommended for checkpoint/resume)
environment No Table Environment specification
resources No Table Resource specification (threads, memory, GPU, disk, time_limit)

Wildcards#

Use {name} syntax for dynamic file patterns:

[[rules]]
name = "align"
input = ["{sample}_R1.fastq.gz", "{sample}_R2.fastq.gz"]
output = ["aligned/{sample}.bam"]
shell = "bwa mem ref.fa {input} | samtools sort -o {output}"

oxo-flow expands {sample} from the available input files or from explicit configuration.

Built-in placeholders#

Placeholder Expands to
{input} Space-separated list of all input files
{output} Space-separated list of all output files
{input[N]} / {output[N]} The Nth file (0-indexed) — see note below
{threads} Thread count for this rule
{config.*} Value from the [config] section

{input} vs {input[0]} — for a single-input rule both expand identically ({input} joins all inputs; {input[0]} takes the first). Use the simple form for single files, the indexed form when selecting specific files from a list.


Dependencies#

oxo-flow infers dependencies automatically: if rule B's input matches rule A's output, B depends on A. You do not need to declare dependencies explicitly.

How dependency inference works#

When the DAG is built, oxo-flow examines every rule's input and output fields and creates edges wherever a match is found:

[[rules]]
name = "step1"
output = ["intermediate.txt"]
shell = "echo hello > intermediate.txt"

[[rules]]
name = "step2"
input = ["intermediate.txt"]   # ← matches step1's output → step2 depends on step1
shell = "cat intermediate.txt"

Step-by-step, the engine:

  1. Registers outputs: Each rule's output paths are recorded — "rule X produces file Y"
  2. Matches inputs: Each rule's input paths are checked against the output registry
  3. Creates edges: Every match creates a dependency edge (producer → consumer)
  4. Adds explicit deps: Any depends_on entries also create edges
  5. Validates: The resulting graph is checked for cycles

Explicit vs inferred dependencies#

Mechanism How it works When to use
File-based (inferred) input/output path matching Standard pipeline steps — the default
depends_on (explicit) Direct rule name reference Ordering without shared files (e.g., mkdir before downstream tools)

Example — when to use depends_on:

# Setup rule creates directories — no shared output files
[[rules]]
name = "setup_dirs"
shell = "mkdir -p results/qc results/aligned"
output = []  # No output files!

# Downstream rule doesn't consume setup's output, but must run after
[[rules]]
name = "align"
depends_on = ["setup_dirs"]   # ← explicit ordering, no file to match
input = ["raw/sample.fastq.gz"]
output = ["results/aligned/sample.bam"]
shell = "bwa mem ref.fa {input} > {output}"

Tip: Prefer file-based dependencies whenever possible — they make the data flow explicit and self-documenting. Use depends_on only when the ordering can't be expressed through files.

Verifying your DAG#

After writing your workflow, inspect the dependency structure:

# See the execution order and dependency graph
oxo-flow graph workflow.oxoflow

# See just the execution plan without running
oxo-flow dry-run workflow.oxoflow

# Check for structural issues (cycles, orphans, collisions)
oxo-flow validate workflow.oxoflow

Multi-line shell commands#

Use triple-quoted strings for complex commands:

shell = """
mkdir -p results
bwa mem -t {threads} {config.reference} {input} | \
  samtools sort -@ {threads} -o {output}
samtools index {output}
"""

Best practices#

Keep rules focused

Each rule should do one logical step. This makes the DAG clearer and allows better parallelism.

Use config variables

Put paths and parameters in [config] so they can be changed without editing rule definitions.

Lock environment versions

Pin tool versions in your conda YAML or Docker tags to ensure reproducibility.

Validate early

Run oxo-flow validate before executing to catch syntax errors and circular dependencies.

Use batch for simple tasks

For quick parallel operations (e.g., running the same command on multiple files), use oxo-flow batch instead of writing a full workflow:

# Instead of writing a workflow, use batch for simple tasks
oxo-flow batch "samtools flagstat {item}" *.bam -j 8
oxo-flow batch "fastqc {item}" *.fastq.gz


Complete example#

See the Workflow Format reference for the full specification.