第一个Cromwell工作流:编写最简单的WDL,运行任务并查看结果

其实写第一个WDL工作流,比你想的要简单得多。我记得刚开始接触Cromwell时,最怕的就是那些复杂的配置文件。但后来发现,从最简单的Hello World开始,一步步来,完全没问题。这章咱们就动手写一个能跑的WDL,然后用Cromwell跑起来,最后看看结果长什么样。

WDL文件长什么样

一个WDL文件,说白了就两个核心块:workflow 和 task。workflow负责编排,task负责干活。打个比方,workflow是项目经理,task是具体干活的人。项目经理只负责喊"开始干活",具体怎么干、用什么工具,那是task的事。

我个人习惯把workflow和task写在同一个文件里,对于小项目这样最省事。下面这张图展示了WDL文件的基本骨架:

WDL 工作流核心结构 WDL 文件 workflow { ... } task { ... } call 任务名 command output 一个 WDL 文件 = workflow(编排) + task(执行) workflow 通过 call 调用 task,task 里写具体命令和输出

你看,结构就这么简单。WDL文件顶层只有workflow和task两种块。workflow里用call来调用task,task里写真正的命令和输出声明。理解了这个结构,后面写多步骤pipeline也就是在这个框架里加东西而已。

WDL 核心关键字速查
关键字作用出现位置
version声明WDL规范版本文件开头
workflow定义工作流,编排任务调用顺序顶层块
task定义具体任务,包含命令和输出顶层块
call在workflow中调用某个taskworkflow内部
command要执行的shell命令task内部
output声明任务的输出变量task内部
runtime指定计算资源需求(CPU/内存等)task内部
小提示:version 1.0 是目前最常用的WDL版本。如果你看到别人写的WDL没有version声明,那多半是老版本的写法。我建议新项目一律从version 1.0开始,兼容性最好。

写一个能跑的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.
关键信息:看到 "completed successfully" 就说明跑通了。如果报错,别慌,八成是路径问题或者Java版本不对。检查一下java -version是不是1.8以上。

跑完去哪看结果

我第一次跑完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!"
}
注意:如果你在task里忘了写output块,Cromwell虽然会把命令跑完,但你拿不到任何结果变量。我在生产环境里遇到过这种情况——任务跑完了,但下游任务等不到数据,卡死在那。检查了好几轮才发现是output没声明。所以,记得写output!

我踩过的几个坑

最后分享几个实际项目中遇到的坑,你碰到了可以少走弯路:

  • 路径里有中文或空格 — 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。