341 lines
15 KiB
Markdown
341 lines
15 KiB
Markdown
---
|
|
title: "Nextflow Language (DSL2)"
|
|
task: ""
|
|
lineage_type: import
|
|
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/9c9bd2e9/skills/nextflow/references/language.md
|
|
upstream_sha: 9c9bd2e9
|
|
imported_at: 2026-06-27
|
|
prompt_class: prompt
|
|
upstream_changes: accepted
|
|
author: upstream
|
|
validated: false
|
|
---
|
|
|
|
# Nextflow Language (DSL2)
|
|
|
|
The complete Nextflow scripting language: 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). Source: https://www.nextflow.io/docs/latest/
|
|
|
|
**Current syntax conventions** (a strict-syntax parser, `NXF_SYNTAX_PARSER=v2`, is opt-in in 25.x and becomes the default in **26.04** — write to it now, it also runs on the legacy parser):
|
|
- **`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()`.
|
|
|
|
## Table of Contents
|
|
|
|
- [Script structure](#script-structure)
|
|
- [Processes](#processes)
|
|
- [Process directives](#process-directives)
|
|
- [Channels](#channels)
|
|
- [Operators](#operators)
|
|
- [Workflows](#workflows)
|
|
- [Modules](#modules)
|
|
- [Dynamic resources and error handling](#dynamic-resources-and-error-handling)
|
|
- [Groovy essentials and gotchas](#groovy-essentials-and-gotchas)
|
|
|
|
## Script structure
|
|
|
|
A Nextflow script (`.nf`) mixes process/workflow definitions with Groovy. Every script enables DSL2 implicitly (it is the default since 22.03). A run begins at the **unnamed `workflow {}`** block (the entry workflow).
|
|
|
|
```nextflow
|
|
#!/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)
|
|
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`.
|
|
|
|
## 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.
|
|
|
|
```nextflow
|
|
process ALIGN {
|
|
tag "$meta.id" // label shown in the log/trace
|
|
label 'process_high' // maps to resources in config
|
|
container 'quay.io/biocontainers/bwa:0.7.17--hed695b0_7'
|
|
publishDir "${params.outdir}/bam", mode: 'copy'
|
|
|
|
input:
|
|
tuple val(meta), path(reads) // a sample: metadata map + file(s)
|
|
path index // a shared reference (value channel)
|
|
|
|
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 $index ${reads} | samtools sort -o ${prefix}.bam
|
|
|
|
cat <<-END_VERSIONS > versions.yml
|
|
"${task.process}":
|
|
bwa: \$(bwa 2>&1 | sed -n 's/Version: //p')
|
|
END_VERSIONS
|
|
"""
|
|
|
|
stub:
|
|
"""
|
|
touch ${meta.id}.bam
|
|
touch versions.yml
|
|
"""
|
|
}
|
|
```
|
|
|
|
### Inputs
|
|
|
|
Declared one per line under `input:`. Each input consumes one item from a channel.
|
|
|
|
| Qualifier | Meaning |
|
|
|-----------|---------|
|
|
| `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 |
|
|
| `stdin` | Feed the channel item to the script's stdin |
|
|
| `each x` | Repeat the process once per value in `x` (combinatorial) |
|
|
|
|
Inputs are positional and matched to channels in call order: `ALIGN(reads_ch, index_ch)`.
|
|
|
|
### 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]`.
|
|
|
|
| Form | Meaning |
|
|
|------|---------|
|
|
| `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 x` | Emit a value computed in the process |
|
|
| `stdout` | Capture 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: name` | Named output channel (reference as `PROC.out.name`) |
|
|
| `..., topic: versions` | Also route this output to a named topic channel |
|
|
| `optional true` | Output may be absent without erroring |
|
|
|
|
### 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`).
|
|
|
|
```nextflow
|
|
process PY {
|
|
input: val x
|
|
output: stdout
|
|
script:
|
|
"""
|
|
#!/usr/bin/env python
|
|
print(${x} ** 2)
|
|
"""
|
|
}
|
|
```
|
|
|
|
### 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:` — an alternate minimal script run with `-stub-run` to test pipeline wiring without the real tool. nf-core requires stubs.
|
|
|
|
## Process directives
|
|
|
|
Set inside a process (or globally via config). Most-used:
|
|
|
|
| Directive | Purpose |
|
|
|-----------|---------|
|
|
| `cpus`, `memory`, `time`, `disk` | Resource requests (e.g. `memory '8.GB'`, `time '2.h'`) |
|
|
| `container` | Container image for this process |
|
|
| `conda` | Conda packages/env for this process |
|
|
| `publishDir` | Copy/link outputs to a results dir (`mode: 'copy'|'symlink'|'link'`) |
|
|
| `tag` | Human-readable label per task in logs/trace |
|
|
| `label` | Group processes (target with `withLabel:` in config) |
|
|
| `errorStrategy` | `'terminate'` (default), `'ignore'`, `'retry'`, `'finish'` |
|
|
| `maxRetries`, `maxErrors` | Retry limits |
|
|
| `cache` | `true`/`'lenient'`/`'deep'`/`false` — caching behavior |
|
|
| `scratch` | Run in node-local scratch then stage out |
|
|
| `stageInMode`/`stageOutMode` | `'symlink'`/`'copy'`/`'link'` staging |
|
|
| `beforeScript`/`afterScript` | Commands wrapping the task script |
|
|
| `accelerator` | GPU request (e.g. `accelerator 1, type: 'nvidia-tesla-v100'`) |
|
|
| `array` | Submit as a job array (HPC/cloud), e.g. `array 100` |
|
|
| `ext` | Free-form map (`ext.args`, `ext.prefix`) injected from config |
|
|
| `pod` | Kubernetes pod options |
|
|
| `module` | Load an HPC environment module |
|
|
| `maxForks` | Cap parallel tasks for this process |
|
|
|
|
Access the resolved values at runtime via `task.*` (`task.cpus`, `task.memory`, `task.attempt`, `task.process`, `task.ext.args`).
|
|
|
|
## 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. Can be consumed once.
|
|
- **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.
|
|
|
|
### Channel factories
|
|
|
|
```nextflow
|
|
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: arity:'1', type:'file', hidden:true
|
|
channel.fromFilePairs('data/*_{1,2}.fastq.gz') // -> [id, [r1, r2]] for paired reads
|
|
channel.topic('versions') // collect values emitted to a named topic (24.04+)
|
|
channel.empty() // emits nothing
|
|
```
|
|
|
|
> `channel.fromSRA(...)` exists but is deprecated as of 26.04 — prefer a **samplesheet** (`splitCsv`) over fetching reads by accession.
|
|
|
|
`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.
|
|
|
|
## Operators
|
|
|
|
Operators transform/combine channels. Chain with `.`; the dataflow graph is built from these connections.
|
|
|
|
| Operator | Purpose |
|
|
|----------|---------|
|
|
| `map { }` | Transform each item |
|
|
| `filter { }` | Keep items matching a condition/type/regex |
|
|
| `flatten` | Flatten nested emissions into individual items |
|
|
| `collect` | Gather all items into a single list (→ value channel) |
|
|
| `toList` / `toSortedList` | Collect into one (sorted) list |
|
|
| `groupTuple` | Group tuples by key (e.g. by `meta`) — often needs `groupTuple(by: 0)` |
|
|
| `join` | Inner-join two channels by a matching key |
|
|
| `combine` | Cartesian product (optionally `by:` a key) |
|
|
| `cross` | Combine matching keyed items |
|
|
| `mix` | Merge multiple channels into one stream |
|
|
| `concat` | Emit one channel fully, then the next, in order |
|
|
| `branch { }` | Route items into multiple named sub-channels by condition |
|
|
| `multiMap { }` | Emit to several channels from one pass |
|
|
| `splitCsv` / `splitText` / `splitFasta` / `splitFastq` | Split file contents into items |
|
|
| `collectFile` | Write items into one or more files |
|
|
| `unique` / `distinct` | De-duplicate |
|
|
| `first` / `last` / `take` / `until` | Select subsets |
|
|
| `set { ch }` | Name the resulting channel (alternative to `ch =`) |
|
|
| `view { }` | Print items for debugging (returns the channel unchanged) |
|
|
| `ifEmpty` | Provide a default if the channel is empty |
|
|
| `dump(tag:'x')` | Debug-print when run with `-dump-channels x` |
|
|
|
|
```nextflow
|
|
// Build the nf-core [meta, reads] shape from a samplesheet
|
|
channel
|
|
.fromPath(params.input)
|
|
.splitCsv(header: true)
|
|
.map { row -> tuple([id: row.sample, single_end: row.fastq_2 ? false : true],
|
|
row.fastq_2 ? [file(row.fastq_1), file(row.fastq_2)] : [file(row.fastq_1)]) }
|
|
.set { reads_ch }
|
|
|
|
// Group per-sample results, then join two channels by meta
|
|
counts.groupTuple()
|
|
.join(metadata) // matches on the first (key) element
|
|
.view()
|
|
```
|
|
|
|
## Workflows
|
|
|
|
A `workflow` composes processes and other workflows. The **unnamed** workflow is the entry point. **Named** workflows are reusable (sub)workflows.
|
|
|
|
```nextflow
|
|
workflow RNASEQ {
|
|
take: // typed inputs (channels)
|
|
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)
|
|
index = channel.value(file(params.index))
|
|
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):
|
|
|
|
```nextflow
|
|
workflow {
|
|
main:
|
|
ch = ANALYZE(input)
|
|
publish:
|
|
results = ch // name the published channel
|
|
}
|
|
output {
|
|
results { path 'analysis' } // -> <outputDir>/analysis (default outputDir: results/)
|
|
}
|
|
```
|
|
|
|
## Modules
|
|
|
|
Modules are `.nf` files whose processes/workflows are imported with `include`. This is the basis of nf-core's reusable components.
|
|
|
|
```nextflow
|
|
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).
|
|
|
|
## 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.
|
|
|
|
```nextflow
|
|
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 typically mean out-of-memory/walltime kills — retry with more resources.
|
|
- `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`).
|
|
|
|
## 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**:
|
|
- Re-using a consumed **queue** channel yields nothing — use a **value** channel (or `collect`) for things consumed by many tasks (like a reference index).
|
|
- `groupTuple` may emit before all items arrive unless sizes are known; provide `size:` or use `groupTuple(by:)` carefully.
|
|
- A process called twice without aliasing is an error; `include ... as`.
|
|
- Globs in `output:` match the **work directory**, not `publishDir`.
|
|
- Prefer filtering channels over `when:` for clarity and caching.
|
|
- **Strict syntax / language server**: recent Nextflow ships 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).
|