4. 第一个Nextflow工作流:Hello World
终于要动手写第一个真正的Nextflow工作流了。说实话,我第一次接触Nextflow时,心里想的是“又一个命令行工具?”结果一上手才发现,这东西的思维方式和普通的Shell脚本完全不一样。
咱们今天就从一个最简单的例子入手——打印“Hello World”。别看它简单,它背后的流程定义、通道传递、进程执行,这些概念以后会反复用到。我建议你把这段代码亲手敲一遍,一边敲一边感受Nextflow的节奏。
4.1 工作流的基本骨架
Nextflow的工作流本质上是一个DSL(领域特定语言)脚本,默认用.nf后缀。一个最小的可执行工作流长这样:
#!/usr/bin/env nextflow
nextflow.enable.dsl=2
process sayHello {
output:
stdout
script:
"""
echo 'Hello World!'
"""
}
workflow {
sayHello()
}
来解释一下每部分在干什么。
- 第一行 shebang:告诉系统用Nextflow来执行这个脚本。虽然不加也能跑,但我习惯加上,尤其是在集群上提交作业时能避免不少麻烦。
nextflow.enable.dsl=2:启用DSL2语法。Nextflow有DSL1和DSL2两套语法,DSL2是当前推荐版本,更清晰、更灵活。你以后看到的所有示例,我都会用DSL2。process sayHello:定义一个进程,名字叫sayHello。process是Nextflow的核心单元,每个进程独立执行一个任务。output: stdout:声明这个进程的输出是标准输出。stdout是个特殊变量,代表进程的标准输出流。script::下面跟着的是一段Shell脚本,Nextflow会自动把它包装成一个任务去执行。字符串可以用单引号或双引号,区别在于双引号支持变量替换。workflow { }:DSL2的入口块,定义了工作流的执行顺序。这里我们只调用了一个进程sayHello()。
注意:在DSL2中,workflow块只能有一个,且必须放在所有process定义之后。这在Nextflow 22.04版本之后是强制要求。
4.2 运行你的第一个工作流
保存上面的代码为hello.nf,然后在终端执行:
nextflow run hello.nf
你会看到类似这样的输出:
N E X T F L O W ~ version 24.04.2
Launching `hello.nf` [reverent_tesla] DSL2 - revision: 6249bf1ee0
executor > local (1)
[20/f1e36e] sayHello | 1 of 1 ✔
Hello World!
嗯,这里要注意几件事:
- 任务标识符:
[20/f1e36e]是这次任务的任务ID,由一段哈希和随机字符串组成。每个任务都有唯一ID,用于日志、缓存和调度。 - 执行状态:
1 of 1 ✔表示成功执行了1个任务,总共1个。绿色对号表示成功。 - 输出:最后一行就是我们要的“Hello World!”。
为什么它叫“工作流”而不是“脚本”?因为Nextflow会为每个进程创建一个独立的工作目录(在work/目录下),在里面执行你的Shell命令,并管理输入输出文件流。这种隔离机制保证了可重复性——即使中途崩溃,重新运行时也能从缓存里恢复。
小技巧:如果你想看到更多细节,加上-with-report或-with-trace参数。我个人调试时常用-process.echo,这样进程的标准输出会直接显示在终端,方便观察中间结果。
4.3 让Hello World更“工作流”一点
只打印一句话实在太单调了。我们加点输入和输出,让它看起来像个正经的工作流。修改代码如下:
#!/usr/bin/env nextflow
nextflow.enable.dsl=2
params.greeting = "Hello"
params.target = "World"
process sayHello {
input:
val greeting
val target
output:
val message
script:
message = "${greeting}, ${target}!"
"""
echo "${message}"
"""
}
workflow {
message = sayHello(params.greeting, params.target)
message.view()
}
这里新引入了几个概念:
params:参数定义。可以在脚本顶部统一声明,运行时通过--greeting 'Hi'覆盖。我建议把所有可配置参数都放在这里,方便以后修改。input::声明进程的输入。这里用了val类型,代表标量值(字符串、数字等)。常见的还有path(文件路径)和tuple(元组)。output::这里输出的是一个val message,意思是进程结束后会发出一个包含消息的对象。message.view():view()是通道操作符,它会输出通道中每个元素到终端。实际上,message现在是一个通道(channel),里面装着输出值。
运行:
nextflow run hello.nf --greeting '你好' --target 'Nextflow'
输出:
你好, Nextflow!
看到没有?通过参数控制行为,工作流的灵活性一下就体现出来了。我在实际项目中经常遇到这种情况:同一个流程,只需要改几个参数就能跑不同样本,根本不用改代码。
4.4 理解通道(channel)与数据流
刚才的message变量其实是一个通道。你可以把通道想象成一个“数据管道”——每个进程从管道里取数据,处理完后再把结果放回管道里。Nextflow的整个工作流就是由这些通道连接起来的。
下面的SVG图展示了我们这个Hello World工作流的数据流动:
这个图看起来简单,但它的模式会反复出现在你以后写的所有工作流中:输入 → 进程 → 通道 → 输出。当你处理几十个样本时,每个样本就是一条独立的数据流,Nextflow会自动把它们并行跑起来。
4.5 关于日志和缓存的小坑
我第一次跑Nextflow时,把输出结果直接打印到终端就以为完事了。结果项目经理让我提供“每个样本的处理日志”,我才发现默认情况下Nextflow会把所有任务的日志写到work/目录下。如果没用参数指定结果文件路径,那些中间文件很快就会丢失。
给个避坑指南:
| 场景 | 我踩过的坑 | 正确做法 |
|---|---|---|
| 输出文件管理 | 默认work/目录会被自动清理,或者被下次运行覆盖 |
在进程里用publishDir指定输出目录,如publishDir "$PWD/results", mode: 'copy' |
| 参数传递 | 把参数写死在script:里,导致样本多时改一次跑一天 |
所有样本相关的路径、阈值都通过input:从通道传入 |
| 缓存失效 | 改了脚本但忘记改进程名,缓存重用旧结果,怎么跑都不更新 | 进程名或输入文件的校验和变化时,Nextflow会自动让缓存失效;但如果只是改了脚本里的逻辑,记得加个版本标记 |
重要警告:千万不要在生产环境中依赖work/目录存储最终结果。这个目录是运行时临时工作区,随时可能被nextflow clean命令或磁盘空间不足而删除。所有重要结果必须显式复制或移动到指定输出路径。
4.6 从Hello到生产:一个心态上的转变
你可能觉得这个Hello World太基础了。但请你想想看:当你面对上百个FastQ文件、几十个质控步骤时,你的工作流本质上还是这种“输入→进程→输出”的循环。只是输入变成了文件通道,进程变成了BWA、FastQC、MultiQC,输出变成了比对结果和质控报告。
我刚开始带团队时,有个同事总是用Shell for循环跑样本,每次失败了都要手动重跑所有。后来换用Nextflow,改一下参数,-resume一敲,只重跑失败的样本。那种感觉,嗯,真香。
好,第一个工作流就到这里。接下来你需要做的是:把今天这段代码多跑几遍,改改参数,看看work/目录下生成了什么文件。你亲手跑过一遍,理解就会完全不同。