Skill Tracer
Skills › Scientific Agent Skills › nextflow › language.md

language.md

335 lines · skills/nextflow/references/language.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Nextflow Language (DSL2)
2
3A working reference for the untyped DSL2 interfaces used by many existing nf-core pipelines: processes, channels, operators, workflows, and modules. Nextflow is a Groovy-based DSL; DSL2 is the default and only DSL (DSL1 is removed, so `nextflow.enable.dsl=2` is unnecessary). Targets stable 26.04.6. Source: https://github.com/nextflow-io/nextflow/tree/v26.04.6/docs and https://docs.seqera.io/nextflow/ . Incomplete biological/tool examples below are illustrative; they require the named tools, references and missing module definitions.
4
5**Current syntax conventions**: `NXF_SYNTAX_PARSER=v2` is the default in **26.04** (opt-in in 25.x). Strict syntax does not imply static typing. Typed processes/workflows remain a preview and require `nextflow.enable.types = true` in each applicable script; do not mix their syntax with untyped examples.
6- **`channel.of(...)`** (lowercase namespace) is canonical; `Channel.of(...)` still works but is discouraged.
7- **Explicit closure parameters** (`{ v -> v * 2 }`) are preferred over the implicit `it`.
8- Name process outputs with **`emit:`**; scale resources with **`task.attempt`**.
9- **`output {}` + `publish:`** (full feature in 25.10) is the new declarative way to publish results; `publishDir` still works and remains dominant in nf-core — both are shown below.
10- Avoid removed/deprecated idioms: `for`/`while` loops (use `each`/`collect`), `import`/custom `class` (use functions or `lib/`), `process shell:` (use `script:`), `include … addParams()`.

Nextflow Language (DSL2)

A working reference for the untyped DSL2 interfaces used by many existing nf-core pipelines: processes, channels, operators, workflows, and modules. Nextflow is a Groovy-based DSL; DSL2 is the default and only DSL (DSL1 is removed, so nextflow.enable.dsl=2 is unnecessary). Targets stable 26.04.6. Source: https://github.com/nextflow-io/nextflow/tree/v26.04.6/docs and https://docs.seqera.io/nextflow/ . Incomplete biological/tool examples below are illustrative; they require the named tools, references and missing module definitions.

Current syntax conventions: NXF_SYNTAX_PARSER=v2 is the default in 26.04 (opt-in in 25.x). Strict syntax does not imply static typing. Typed processes/workflows remain a preview and require nextflow.enable.types = true in each applicable script; do not mix their syntax with untyped examples.

  • **channel.of(...)** (lowercase namespace) is canonical; Channel.of(...) still works but is discouraged.
  • Explicit closure parameters ({ v -> v * 2 }) are preferred over the implicit it.
  • Name process outputs with **emit:; scale resources with task.attempt**.
  • **output {} + publish:** (full feature in 25.10) is the new declarative way to publish results; publishDir still works and remains dominant in nf-core — both are shown below.
  • Avoid removed/deprecated idioms: for/while loops (use each/collect), import/custom class (use functions or lib/), process shell: (use script:), include … addParams().
Raw lines 1 to 11
12## Table of Contents
14- [Script structure](#script-structure)
15- [Processes](#processes)
16- [Process directives](#process-directives)
17- [Channels](#channels)
18- [Operators](#operators)
19- [Workflows](#workflows)
20- [Modules](#modules)
21- [Dynamic resources and error handling](#dynamic-resources-and-error-handling)
22- [Groovy essentials and gotchas](#groovy-essentials-and-gotchas)

Table of Contents

  • Script structure
  • Processes
  • Process directives
  • Channels
  • Operators
  • Workflows
  • Modules
  • Dynamic resources and error handling
  • Groovy essentials and gotchas
Raw lines 12 to 23
24## Script structure
26A Nextflow script (`.nf`) uses process/workflow definitions and the supported subset of Groovy; strict syntax does not allow arbitrary Groovy. Every script enables DSL2 implicitly (it is the default since 22.03). A run begins at the **unnamed `workflow {}`** block (the entry workflow).
28```nextflow
29#!/usr/bin/env nextflow
31params.input = 'data/*.fastq' // pipeline parameter with a default
33process FASTQC { /* ... */ } // a process definition
35workflow { // entry point
36 reads = channel.fromPath(params.input, checkIfExists: true)
37 FASTQC(reads)
39```
41Run with `nextflow run main.nf --input 'data/*.fastq'`. Parameters declared as `params.x` are overridable on the CLI (`--x`), in `nextflow.config`, or in a `-params-file`.

Script structure

A Nextflow script (.nf) uses process/workflow definitions and the supported subset of Groovy; strict syntax does not allow arbitrary Groovy. Every script enables DSL2 implicitly (it is the default since 22.03). A run begins at the **unnamed workflow {}** block (the entry workflow).

#!/usr/bin/env nextflow

params.input = 'data/*.fastq'        // pipeline parameter with a default

process FASTQC { /* ... */ }          // a process definition

workflow {                            // entry point
    reads = channel.fromPath(params.input, checkIfExists: true)
    FASTQC(reads)
}

Run with nextflow run main.nf --input 'data/*.fastq'. Parameters declared as params.x are overridable on the CLI (--x), in nextflow.config, or in a -params-file.

Raw lines 24 to 42
Runs code bin/env :29
43## Processes
45A `process` defines a task: a (usually Bash) script executed in its own isolated **work directory**. Nextflow stages declared inputs in and declared outputs out, so processes never read/write each other's files directly — they communicate only through channels.
47```nextflow
48process ALIGN {
49 tag "$meta.id" // label shown in the log/trace
50 label 'process_high' // maps to resources in config
51 // Supply a pinned environment containing BOTH bwa and samtools.
52 publishDir "${params.outdir}/bam", mode: 'copy'
54 input:
55 tuple val(meta), path(reads) // a sample: metadata map + file(s)
56 tuple path(reference), path(index_files) // shared FASTA + BWA sidecars as a value
58 output:
59 tuple val(meta), path("*.bam"), emit: bam
60 path "versions.yml", emit: versions
62 when:
63 meta.run_alignment != false // skip task if false
65 script:
66 def prefix = task.ext.prefix ?: meta.id
67 def args = task.ext.args ?: '' // extra flags injected from config
68 """
69 bwa mem $args -t $task.cpus "$reference" ${reads} | samtools sort -o "${prefix}.bam"
71 cat <<-END_VERSIONS > versions.yml
72 "${task.process}":
73 bwa: \$(bwa 2>&1 | sed -n 's/Version: //p')
74 samtools: \$(samtools --version | sed '1!d; s/samtools //')
75 END_VERSIONS
76 """
78 stub:
79 """
80 touch ${meta.id}.bam
81 touch versions.yml
82 """
84```

Processes

A process defines a task: a (usually Bash) script executed in its own isolated work directory. Nextflow stages declared inputs in and declared outputs out, so processes never read/write each other's files directly — they communicate only through channels.

process ALIGN {
    tag    "$meta.id"                 // label shown in the log/trace
    label  'process_high'             // maps to resources in config
    // Supply a pinned environment containing BOTH bwa and samtools.
    publishDir "${params.outdir}/bam", mode: 'copy'

    input:
    tuple val(meta), path(reads)      // a sample: metadata map + file(s)
    tuple path(reference), path(index_files)  // shared FASTA + BWA sidecars as a value

    output:
    tuple val(meta), path("*.bam"), emit: bam
    path  "versions.yml",           emit: versions

    when:
    meta.run_alignment != false       // skip task if false

    script:
    def prefix = task.ext.prefix ?: meta.id
    def args   = task.ext.args  ?: ''   // extra flags injected from config
    """
    bwa mem $args -t $task.cpus "$reference" ${reads} | samtools sort -o "${prefix}.bam"

    cat <<-END_VERSIONS > versions.yml
    "${task.process}":
        bwa: \$(bwa 2>&1 | sed -n 's/Version: //p')
        samtools: \$(samtools --version | sed '1!d; s/samtools //')
    END_VERSIONS
    """

    stub:
    """
    touch ${meta.id}.bam
    touch versions.yml
    """
}
Raw lines 43 to 85
86### Inputs
88Declared one per line under `input:`. Each input consumes one item from a channel.
90| Qualifier | Meaning |
91|-----------|---------|
92| `val(x)` | Any value (string, number, map) |
93| `path(f)` | A file/dir; staged into the work dir. Use `path` (not the old `file`) |
94| `tuple val(meta), path(reads)` | A composite item — the nf-core standard: a meta map + files |
95| `env(NAME)` | Value exposed as an environment variable |
96| `stdin` | Feed the channel item to the script's stdin |
97| `each x` | Repeat the process once per value in `x` (combinatorial) |
99Inputs are positional and matched to channels in call order: `ALIGN(reads_ch, index_ch)`.

Inputs

Declared one per line under input:. Each input consumes one item from a channel.

QualifierMeaning
val(x)Any value (string, number, map)
path(f)A file/dir; staged into the work dir. Use path (not the old file)
tuple val(meta), path(reads)A composite item — the nf-core standard: a meta map + files
env(NAME)Value exposed as an environment variable
stdinFeed the channel item to the script's stdin
each xRepeat the process once per value in x (combinatorial)

Inputs are positional and matched to channels in call order: ALIGN(reads_ch, index_ch).

Raw lines 86 to 100
101### Outputs
103Declared under `output:`; each becomes a channel. Use `emit:` to name outputs so callers can reference `ALIGN.out.bam` instead of positional `ALIGN.out[0]`.
105| Form | Meaning |
106|------|---------|
107| `path "*.bam"` | Files matched by glob in the work dir after the script runs |
108| `tuple val(meta), path("*.bam")` | Carry metadata forward with the file |
109| `val x` | Emit a value computed in the process |
110| `stdout` | Capture the script's stdout as the output |
111| `eval('cmd')` | Capture the stdout of a command run in the task env (24.04+) — used for tool versions |
112| `path "out", emit: name` | Named output channel (reference as `PROC.out.name`) |
113| `..., topic: versions` | Also route this output to a named topic channel |
114| `path "out", optional: true` | Output may be absent without erroring |

Outputs

Declared under output:; each becomes a channel. Use emit: to name outputs so callers can reference ALIGN.out.bam instead of positional ALIGN.out[0].

FormMeaning
path "*.bam"Files matched by glob in the work dir after the script runs
tuple val(meta), path("*.bam")Carry metadata forward with the file
val xEmit a value computed in the process
stdoutCapture the script's stdout as the output
eval('cmd')Capture the stdout of a command run in the task env (24.04+) — used for tool versions
path "out", emit: nameNamed output channel (reference as PROC.out.name)
..., topic: versionsAlso route this output to a named topic channel
path "out", optional: trueOutput may be absent without erroring
Raw lines 101 to 115
116### Script, shell, exec
118- **`script:`** (default) — a multi-line string run as Bash. Nextflow variables interpolate with `$var`/`${expr}`; escape shell vars you do NOT want Nextflow to touch as `\$var`.
119- **`shell:`** — like script but Nextflow vars use `!{var}`, leaving `$` for the shell. **Deprecated as of 25.04** — use `script:` with `\$` to escape shell vars.
120- **`exec:`** — native Groovy, no external process (for in-line computation).
121- A process can run any interpreter via a shebang (e.g. `#!/usr/bin/env python`).
123```nextflow
124process PY {
125 input: val x
126 output: stdout
127 script:
128 """
129 #!/usr/bin/env python
130 print(${x} ** 2)
131 """
133```

Script, shell, exec

  • **script:** (default) — a multi-line string run as Bash. Nextflow variables interpolate with $var/${expr}; escape shell vars you do NOT want Nextflow to touch as \$var.
  • **shell:** — like script but Nextflow vars use !{var}, leaving $ for the shell. Deprecated as of 25.04 — use script: with \$ to escape shell vars.
  • **exec:** — native Groovy, no external process (for in-line computation).
  • A process can run any interpreter via a shebang (e.g. #!/usr/bin/env python).
process PY {
    input: val x
    output: stdout
    script:
    """
    #!/usr/bin/env python
    print(${x} ** 2)
    """
}
Raw lines 116 to 134
Runs code bin/env :121
135### Conditional execution
137- `when:` — skip the task when the expression is false (prefer filtering channels upstream when possible).
138- A `script:` can branch with normal Groovy `if/else` returning different command strings.
139- `stub:` replaces that task’s script under `-stub-run`; a task without one runs its real script. Containers, staging and `eval()` outputs may still run, so this is not a no-execution or no-cost dry run. nf-core modules require stubs.

Conditional execution

  • when: — skip the task when the expression is false (prefer filtering channels upstream when possible).
  • A script: can branch with normal Groovy if/else returning different command strings.
  • stub: replaces that task’s script under -stub-run; a task without one runs its real script. Containers, staging and eval() outputs may still run, so this is not a no-execution or no-cost dry run. nf-core modules require stubs.
Raw lines 135 to 140
141## Process directives
143Set inside a process (or globally via config). Most-used:
145| Directive | Purpose |
146|-----------|---------|
147| `cpus`, `memory`, `time`, `disk` | Resource requests (e.g. `memory '8 GB'`, `time '2 h'`) |
148| `container` | Container image for this process |
149| `conda` | Conda packages/env for this process |
150| `publishDir` | Copy/link outputs to a results dir (`mode: 'copy'|'symlink'|'link'`) |
151| `tag` | Human-readable label per task in logs/trace |
152| `label` | Group processes (target with `withLabel:` in config) |
153| `errorStrategy` | `'terminate'` (default), `'ignore'`, `'retry'`, `'finish'` |
154| `maxRetries`, `maxErrors` | Retry limits |
155| `cache` | `true`/`'lenient'`/`'deep'`/`false` — caching behavior |
156| `scratch` | Run in node-local scratch then stage out |
157| `stageInMode` / `stageOutMode` | Input: symlink/copy/link/rellink; scratch output: copy/move/rsync/fcp/rclone |
158| `beforeScript`/`afterScript` | Commands wrapping the task script |
159| `accelerator` | GPU request (e.g. `accelerator 1, type: 'nvidia-tesla-v100'`) |
160| `array` | Submit as a job array (HPC/cloud), e.g. `array 100` |
161| `ext` | Free-form map (`ext.args`, `ext.prefix`) injected from config |
162| `pod` | Kubernetes pod options |
163| `module` | Load an HPC environment module |
164| `maxForks` | Cap parallel tasks for this process |
166Access the resolved values at runtime via `task.*` (`task.cpus`, `task.memory`, `task.attempt`, `task.process`, `task.ext.args`).

Process directives

Set inside a process (or globally via config). Most-used:

DirectivePurpose
cpus, memory, time, diskResource requests (e.g. memory '8 GB', time '2 h')
containerContainer image for this process
condaConda packages/env for this process
publishDirCopy/link outputs to a results dir (`mode: 'copy''symlink''link'`)
tagHuman-readable label per task in logs/trace
labelGroup processes (target with withLabel: in config)
errorStrategy'terminate' (default), 'ignore', 'retry', 'finish'
maxRetries, maxErrorsRetry limits
cachetrue/'lenient'/'deep'/false — caching behavior
scratchRun in node-local scratch then stage out
stageInMode / stageOutModeInput: symlink/copy/link/rellink; scratch output: copy/move/rsync/fcp/rclone
beforeScript/afterScriptCommands wrapping the task script
acceleratorGPU request (e.g. accelerator 1, type: 'nvidia-tesla-v100')
arraySubmit as a job array (HPC/cloud), e.g. array 100
extFree-form map (ext.args, ext.prefix) injected from config
podKubernetes pod options
moduleLoad an HPC environment module
maxForksCap parallel tasks for this process

Access the resolved values at runtime via task.* (task.cpus, task.memory, task.attempt, task.process, task.ext.args).

Raw lines 141 to 167
168## Channels
170Channels are the asynchronous queues connecting processes. Two kinds:
172- **Queue channel**: an ordered, *consumable* stream of items. Produced by most factories/operators and by process outputs. DSL2 automatically fans the stream out to separate downstream consumers; each receives all items. A single process invocation consumes one item per queue input for each task.
173- **Value channel** (singleton): holds one value that can be read an unlimited number of times. Created by `channel.value()`, by operators like `collect`/`first`, or implicitly from a single value. A process input bound to a value channel is reused for every task.

Channels

Channels are the asynchronous queues connecting processes. Two kinds:

  • Queue channel: an ordered, consumable stream of items. Produced by most factories/operators and by process outputs. DSL2 automatically fans the stream out to separate downstream consumers; each receives all items. A single process invocation consumes one item per queue input for each task.
  • Value channel (singleton): holds one value that can be read an unlimited number of times. Created by channel.value(), by operators like collect/first, or implicitly from a single value. A process input bound to a value channel is reused for every task.
Raw lines 168 to 174
175### Channel factories
177```nextflow
178channel.of(1, 2, 3) // emit given values (ranges expand: 1..23)
179channel.fromList([1, 2, 3]) // emit list items
180channel.value('ref.fa') // singleton value channel
181channel.fromPath('data/*.bam') // one item per matching file
182channel.fromPath('data/**/*.fastq', checkIfExists: true) // also: type:'file', hidden:true
183channel.fromFilePairs('data/*_{1,2}.fastq.gz') // -> [id, [r1, r2]] for paired reads
184channel.topic('versions') // stable since 25.04; preview in 24.04/24.10
185channel.empty() // emits nothing
186```
188> `channel.fromSRA(...)` exists but is deprecated as of 26.04 — use the documented Entrez Direct route to discover accessions, then an explicit validated samplesheet and download workflow.
190`arity` is a process `path` input/output option, not a `fromPath` option. `checkIfExists` checks matching paths, not sample pairing or biological metadata.
192`fromFilePairs` is the idiomatic way to group paired-end reads; it yields `[ sampleId, [read1, read2] ]`, which you typically `map` into the nf-core `[ meta, [reads] ]` shape.

Channel factories

channel.of(1, 2, 3)                                  // emit given values (ranges expand: 1..23)
channel.fromList([1, 2, 3])                          // emit list items
channel.value('ref.fa')                              // singleton value channel
channel.fromPath('data/*.bam')                       // one item per matching file
channel.fromPath('data/**/*.fastq', checkIfExists: true) // also: type:'file', hidden:true
channel.fromFilePairs('data/*_{1,2}.fastq.gz')       // -> [id, [r1, r2]] for paired reads
channel.topic('versions')                            // stable since 25.04; preview in 24.04/24.10
channel.empty()                                      // emits nothing
channel.fromSRA(...) exists but is deprecated as of 26.04 — use the documented Entrez Direct route to discover accessions, then an explicit validated samplesheet and download workflow.

arity is a process path input/output option, not a fromPath option. checkIfExists checks matching paths, not sample pairing or biological metadata.

fromFilePairs is the idiomatic way to group paired-end reads; it yields [ sampleId, [read1, read2] ], which you typically map into the nf-core [ meta, [reads] ] shape.

Raw lines 175 to 193
194## Operators
196Operators transform/combine channels. Chain with `.`; the dataflow graph is built from these connections. The table includes legacy operators still used by untyped nf-core code. In 26.04, `groupTuple`, `flatten`, `concat`, `distinct`, `first`/`last`, the channel `split*` operators, `set`, and `dump` are deprecated; use the [core operator migration guide](https://docs.seqera.io/nextflow/tutorials/static-types-operators) for new code (`groupBy`, `flatMap` with Path `split*` methods, `mix`, `unique`, direct assignment, `view`, as applicable). Do not replace order-dependent logic mechanically.
198| Operator | Purpose |
199|----------|---------|
200| `map { }` | Transform each item |
201| `filter { }` | Keep items matching a condition/type/regex |
202| `flatten` | Flatten nested emissions into individual items |
203| `collect` | Gather all items into a single list (→ value channel) |
204| `toList` / `toSortedList` | Collect into one (sorted) list |
205| `groupTuple` | Group tuples by key (e.g. by `meta`) — often needs `groupTuple(by: 0)` |
206| `join` | Inner-join two channels by a matching key |
207| `combine` | Cartesian product (optionally `by:` a key) |
208| `cross` | Combine matching keyed items |
209| `mix` | Merge multiple channels into one stream |
210| `concat` | Emit one channel fully, then the next, in order |
211| `branch { }` | Route each item to the first matching named branch |
212| `multiMap { }` | Emit to several channels from one pass |
213| `splitCsv` / `splitText` / `splitFasta` / `splitFastq` | Split file contents into items |
214| `collectFile` | Write items into one or more files |
215| `unique` / `distinct` | De-duplicate |
216| `first` / `last` / `take` / `until` | Select subsets |
217| `set { ch }` | Name the resulting channel (alternative to `ch =`) |
218| `view { }` | Print items for debugging (returns the channel unchanged) |
219| `ifEmpty` | Provide a default if the channel is empty |
220| `dump(tag:'x')` | Debug-print when run with `-dump-channels x` |
222```nextflow
223// Build the nf-core [meta, reads] shape from a samplesheet
224reads_ch = channel
225 .fromPath(params.input, checkIfExists: true)
226 .flatMap { sheet -> sheet.splitCsv(header: true) }
227 .map { row -> tuple([id: row.sample, single_end: row.fastq_2 ? false : true],
228 row.fastq_2 ? [file(row.fastq_1, checkIfExists: true), file(row.fastq_2, checkIfExists: true)] : [file(row.fastq_1, checkIfExists: true)]) }
230// Legacy tuple workflow: group results, then join on an exact meta key
231counts.groupTuple()
232 .join(metadata, failOnDuplicate: true, failOnMismatch: true) // exact keyed matching
233 .view()
234```

Operators

Operators transform/combine channels. Chain with .; the dataflow graph is built from these connections. The table includes legacy operators still used by untyped nf-core code. In 26.04, groupTuple, flatten, concat, distinct, first/last, the channel split* operators, set, and dump are deprecated; use the core operator migration guide for new code (groupBy, flatMap with Path split* methods, mix, unique, direct assignment, view, as applicable). Do not replace order-dependent logic mechanically.

OperatorPurpose
map { }Transform each item
filter { }Keep items matching a condition/type/regex
flattenFlatten nested emissions into individual items
collectGather all items into a single list (→ value channel)
toList / toSortedListCollect into one (sorted) list
groupTupleGroup tuples by key (e.g. by meta) — often needs groupTuple(by: 0)
joinInner-join two channels by a matching key
combineCartesian product (optionally by: a key)
crossCombine matching keyed items
mixMerge multiple channels into one stream
concatEmit one channel fully, then the next, in order
branch { }Route each item to the first matching named branch
multiMap { }Emit to several channels from one pass
splitCsv / splitText / splitFasta / splitFastqSplit file contents into items
collectFileWrite items into one or more files
unique / distinctDe-duplicate
first / last / take / untilSelect subsets
set { ch }Name the resulting channel (alternative to ch =)
view { }Print items for debugging (returns the channel unchanged)
ifEmptyProvide a default if the channel is empty
dump(tag:'x')Debug-print when run with -dump-channels x
// Build the nf-core [meta, reads] shape from a samplesheet
reads_ch = channel
    .fromPath(params.input, checkIfExists: true)
    .flatMap { sheet -> sheet.splitCsv(header: true) }
    .map { row -> tuple([id: row.sample, single_end: row.fastq_2 ? false : true],
                        row.fastq_2 ? [file(row.fastq_1, checkIfExists: true), file(row.fastq_2, checkIfExists: true)] : [file(row.fastq_1, checkIfExists: true)]) }

// Legacy tuple workflow: group results, then join on an exact meta key
counts.groupTuple()
       .join(metadata, failOnDuplicate: true, failOnMismatch: true) // exact keyed matching
       .view()
Raw lines 194 to 235
236## Workflows
238A `workflow` composes processes and other workflows. The **unnamed** workflow is the entry point. **Named** workflows are reusable (sub)workflows.
240```nextflow
241workflow RNASEQ {
242 take: // named inputs (untyped here)
243 reads
244 index
246 main: // pipeline logic
247 FASTQC(reads)
248 ALIGN(reads, index)
249 QUANT(ALIGN.out.bam)
251 emit: // named outputs
252 bam = ALIGN.out.bam
253 counts = QUANT.out.counts
254 versions = FASTQC.out.versions.mix(ALIGN.out.versions)
257workflow { // entry: wire inputs and call the named workflow
258 reads = channel.fromFilePairs(params.reads, checkIfExists: true)
259 .map { id, pair -> tuple([id: id, single_end: false], pair) }
260 index = channel.value(tuple(file(params.fasta, checkIfExists: true),
261 files(params.bwa_index, checkIfExists: true)))
262 RNASEQ(reads, index)
263 RNASEQ.out.counts.view()
265```
267- Call a process/workflow like a function: `ALIGN(reads, index)`. Outputs are on `.out` (use `emit:` names: `ALIGN.out.bam`).
268- A process can only be **called once** per workflow; to reuse it, `include` it again under an alias.
269- Pipe syntax works for simple chains: `reads | FASTQC`.
270- **Declarative outputs (25.10+)**: assign channels in the entry workflow's `publish:` section and describe them in a top-level `output {}` block — the recommended replacement for the `publishDir` directive (which still works and dominates nf-core):
272```nextflow
273workflow {
274 main:
275 ch = ANALYZE(input)
276 publish:
277 results = ch // name the published channel
279output {
280 results { path 'analysis' } // -> <outputDir>/analysis (default outputDir: results/)
282```

Workflows

A workflow composes processes and other workflows. The unnamed workflow is the entry point. Named workflows are reusable (sub)workflows.

workflow RNASEQ {
    take:                       // named inputs (untyped here)
    reads
    index

    main:                       // pipeline logic
    FASTQC(reads)
    ALIGN(reads, index)
    QUANT(ALIGN.out.bam)

    emit:                       // named outputs
    bam    = ALIGN.out.bam
    counts = QUANT.out.counts
    versions = FASTQC.out.versions.mix(ALIGN.out.versions)
}

workflow {                      // entry: wire inputs and call the named workflow
    reads = channel.fromFilePairs(params.reads, checkIfExists: true)
                   .map { id, pair -> tuple([id: id, single_end: false], pair) }
    index = channel.value(tuple(file(params.fasta, checkIfExists: true),
                                files(params.bwa_index, checkIfExists: true)))
    RNASEQ(reads, index)
    RNASEQ.out.counts.view()
}
  • Call a process/workflow like a function: ALIGN(reads, index). Outputs are on .out (use emit: names: ALIGN.out.bam).
  • A process can only be called once per workflow; to reuse it, include it again under an alias.
  • Pipe syntax works for simple chains: reads | FASTQC.
  • Declarative outputs (25.10+): assign channels in the entry workflow's publish: section and describe them in a top-level output {} block — the recommended replacement for the publishDir directive (which still works and dominates nf-core):
workflow {
    main:
    ch = ANALYZE(input)
    publish:
    results = ch                       // name the published channel
}
output {
    results { path 'analysis' }        // -> <outputDir>/analysis (default outputDir: results/)
}
Raw lines 236 to 283
284## Modules
286Modules are `.nf` files whose processes/workflows are imported with `include`. This is the basis of nf-core's reusable components.
288```nextflow
289include { FASTQC } from './modules/fastqc/main.nf'
290include { ALIGN as ALIGN_TUMOR;
291 ALIGN as ALIGN_NORMAL } from './modules/align/main.nf'
292include { RNASEQ } from './subworkflows/rnaseq.nf'
293```
295- `as` aliases let you include the same component multiple times.
296- Includes are resolved relative to the including file; `.nf` extension optional.
297- Params should be passed explicitly (as inputs), not read globally inside modules — this keeps modules portable (an nf-core requirement).
298- Since 26.04, registry modules can be installed with `nextflow module install -version <version> <name>` and included by registry name. This is distinct from `nf-core modules install` and its `modules.json` bookkeeping. Pin the registry version/checksum; see [module registry](https://docs.seqera.io/nextflow/modules/module-registry). Local nf-core includes remain supported.

Modules

Modules are .nf files whose processes/workflows are imported with include. This is the basis of nf-core's reusable components.

include { FASTQC }                     from './modules/fastqc/main.nf'
include { ALIGN as ALIGN_TUMOR;
          ALIGN as ALIGN_NORMAL }      from './modules/align/main.nf'
include { RNASEQ }                     from './subworkflows/rnaseq.nf'
  • as aliases let you include the same component multiple times.
  • Includes are resolved relative to the including file; .nf extension optional.
  • Params should be passed explicitly (as inputs), not read globally inside modules — this keeps modules portable (an nf-core requirement).
  • Since 26.04, registry modules can be installed with nextflow module install -version <version> <name> and included by registry name. This is distinct from nf-core modules install and its modules.json bookkeeping. Pin the registry version/checksum; see module registry. Local nf-core includes remain supported.
Raw lines 284 to 299
300## Dynamic resources and error handling
302Make pipelines robust by retrying failures with more resources instead of over-provisioning everything. `task.attempt` increments on each retry.
304```nextflow
305process BIG_JOB {
306 label 'process_high'
307 cpus { 4 * task.attempt }
308 memory { 8.GB * task.attempt }
309 time { 4.h * task.attempt }
311 errorStrategy { task.exitStatus in [137, 140, 143] ? 'retry' : 'terminate' }
312 maxRetries 3
313 // ...
315```
317- Exit codes 137/140/143 identify signals, not a definitive OOM diagnosis. Check scheduler/task logs before escalating resources; a cancellation, timeout or application failure can look similar. Bound retries and resource requests.
318- `errorStrategy 'ignore'` lets the pipeline continue past a failed task; `'finish'` stops launching new tasks but lets running ones complete.
319- In nf-core, resource scaling lives in `conf/base.config` keyed on `process_*` labels (see `references/developing.md`).

Dynamic resources and error handling

Make pipelines robust by retrying failures with more resources instead of over-provisioning everything. task.attempt increments on each retry.

process BIG_JOB {
    label 'process_high'
    cpus   { 4 * task.attempt }
    memory { 8.GB * task.attempt }
    time   { 4.h * task.attempt }

    errorStrategy { task.exitStatus in [137, 140, 143] ? 'retry' : 'terminate' }
    maxRetries 3
    // ...
}
  • Exit codes 137/140/143 identify signals, not a definitive OOM diagnosis. Check scheduler/task logs before escalating resources; a cancellation, timeout or application failure can look similar. Bound retries and resource requests.
  • errorStrategy 'ignore' lets the pipeline continue past a failed task; 'finish' stops launching new tasks but lets running ones complete.
  • In nf-core, resource scaling lives in conf/base.config keyed on process_* labels (see references/developing.md).
Raw lines 300 to 320
321## Groovy essentials and gotchas
323- Strings: single-quoted are literal; double-quoted interpolate (`"${x}"`). In `script:` blocks, escape shell variables as `\$VAR`.
324- Define helper values with `def` inside `script:`/closures to avoid leaking globals.
325- Maps use Groovy syntax: `[ id: 'x', single_end: false ]`; access as `meta.id`.
326- **Common gotchas**:
327 - DSL2 allows a stream to feed multiple processes. The hazard is passing multiple independent queues into one process: completion order can mismatch samples. Join on an immutable sample key into one tuple channel; use `channel.value(...)` or `toList()` for a shared reference value.
328 - Legacy `groupTuple` without `size` buffers until the source closes. With `size`, it emits completed groups earlier and can drop incomplete groups unless `remainder: true`. Validate expected replicate/lane counts; never mistake a partial group for a complete sample.
329 - Legacy tuple `join` does not support duplicate keys. Set `failOnDuplicate`/`failOnMismatch` explicitly; for deliberate many-to-many matching use `combine(by:)` and validate cardinality. Do not assume parser mode alone supplies these checks.
330 - A full meta map used as a key must match exactly; extra/different fields can prevent joins. Keep sample keys immutable and check identities across branches.
331 - `channel.topic` consumers must not feed outputs back into the same topic: that cycle can hang the workflow.
332 - A process called twice without aliasing is an error; `include ... as`.
333 - Globs in `output:` match the **work directory**, not `publishDir`. Publishing is asynchronous; downstream processes must consume output channels. Prevent output filename collisions between samples.
334 - Prefer filtering channels over `when:` for clarity and caching.
335- **Strict syntax / language server**: Nextflow provides a VS Code extension + `nextflow lint` and a stricter parser; nf-core is migrating pipelines to it. Keep scripts to documented DSL2 constructs and avoid deprecated DSL1 idioms (`Channel.create()`, `.into{}` overuse, top-level `file()` for inputs).

Groovy essentials and gotchas

  • Strings: single-quoted are literal; double-quoted interpolate ("${x}"). In script: blocks, escape shell variables as \$VAR.
  • Define helper values with def inside script:/closures to avoid leaking globals.
  • Maps use Groovy syntax: [ id: 'x', single_end: false ]; access as meta.id.
  • Common gotchas:
  • DSL2 allows a stream to feed multiple processes. The hazard is passing multiple independent queues into one process: completion order can mismatch samples. Join on an immutable sample key into one tuple channel; use channel.value(...) or toList() for a shared reference value.
  • Legacy groupTuple without size buffers until the source closes. With size, it emits completed groups earlier and can drop incomplete groups unless remainder: true. Validate expected replicate/lane counts; never mistake a partial group for a complete sample.
  • Legacy tuple join does not support duplicate keys. Set failOnDuplicate/failOnMismatch explicitly; for deliberate many-to-many matching use combine(by:) and validate cardinality. Do not assume parser mode alone supplies these checks.
  • A full meta map used as a key must match exactly; extra/different fields can prevent joins. Keep sample keys immutable and check identities across branches.
  • channel.topic consumers must not feed outputs back into the same topic: that cycle can hang the workflow.
  • A process called twice without aliasing is an error; include ... as.
  • Globs in output: match the work directory, not publishDir. Publishing is asynchronous; downstream processes must consume output channels. Prevent output filename collisions between samples.
  • Prefer filtering channels over when: for clarity and caching.
  • Strict syntax / language server: Nextflow provides a VS Code extension + nextflow lint and a stricter parser; nf-core is migrating pipelines to it. Keep scripts to documented DSL2 constructs and avoid deprecated DSL1 idioms (Channel.create(), .into{} overuse, top-level file() for inputs).
Raw lines 321 to 335