4. 第一个Nextflow流程:编写Hello World工作流,理解进程和输出

说实话,很多人学Nextflow的第一反应就是跑个Hello World。但你别小看这一步。我见过不少同事,把Nextflow当普通脚本写,结果跑完连“输出到底在哪”都搞不清楚。我们这章就把这个最简单的流程拆开来讲。你跟着我走一遍,后面写质控节点就顺了。

4.1 创建一个工作流文件

Nextflow的工作流本质就是一个.nf文件。里面的核心单元叫进程(process)。进程负责执行具体的任务,比如运行一个命令、调一个Python脚本。而工作流本身用来编排进程间的依赖关系。

我们新建一个文件,就叫hello.nf。我个人习惯把工作流文件和配置分开,但初学阶段,一个文件搞定就好。

4.2 最简单的进程——无输入无输出

先写一个极端简单的进程,只打印一句话。

process sayHello {
    script:
    """
    echo 'Hello, Nextflow!'
    """
}

workflow {
    sayHello()
}

保存后,在终端执行:

nextflow run hello.nf

你会看到终端输出类似:

N E X T F L O W  ~  version 24.10.0
Launching `hello.nf` [agitated_wozniak] DSL2 - revision: 1164db094b
executor >  local (1)
[a7/3b9c8e] process > sayHello (1) [100%] 1 of 1 ✔

Hello, Nextflow!

看到了吗?最下面一行就是我们echo的内容。但如果你以为这样就把工作流写完了,那就错了。这种进程没有任何输入和输出,在实际项目中几乎用不到。

注意: 进程名必须用小写字母开头,后面可以用字母、数字或下划线。如果写成 SayHello(首字母大写),Nextflow会直接报错。我当初就被这个坑过,排查了半天。

4.3 理解进程的输入/输出声明

刚才那个进程是“一次性”的。真实场景下,进程需要接收数据、产出结果。Nextflow通过input和output块来声明。

我们改一下:让进程接收一个字符串,然后打印它,再输出一个文件。

process greet {
    input:
    val greeting

    output:
    path "greeting.txt"

    script:
    """
    echo "$greeting" > greeting.txt
    """
}

workflow {
    greet("Hello from workflow!")
}

这里有几个关键点:

  • val greeting:定义一个值类型的输入,名为greeting。
  • path "greeting.txt":声明进程会生成一个文件,名字叫greeting.txt。
  • script块里的$greeting:通过美元符号引用输入变量,用法和bash一样。
  • 工作流调用时传参:greet("Hello from workflow!")。

运行一下:

nextflow run hello.nf

输出:

process > greet (1) [100%] 1 of 1 ✔

但这次你会在工作目录下发现一个greeting.txt文件。打开看,内容正是“Hello from workflow!”。

小技巧: Nextflow会把每个进程的执行结果放在work/目录下独立的文件夹里,文件名也是原样。但你不用直接去work/里找文件,后面我们会讲怎么通过publishDir把结果收集出来。

4.4 输出到底是什么?——理解通道(channel)

这才是我最想讲的部分。很多人把进程的输出理解为“文件”,没错,但不全面。实际上,进程的输出会变成一个通道(channel)。通道是Nextflow里数据流动的“管道”。

拿上面那个例子来说,greet进程的输出是一个通道,里面装着一个文件路径。我们可以把这个通道传给另一个进程。

我们再加一个进程,用来读取刚才生成的文件并打印内容:

process greet {
    input:
    val greeting

    output:
    path "greeting.txt"

    script:
    """
    echo "$greeting" > greeting.txt
    """
}

process readGreeting {
    input:
    path greeting_file

    output:
    stdout()

    script:
    """
    cat $greeting_file
    """
}

workflow {
    greeting_ch = greet("Hello from workflow!")
    readGreeting(greeting_ch)
}

看到没?greet的输出赋值给了greeting_ch(一个通道变量),然后直接传给readGreeting的输入。工作流内部就是通过通道把数据从一个进程“流”到另一个进程。

核心理解: 在Nextflow里,进程的output块声明的不是文件本身,而是通道的发射内容。每个输出声明都会在进程执行成功后,把生成的文件或值发射到对应的输出通道。工作流里用=来捕获这些通道。

4.5 知识结构总览

下面这张图总结了本章的核心逻辑关系:

第一个Nextflow流程核心结构 workflow 入口 greeting_ch = greet("Hello!") process greet input: val greeting → output: path "greeting.txt" 通道 greeting_ch 输出会变成一个通道,而不是直接返回文件路径本身 工作流通过 = 捕获输出通道,然后传递给下一个进程的 input

这张图的核心就一句话:进程的output变成通道,工作流用赋值拿到通道,再传给下一个进程。弄懂这个,Nextflow的流程编排你就掌握了30%。

4.6 常见的输入/输出类型

除了val和path,Nextflow还支持其他几种常用类型。我做了一个对照表:

声明方式含义示例典型用途
val x值类型,传一个变量val name样本ID、参数控制
path f文件路径,传一个或多个文件path reads测序数据、参考基因组
env VAR环境变量env PATH设置外部工具路径
stdout()捕获进程的标准输出stdout()从命令输出里提取结果
tuple(a,b)元组,组合多个值或文件tuple val(sample_id), path(bam)把ID和文件打包传递

在质控节点里,你最常用的是path和tuple。比如FastQC输入是fastq文件,输出是zip和html,通常用tuple把它们绑在一起。

4.7 理解工作目录与输出文件的位置

每次运行Nextflow,它都会在work/目录下生成一些子目录,里面放着每个进程的执行现场。包括脚本、输入文件的符号链接、输出文件、以及运行日志。你可以把它想象成每个进程的“小隔间”。

但实际项目中,我们不想去work/里翻文件。常见的做法是用publishDir指令把结果发布到指定目录。我给个示例:

process greet {
    publishDir "results", mode: 'copy'

    input:
    val greeting

    output:
    path "greeting.txt"

    script:
    """
    echo "$greeting" > greeting.txt
    """
}

workflow {
    greet("Publish this!")
}

加上publishDir后,进程执行完会自动把greeting.txt拷贝到项目根目录下的results/文件夹里。你还可以设置mode: 'link'创建硬链接或符号链接,节省磁盘空间。我个人习惯用copy,因为后续分析环境迁移时文件不会断链。

一个坑,我曾经踩过: publishDir默认只在进程成功结束后执行一次。如果你把同一个进程跑多次(比如并行处理多个样本),每个实例都会往同一个results/里写文件。如果输出文件名一样,后面的会覆盖前面的。解决方法是把样本ID等唯一标识放进输出文件名,或者用publishDir的pattern选项按规则分目录。我们后面讲质控节点时会细说。

4.8 本章小结:你应该带走的三个要点

  1. 进程必须有输入/输出声明才算完整。 无输入无输出的进程除了测试,基本用不上。
  2. 输出通道是数据流动的载体。 工作流通过=捕获进程的输出通道,然后传给下一个进程的input。这就是Nextflow的“工作流”本质。
  3. 善用publishDir管理结果。 不要自己去work/里翻,把关键的输出发到项目目录,后续处理也会更顺手。