第一个Cromwell工作流:编写最简单的WDL,运行任务并查看结果
其实写第一个WDL工作流,比你想的要简单得多。我记得刚开始接触Cromwell时,最怕的就是那些复杂的配置文件。但后来发现,从最简单的Hello World开始,一步步来,完全没问题。这章咱们就动手写一个能跑的WDL,然后用Cromwell跑起来,最后看看结果长什么样。
WDL文件长什么样
一个WDL文件,说白了就两个核心块:workflow 和 task。workflow负责编排,task负责干活。打个比方,workflow是项目经理,task是具体干活的人。项目经理只负责喊"开始干活",具体怎么干、用什么工具,那是task的事。
我个人习惯把workflow和task写在同一个文件里,对于小项目这样最省事。下面这张图展示了WDL文件的基本骨架:
你看,结构就这么简单。WDL文件顶层只有workflow和task两种块。workflow里用call来调用task,task里写真正的命令和输出声明。理解了这个结构,后面写多步骤pipeline也就是在这个框架里加东西而已。
| 关键字 | 作用 | 出现位置 |
|---|---|---|
| version | 声明WDL规范版本 | 文件开头 |
| workflow | 定义工作流,编排任务调用顺序 | 顶层块 |
| task | 定义具体任务,包含命令和输出 | 顶层块 |
| call | 在workflow中调用某个task | workflow内部 |
| command | 要执行的shell命令 | task内部 |
| output | 声明任务的输出变量 | task内部 |
| runtime | 指定计算资源需求(CPU/内存等) | task内部 |
写一个能跑的WDL
理论说完了,直接上手写代码。下面这个例子,就是打印一句"Hello, Cromwell World!",然后把这句话作为输出捕获回来。麻雀虽小,五脏俱全。
version 1.0
workflow HelloWorld {
call SayHello
}
task SayHello {
command {
echo "Hello, Cromwell World!"
}
output {
String message = read_string(stdout())
}
}
来,逐行拆解一下:
- version 1.0 — 告诉Cromwell用1.0规范来解析这个文件。不写的话会报错,我刚开始就栽在这个坑里。
- workflow HelloWorld { ... } — 定义一个叫HelloWorld的工作流。花括号里写要调哪些task。
- call SayHello — 在workflow里调用SayHello这个task。注意顺序:先定义task,然后在workflow里call它。
- task SayHello { ... } — 定义具体任务。名字可以随便起,但建议和做的事情对应起来。
- command { ... } — 里面放真正要执行的shell命令。这里就是echo一句话。
- output { ... } — 声明输出变量。read_string(stdout()) 的意思是"读取标准输出,转成字符串"。
你想想看,这个WDL做完的事情就是:Cromwell启动一个容器或本地进程,执行echo,然后把打印的内容捕获成一个变量。就这么简单。
用Cromwell跑起来
文件写好了,怎么跑?如果你是用jar包方式安装的Cromwell,命令非常简单:
java -jar cromwell-85.jar run hello.wdl
如果你的Cromwell是装在服务器上的,也可能是这样:
cromwell run hello.wdl
跑起来之后,终端会刷刷刷地打印日志。你会看到类似这样的输出:
[2025-01-15 10:23:45 +08:00] Workflow HelloWorld submitted ...
[2025-01-15 10:23:46 +08:00] Starting workflow execution.
[2025-01-15 10:23:47 +08:00] Task SayHello: started.
[2025-01-15 10:23:47 +08:00] Task SayHello: stdout: Hello, Cromwell World!
[2025-01-15 10:23:47 +08:00] Workflow HelloWorld: completed successfully.
跑完去哪看结果
我第一次跑完Cromwell,找了半天输出文件。它不像普通脚本那样直接打印在屏幕上就完了,Cromwell会把所有输出写到磁盘上。默认会在当前目录生成一个 cromwell-executions 文件夹,里面的目录结构是这样的:
cromwell-executions/
└── HelloWorld/
└── a1b2c3d4-e5f6-7890-abcd-ef1234567890/
└── call-SayHello/
├── execution/
│ ├── script
│ ├── stdout
│ ├── stderr
│ └── rc
└── outputs/
└── 这个目录放你声明的输出文件
来,解释一下这些文件是干嘛的:
- script — Cromwell实际执行的脚本文件,里面就是command块里的命令。
- stdout — 标准输出,也就是echo打印的内容。
- stderr — 标准错误输出,如果命令报错,错误信息在这里。
- rc — 返回值(return code),0表示成功,非0表示出错。
打开stdout看一下:
Hello, Cromwell World!
而你在task里用output声明的 message 变量,Cromwell会把它写到工作流的元数据里。可以用这个命令查看:
java -jar cromwell-85.jar metadata a1b2c3d4-e5f6-7890-abcd-ef1234567890
输出里会有一段:
"outputs": {
"HelloWorld.SayHello.message": "Hello, Cromwell World!"
}
我踩过的几个坑
最后分享几个实际项目中遇到的坑,你碰到了可以少走弯路:
- 路径里有中文或空格 — Cromwell对文件路径的解析比较严格,中文或空格可能导致找不到文件。我建议工作目录和WDL文件名一律用英文+下划线。
- Java版本太低 — Cromwell 85+ 需要 Java 11 以上。如果你用Java 8,启动时会直接报错。可以用
java -version先确认一下。 - 忘了写 version — 不写version 1.0,Cromwell会用默认的老版本模式解析,有些语法会不一样。直接在第一行加上就完事。
- stdout 里有多行内容 — read_string(stdout()) 只会读取第一行。如果你的命令打印了多行,考虑用 read_lines(stdout()) 换成数组。
好了,第一个工作流你已经跑通了。从编写WDL、运行Cromwell到查看输出,整个流程走了一遍。后面的章节我们会在这个基础上,一步步搭出真正能用的多步骤pipeline。