Skip to content

Commit e8cce6d

Browse files
committed
Add topic-channels tutorial
Signed-off-by: Ben Sherman <bentshermann@gmail.com>
1 parent 36899ce commit e8cce6d

4 files changed

Lines changed: 204 additions & 45 deletions

File tree

.docusaurus_site/sidebars.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ module.exports = {
302302
"tutorials/workflow-outputs",
303303
"tutorials/static-types",
304304
"tutorials/static-types-operators",
305+
"tutorials/topic-channels",
305306
"tutorials/metrics",
306307
"tutorials/flux"
307308
]

docs/process-typed.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,8 @@ process cat {
361361

362362
Topic emissions can use the same [output functions][process-reference-typed] as the `output:` section.
363363

364+
See [Collecting values with topic channels][tutorial-topic-channels] for a practical example.
365+
364366
## Script
365367

366368
The `script:` and `exec:` sections behave the same way as [legacy processes][process-script].
@@ -382,3 +384,4 @@ Directives behave the same way as [legacy processes][process-directives].
382384
[static-typing-page]: ./static-typing
383385
[stdlib-types]: ./reference/stdlib-types
384386
[syntax-process-typed]: ./reference/syntax#process-typed
387+
[tutorial-topic-channels]: ./tutorials/topic-channels

docs/reference/stdlib-namespaces/channel.mdx

Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -333,56 +333,13 @@ Y
333333

334334
<AddedInVersion version="25.04" />
335335

336-
:::note
337-
This feature was previewed in versions 24.04 and 24.10 with the `nextflow.preview.topic` feature flag.
338-
:::
339-
340-
A *topic channel* is a channel that can receive values from many sources *implicitly* based on a matching *topic name*.
341-
342-
A typed process can emit values to a topic using the `topic:` section:
343-
344-
```nextflow
345-
nextflow.enable.types = true
346-
347-
process hello {
348-
topic:
349-
file('hello.txt') >> 'my-topic'
350-
351-
// ...
352-
}
353-
354-
process bye {
355-
topic:
356-
file('bye.txt') >> 'my-topic'
357-
358-
// ...
359-
}
360-
```
361-
362-
A legacy process can assign outputs in the `output:` section to a topic using the `topic` option:
363-
364-
```nextflow
365-
process hello {
366-
output:
367-
path('hello.txt'), topic: 'my-topic'
368-
369-
// ...
370-
}
371-
```
372-
373-
The `channel.topic` factory returns the topic channel for the given name:
336+
Get the *topic channel* for the given *topic name*:
374337

375338
```nextflow
376339
channel.topic('my-topic').view()
377340
```
378341

379-
The above example emits all values sent to the `my-topic` topic from processes such as `hello` and `bye`.
380-
381-
This approach is a convenient way to collect related items from many different sources without explicitly connecting them (e.g. using the `mix` operator).
382-
383-
:::warning
384-
Any process that consumes a topic channel (directly or indirectly) should not send any outputs to that topic, or else the pipeline will hang forever.
385-
:::
342+
See [Collecting values with topic channels][tutorial-topic-channels] for more information.
386343

387344
##### `value( value: V ) -> Value<V>` {#value}
388345

@@ -432,4 +389,7 @@ The `watchPath` factory only works with local and shared filesystems. It does no
432389
[glob]: http://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob
433390
[operator-take]: ../operator#take
434391
[operator-until]: ../operator#until
392+
[process-topic-option]: ../process#topic-name
393+
[process-typed-topics]: ../../process-typed#topics
435394
[static-types-samplesheet]: ../../tutorials/static-types#loading-a-samplesheet-input
395+
[tutorial-topic-channels]: ../../tutorials/topic-channels

docs/tutorials/topic-channels.mdx

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
---
2+
title: Collecting values with topic channels
3+
description: Use topic channels to collect values such as tool versions from many processes without connecting them explicitly.
4+
---
5+
6+
# Collecting values with topic channels
7+
8+
A *topic channel* is a channel that receives values from many processes *implicitly*, based on a matching *topic name*. Instead of connecting a process output to a channel in the workflow body, a process declares the topic that its output belongs to, and any workflow can read the entire topic with the [channel.topic][channel-topic] factory.
9+
10+
Topics are useful when a pipeline needs to collect the same kind of value from processes throughout the pipeline. The canonical example is tool versions: every process reports the version of the tool that it ran, and the pipeline collates these versions into a single report.
11+
12+
This tutorial demonstrates how to replace version-tracking boilerplate with a `versions` topic, using the [nf-core/rnaseq](https://github.com/nf-core/rnaseq) pipeline as an example (simplified for brevity).
13+
14+
:::note
15+
Topic channels are stable in Nextflow 25.04. They were previewed in versions 24.04 and 24.10 with the `nextflow.preview.topic` feature flag.
16+
:::
17+
18+
## The problem: version plumbing
19+
20+
By convention, each nf-core module writes a `versions.yml` file and emits it as a process output:
21+
22+
```nextflow
23+
process STAR_ALIGN {
24+
input:
25+
tuple val(meta), path(reads)
26+
path(index)
27+
28+
output:
29+
tuple val(meta), path('*.bam'), emit: bam
30+
path 'versions.yml', emit: versions
31+
32+
script:
33+
"""
34+
STAR --genomeDir ${index} --readFilesIn ${reads}
35+
36+
cat <<-END_VERSIONS > versions.yml
37+
"${task.process}":
38+
star: \$(STAR --version | sed 's/STAR_//')
39+
END_VERSIONS
40+
"""
41+
}
42+
```
43+
44+
Because each `versions.yml` file is a regular process output, every workflow that invokes a process must collect it by hand:
45+
46+
```nextflow
47+
workflow ALIGN_STAR {
48+
take:
49+
reads
50+
index
51+
52+
main:
53+
ch_versions = channel.empty()
54+
55+
STAR_ALIGN(reads, index)
56+
ch_versions = ch_versions.mix(STAR_ALIGN.out.versions)
57+
58+
BAM_SORT_STATS_SAMTOOLS(STAR_ALIGN.out.bam)
59+
ch_versions = ch_versions.mix(BAM_SORT_STATS_SAMTOOLS.out.versions)
60+
61+
// ... one mix for every process ...
62+
63+
emit:
64+
bam = BAM_SORT_STATS_SAMTOOLS.out.bam
65+
versions = ch_versions
66+
}
67+
```
68+
69+
Each subworkflow must then emit its `versions` channel so that its caller can mix it in turn, all the way up to the entry workflow. The result is a channel that has nothing to do with the dataflow of the pipeline, but appears in every process and workflow.
70+
71+
## Sending values to a topic
72+
73+
Replace `emit: versions` with `topic: versions`:
74+
75+
```nextflow
76+
process STAR_ALIGN {
77+
// ...
78+
79+
output:
80+
tuple val(meta), path('*.bam'), emit: bam
81+
path 'versions.yml', topic: versions
82+
83+
// ...
84+
}
85+
```
86+
87+
Delete all of the version plumbing in the workflow:
88+
89+
```nextflow
90+
workflow ALIGN_STAR {
91+
take:
92+
reads
93+
index
94+
95+
main:
96+
STAR_ALIGN(reads, index)
97+
BAM_SORT_STATS_SAMTOOLS(STAR_ALIGN.out.bam)
98+
99+
emit:
100+
bam = BAM_SORT_STATS_SAMTOOLS.out.bam
101+
}
102+
```
103+
104+
Read the topic channel directly from the entry workflow. It emits the `versions.yml` files from every process in the run, no matter how deeply nested the process was invoked:
105+
106+
```nextflow
107+
workflow {
108+
main:
109+
// ...
110+
111+
channel.topic('versions')
112+
.unique()
113+
.collectFile(
114+
storeDir: "${params.outdir}/pipeline_info",
115+
name: 'nf_core_rnaseq_software_mqc_versions.yml'
116+
)
117+
}
118+
```
119+
120+
## Using `eval` outputs
121+
122+
The `versions.yml` boilerplate can be simplified further by using an [eval output][process-eval-output] to capture the tool version as a value instead of a file:
123+
124+
```nextflow
125+
process STAR_ALIGN {
126+
input:
127+
tuple val(meta), path(reads)
128+
path(index)
129+
130+
output:
131+
tuple val(meta), path('*.bam'), emit: bam
132+
tuple val('star'), eval('STAR --version | sed "s/STAR_//"'), topic: versions
133+
134+
script:
135+
"""
136+
STAR --genomeDir ${index} --readFilesIn ${reads}
137+
"""
138+
}
139+
```
140+
141+
A [typed process][process-typed] declares topic emissions in the `topic:` section, which keeps them separate from the process outputs:
142+
143+
```nextflow
144+
nextflow.enable.types = true
145+
146+
process STAR_ALIGN {
147+
input:
148+
tuple(meta: Map, reads: List<Path>)
149+
index: Path
150+
151+
output:
152+
tuple(meta, file('*.bam'))
153+
154+
topic:
155+
tuple('star', eval('STAR --version | sed "s/STAR_//"')) >> 'versions'
156+
157+
script:
158+
"""
159+
STAR --genomeDir ${index} --readFilesIn ${reads}
160+
"""
161+
}
162+
```
163+
164+
Each topic emission is a `(name, version)` tuple, so the entry workflow can format the report however it likes:
165+
166+
```nextflow
167+
workflow {
168+
main:
169+
// ...
170+
171+
channel.topic('versions')
172+
.unique()
173+
.map { name, version -> "${name}: ${version}" }
174+
.collectFile(
175+
storeDir: "${params.outdir}/pipeline_info",
176+
name: 'nf_core_rnaseq_software_mqc_versions.yml',
177+
newLine: true,
178+
sort: true
179+
)
180+
}
181+
```
182+
183+
## Guidelines
184+
185+
Topic channels trade explicit wiring for convenience, so keep the following in mind:
186+
187+
- **A topic must not be consumed by a process that sends to it.** Any process that consumes a topic channel, directly or indirectly, must not send any outputs to that topic, or else the pipeline will hang forever, because the process would be waiting on a channel that it feeds.
188+
189+
- **Emission order is not deterministic.** Values arrive in the order that tasks complete. Use `unique()` or `Iterable::toSorted()` when the collected output must be stable across runs.
190+
191+
- **Topic connections are invisible in the workflow body.** Nothing in the workflow shows where the values in a topic come from. Reserve topics for cross-cutting concerns such as versions and telemetry, and use explicit channels for the dataflow of the pipeline.
192+
193+
[channel-topic]: ../reference/stdlib-namespaces/channel#topic
194+
[process-eval-output]: ../process#eval-output-eval
195+
[process-typed]: ../process-typed

0 commit comments

Comments
 (0)