3. Workflow定义语言(WDL)基础:语法结构、任务与工作流定义
好,咱们开始聊WDL。这个Workflow Definition Language,说白了就是Cromwell能听懂的一套剧本。你告诉它“先做A,再做B,如果C条件满足就做D”,全靠WDL来描述。我最初接触的时候,觉得它长得挺像函数式编程,后来发现它本质上是把生物信息学的管道拆成一个个小盒子。
WDL的核心理念就两个:task(任务)和workflow(工作流)。task是最小执行单元,定义“干什么”;workflow定义“怎么串联”。我个人习惯把task想象成一个个独立的集装箱,workflow就是吊车和传送带。
3.1 WDL文件的基本骨架
先看一个最简单的WDL长什么样:
version 1.0
workflow my_workflow {
call my_task
}
task my_task {
command {
echo "Hello, WDL"
}
runtime {
docker: "ubuntu:latest"
}
}
嗯,就这么几行。version声明WDL的方言版本,现在主流是1.0。workflow块包裹所有执行逻辑,task块定义具体要做的事。
我遇到过一个坑——版本号写错成draft-2,Cromwell直接报版本不兼容。你想想看,一个连版本都跑不对的WDL,后面全白搭。
3.2 Task定义详解
Task是WDL的肌肉。一条task通常包含以下部分:
| 指令 | 作用 | 是否必填 |
|---|---|---|
input |
声明输入参数、默认值、类型 | 选填(但建议写上) |
command |
要执行的shell命令或脚本 | 必填 |
output |
声明输出的文件/变量 | 必填(除非你不想拿结果) |
runtime |
容器镜像、CPU、内存、磁盘 | 强烈建议 |
meta |
作者描述、版本号、文档链接 | 选填 |
parameter_meta |
对每个输入参数的说明 | 选填 |
来看一个稍微完整的task:
task bwa_mem {
input {
File reference
File read1
File read2
String sample_name
Int threads = 4
}
command {
bwa mem -t ~{threads} ~{reference} ~{read1} ~{read2} \
> ~{sample_name}.sam
}
output {
File sam = "~{sample_name}.sam"
}
runtime {
docker: "biocontainers/bwa:v0.7.17"
cpu: 4
memory: "8 GB"
disks: "local-disk 20 HDD"
}
meta {
author: "Your Name"
description: "BWA MEM alignment step"
}
}
注意几个细节:~{}是变量插值,类似模板引擎。你可以在command里随便引用input里声明的变量。runtime里的disks是云端执行时必须的——Cromwell会按这个规格创建临时云磁盘。
我曾经犯过一个低级错误:output声明里文件路径写死了"output.sam",但实际命令生成的是sample.sam。Cromwell拿着没找到的文件,直接挂掉。后来我养成习惯,在output里也用变量:"~{sample_name}.sam"。
3.3 Workflow定义详解
Workflow的作用是编排task。它不自己干活,只告诉大家“谁先谁后、数据怎么传”。
workflow alignment_pipeline {
call bwa_mem as alignment {
input:
reference = genome_fasta,
read1 = sample_R1.fastq,
read2 = sample_R2.fastq,
sample_name = my_sample,
threads = 8
}
call samtools_sort {
input:
input_sam = alignment.sam,
sample_name = my_sample
}
output {
File final_bam = samtools_sort.sorted_bam
}
}
这里的关键是call语句。你可以理解成“调用一个任务实例”。as关键字重命名实例名,方便后面引用它的输出。input块就是给这个实例传参。
数据怎么在task之间流动?看这行:input_sam = alignment.sam。alignment是实例名,.sam是它output里的变量名。WDL会自动解析依赖关系,Cromwell知道该先跑bwa_mem,再跑samtools_sort。
3.4 输入类型与数据模式
WDL支持丰富的数据类型。用得最多的是这几类:
- 基本类型:
String,Int,Float,Boolean - 文件类型:
File,File?(可选文件),Array[File](文件列表) - 映射和结构体:
Map[String, String],Pair[File, String] - 对象:
Object或自定义struct
举个例子,处理多个样本时,你可以这么声明:
workflow multi_sample {
input {
Array[File] r1_files
Array[File] r2_files
Array[String] sample_names
}
scatter (i in range(length(r1_files))) {
call align_pair {
input:
r1 = r1_files[i],
r2 = r2_files[i],
sample = sample_names[i]
}
}
}
scatter是WDL里做并行最常用的结构。它会自动把数组展开成多个独立任务。我管它叫“隐式循环”——你写一次,Cromwell帮你并行成N份。
r1_files长度10,sample_names长度9。Cromwell不会自动截断或补全,它直接报数组越界。建议在workflow启动前做输入校验,或者用select_first等方法兜底。3.5 作用域与变量可见性
WDL的作用域规则比一般脚本语言简单:
- workflow级别的变量在所有call里可见(但需要显式传入input)
- task内部定义的变量只在本task可见
scatter块内定义的变量只在块内可见- 在workflow里引用task的输出,必须用
task_instance.output_name
我踩过的一个坑:在task的runtime里想用workflow的变量,结果直接引用导致解析失败。后来才发现runtime块里不能直接用workflow变量——你得通过input把变量传进task,才能在command和output里用。runtime只能写字面量或task input里的变量,这点很坑。
3.6 条件执行:if 与 select
WDL里条件逻辑用if语句实现:
workflow conditional_pipeline {
Boolean run_extra = true
call basic_analysis
if (run_extra) {
call extra_analysis {
input:
previous_result = basic_analysis.result
}
}
}
注意if块里的task只有在条件为true时才会调度。这很符合云端按需计费的逻辑——如果条件不满足,Cromwell连启动信号都不会发出。
select_first或if then else来做分支选择,而不是用嵌套if。WDL 1.0之后的if已经支持else了。3.7 WDL的整体结构图
下面这张图展示了WDL文件里各层级的关系:
3.7 小经验总结
写WDL这件事,上手很快,踩坑也很快。我概括几个原则:
- 变量插值用
~{},别用${}——那是shell的,WDL里不认识。 - output必须和命令生成的文件名一致,否则Cromwell找不到文件会直接失败。
- 尽量在每个task里写上runtime,尤其是docker镜像。云端执行没有镜像等于裸奔,环境不一致会哭死。
- Workflow里的output块别省。就算你只想看中间结果,写上才能暴露给外部API。
- 用
scatter做并行之前,确认你的任务本身是幂等的——并行副本之间不能有状态冲突。
java -jar cromwell.jar run my_workflow.wdl --inputs inputs.json跑一遍。Cromwell会在当前目录生成cromwell-executions文件夹,里面每个task的调用日志、stdout、stderr都在,翻一遍就知道哪里出的问题。