2.10. ワークフロー#
Workflowは、CommandLineTool、ExpressionTool、またはWorkflow(サブワークフロー)をステップ として実行する CWL 処理単位です。CWL定義にはinputs
、outputs
、steps
を定義する必要があります。
CWLワークフローです。#
CWL定義echo-uppercase.cwl
は、CommandLineTool、および先の例で示したExpressionToolを実行するワークフローを定義しています。
echo-uppercase.cwl
#cwlVersion: v1.2
class: Workflow
requirements:
InlineJavascriptRequirement: {}
inputs:
message: string
outputs:
out:
type: string
outputSource: uppercase/uppercase_message
steps:
echo:
run: echo.cwl
in:
message: message
out: [out]
uppercase:
run: uppercase.cwl
in:
message:
source: echo/out
out: [uppercase_message]
CommandLineToolやExpressionToolは、Workflowと同じCWL定義に直接記述することも可能です。例えば、echo-uppercase.cwl
ワークフローを1つのファイルとして書き換えることができます:
echo-uppercase-single-file.cwl
#cwlVersion: v1.2
class: Workflow
requirements:
InlineJavascriptRequirement: {}
inputs:
message: string
outputs:
out:
type: string
outputSource: uppercase/uppercase_message
steps:
echo:
run:
class: CommandLineTool
baseCommand: echo
stdout: output.txt
inputs:
message:
type: string
inputBinding: {}
outputs:
out:
type: string
outputBinding:
glob: output.txt
loadContents: true
outputEval: $(self[0].contents)
in:
message: message
out: [out]
uppercase:
run:
class: ExpressionTool
requirements:
InlineJavascriptRequirement: {}
inputs:
message: string
outputs:
uppercase_message: string
expression: |
${ return {"uppercase_message": inputs.message.toUpperCase()}; }
in:
message:
source: echo/out
out: [uppercase_message]
ファイルを分けることは、モジュール化やコードの整理に役立ちます。しかし、開発のためにすべてを1つのファイルに書いておくと便利なことがあります。他にも複数のファイルを1つのファイルにまとめる方法があり(例:cwltool --pack
)このユーザーガイドの他のセクションで詳しく説明します。
注釈
サブワークフロー使うにはRequirementの1つである、SubworkflowFeatureRequirement
を有効にする必要があります。このユーザーガイドの別のセクションで、より詳しく説明しています。
2.10.1. ワークフローを書く#
このワークフローでは、tarファイルからJavaのソースファイルを取り出し、コンパイルします。
1st-workflow.cwl
##!/usr/bin/env cwl-runner
cwlVersion: v1.2
class: Workflow
inputs:
tarball: File
name_of_file_to_extract: string
outputs:
compiled_class:
type: File
outputSource: compile/classfile
steps:
untar:
run: tar-param.cwl
in:
tarfile: tarball
extractfile: name_of_file_to_extract
out: [extracted_file]
compile:
run: arguments.cwl
in:
src: untar/extracted_file
out: [classfile]
Runの入力データを記述するために、別ファイルのYAMLまたはJSONオブジェクトを使用します:
1st-workflow-job.yml
#tarball:
class: File
path: hello.tar
name_of_file_to_extract: Hello.java
次に、サンプルとなるJavaファイルを作成し、コマンドラインツールで使用するためにtarファイルに追加します。
$ echo "public class Hello {}" > Hello.java && tar -cvf hello.tar Hello.java
Hello.java
ここで、コマンドラインにツール定義と入力オブジェクトを指定して、cwltool
を起動します:
$ cwltool 1st-workflow.cwl 1st-workflow-job.yml
INFO /opt/hostedtoolcache/Python/3.9.19/x64/bin/cwltool 3.1.20240508115724
INFO Resolved '1st-workflow.cwl' to 'file:///home/runner/work/user_guide/user_guide/src/_includes/cwl/workflows/1st-workflow.cwl'
INFO [workflow ] start
INFO [workflow ] starting step untar
INFO [step untar] start
INFO [job untar] /tmp/4n4_5fwz$ tar \
--extract \
--file \
/tmp/q6qv1pzc/stg6b17059c-878b-4b09-af1e-a90a419c0e51/hello.tar \
Hello.java
INFO [job untar] completed success
INFO [step untar] completed success
INFO [workflow ] starting step compile
INFO [step compile] start
INFO [job compile] /tmp/y4hm2pka$ docker \
run \
-i \
--mount=type=bind,source=/tmp/y4hm2pka,target=/iTZQSi \
--mount=type=bind,source=/tmp/cahdndsh,target=/tmp \
--mount=type=bind,source=/tmp/4n4_5fwz/Hello.java,target=/var/lib/cwl/stg73d20f81-0007-4536-abab-4a3e46676cf2/Hello.java,readonly \
--workdir=/iTZQSi \
--read-only=true \
--net=none \
--user=1001:127 \
--rm \
--cidfile=/tmp/ipj3n_kt/20240518114656-085539.cid \
--env=TMPDIR=/tmp \
--env=HOME=/iTZQSi \
openjdk:9.0.1-11-slim \
javac \
-d \
/iTZQSi \
/var/lib/cwl/stg73d20f81-0007-4536-abab-4a3e46676cf2/Hello.java
INFO [job compile] completed success
INFO [step compile] completed success
INFO [workflow ] completed success
{
"compiled_class": {
"location": "file:///home/runner/work/user_guide/user_guide/src/_includes/cwl/workflows/Hello.class",
"basename": "Hello.class",
"class": "File",
"checksum": "sha1$39e3219327347c05aa3e82236f83aa6d77fe6bfd",
"size": 419,
"path": "/home/runner/work/user_guide/user_guide/src/_includes/cwl/workflows/Hello.class"
}
}INFO Final process status is success
どうなっているのでしょう? 分解してみましょう:
cwlVersion: v1.0
class: Workflow
cwlVersion
フィールドは、この文書が使用するCWL仕様のバージョンを示します。 class
フィールドは、この文書がワークフローを記述していることを示します。
inputs:
tarball: File
name_of_file_to_extract: string
inputs
セクションでは、ワークフローの入力について記述します。 これは入力パラメータのリストであり、各パラメータは識別子とデータ型から構成されます。 これらのパラメータは、特定のワークフロー・ステップへの入力として使用できます。
outputs:
compiled_class:
type: File
outputSource: compile/classfile
outputs
セクションでは、ワークフローの出力について記述します。 これは出力パラメータのリストであり、各パラメータは識別子とデータ型から構成されます。 outputSource
は、compile
ステップの出力パラメータclassfile
を、ワークフロー出力パラメータcompiled_class
とします。
steps:
untar:
run: tar-param.cwl
in:
tarfile: tarball
extractfile: name_of_file_to_extract
out: [extracted_file]
steps
セクションでは、ワークフローの実際のステップを説明しています。 この例では、最初のステップで tar ファイルからファイルを抽出し、2 番目のステップで java コンパイラを使用して最初のステップで抽出されたファイルをコンパイルしています。ワークフローのステップは、必ずしもリストされた順序で実行されるわけではなく、ステップ間の依存関係によって順序が決定されます(source
を使用)。 また、互いに依存しないワークフロー・ステップは、並行して実行されることがあります。
最初のステップであるuntar
は、tar-param.cwl
を実行します(パラメータ参照で以前説明しました)。このツールは、2つの入力パラメータ、tarfile
とextractfile
と1つの出力パラメータextracted_file
を持ちます。
ワークフローステップのin
セクションは、これらの2つの入力パラメータを、source
を使用してワークフローの入力であるtarball
およびname_of_file_to_extract
に接続しています。 つまり、ワークフローステップが実行されると、tarball
とname_of_file_to_extract
に割り当てられた値が、ツールを実行するためにtarfile
とextractfile
のパラメータに使用されることになり ます。
ワークフローステップのout
セクションには、ツールから期待される出力パラメータが記載されています。
compile:
run: arguments.cwl
in:
src: untar/extracted_file
out: [classfile]
第2ステップ compile
は、第1ステップの結果に依存し、入力パラメータsrc
をuntar
の出力パラメータに接続し、untar/extracted_file
を使用します。 これは、arguments.cwl
(以前に 追加の引数とパラメータで説明したものです) を実行します。このステップの出力classfile
は、前述のワークフローのoutputs
セクションに接続されています。
2.10.2. ネストされたワークフロー#
ワークフローは、複数のツールを組み合わせてより大きな処理を実行するための方法です。ワークフローエンジンがSubworkflowFeatureRequirement
をサポートしている場合、CWL ワークフローを別の CWL ワークフローのステップにできます :
requirements:
SubworkflowFeatureRequirement: {}
1st-workflow.cwl
をネストしたワークフローの例を紹介します:
nestedworkflows.cwl
##!/usr/bin/env cwl-runner
cwlVersion: v1.2
class: Workflow
inputs: []
outputs:
classout:
type: File
outputSource: compile/compiled_class
requirements:
SubworkflowFeatureRequirement: {}
steps:
compile:
run: 1st-workflow.cwl
in:
tarball: create-tar/tar_compressed_java_file
name_of_file_to_extract:
default: "Hello.java"
out: [compiled_class]
create-tar:
in: []
out: [tar_compressed_java_file]
run:
class: CommandLineTool
requirements:
InitialWorkDirRequirement:
listing:
- entryname: Hello.java
entry: |
public class Hello {
public static void main(String[] argv) {
System.out.println("Hello from Java");
}
}
inputs: []
baseCommand: [tar, --create, --file=hello.tar, Hello.java]
outputs:
tar_compressed_java_file:
type: File
streamable: true
outputBinding:
glob: "hello.tar"
注釈
Visualization of the workflow and the inner workflow from its `compile` step
この2ステップのワークフローは、create-tar
ステップから始まり、オレンジ色のcompile
ステップに接続されています。compile
は、右図にある別のワークフローです。紫色では、固定文字列"Hello.java"
がname_of_file_to_extract
として与えられているのがわかります。
CWLWorkflow
は、CommandLineTool
をstep
として使用することができ、その CWL ファイルはrun
に記述されます。ワークフローの入力(tarball
およびname_of_file_to_extract
)と出力(compiled_class
)は、ステップの入力/出力になるようにマッピングできます。
compile:
run: 1st-workflow.cwl
in:
tarball: create-tar/tar_compressed_java_file
name_of_file_to_extract:
default: "Hello.java"
out: [compiled_class]
1st-workflow.cwl
はワークフロー入力でパラメータ化されているため、実行時には tar ファイルと*.java
ファイル名を示すジョブファイルを提供する必要がありました。これは、複数の親ワークフローや、同じワークフロー内の複数のステップで再利用できることを意味するため、一般的にベストプラクティスです。
ここでは、default:
を使って、"Hello.java"
をname_of_file_to_extract
input として固定しています。しかし、このワークフローではtarball
の tar ファイルも必要です。これはcreate-tar
ステップで用意します。この時点で、1st-workflow.cwl
、より具体的な入力/出力名を持つようにリファクタリングするのがよいでしょう。これらは、ツールとしての使用法にも現れるからです。
また、あまり一般的でない方法で、ジョブファイルの外部依存を回避することも可能です。そこで、このワークフローでは、先に述べたInitialWorkDirRequirement
の要件を使用して、ハードコードされたHello.java
ファイルを生成してから、それを tar ファイルに追加できます。
create-tar:
requirements:
InitialWorkDirRequirement:
listing:
- entryname: Hello.java
entry: |
public class Hello {
public static void main(String[] argv) {
System.out.println("Hello from Java");
}
}
この場合、ステップではパラメータ化するのではなく、Hello.java
を想定することができるので、baseCommand
に直接書かれた値hello.tar
とHello.java
を使用でき、結果としてoutputs
が得られます:
run:
class: CommandLineTool
inputs: []
baseCommand: [tar, --create, --file=hello.tar, Hello.java]
outputs:
tar_compressed_java_file:
type: File
streamable: true
outputBinding:
glob: "hello.tar"
tar --create
ツールを別のファイルに分割せず、CWL Workflow 定義の中に埋め込んだことにお気づきでしょうか。これは一般的にベストプラクティスではありません。なぜなら、そのツールを再利用できないからです。このケースでそれを行う理由は、コマンドラインに、このワークフロー定義内でしか意味をなさないファイル名がハードコーディングされているためです。
この例では、外部にtarファイルを用意する必要がありました。これは内部ワークフローがそれを入力として受け取るように設計されているためです。内部ワークフローのリファクタリングとしては、コンパイルするjavaファイルのリストを受け取るようにすれば、他のワークフローにおいてもツールステップとしての使い方が簡単になります。
ネストされたワークフローは、より高機能で再利用可能なワークフローユニットを生成する強力な機能ですが、CWLツール定義の作成と同様に、複数のワークフローでの使い勝手を向上させるための配慮が必要です。
2.10.3. Scatter ステップ#
ワークフローの書き方がわかったので、ScatterFeatureRequirement
を利用し始めることができます。この機能は、ツールやワークフローを入力のリストに対して複数回実行したいことをrunnerに伝えます。ワークフローは、入力を配列として受け取り、配列の各要素をあたかも1つの入力であるかのように、指定されたステップを実行します。これにより、多くの異なるコマンドや入力 yaml ファイルを生成することなく、複数の入力に対して同じワークフローを実行できます。
requirements:
ScatterFeatureRequirement: {}
新規ユーザーがscatterを使いたいと思う最も一般的な理由は、異なるサンプルに対して同じ分析を実行することです。最初の例(hello_world.cwl
)を呼び出し、ワークフローへの入力として文字列の配列を受け取る、簡単なワークフローから始めましょう:
scatter-workflow.cwl
##!/usr/bin/env cwl-runner
cwlVersion: v1.2
class: Workflow
requirements:
ScatterFeatureRequirement: {}
inputs:
message_array: string[]
steps:
echo:
run: hello_world.cwl
scatter: message
in:
message: message_array
out: []
outputs: []
requirements
セクションのScatterFeatureRequirement
などを含めて、ここはどうなっているのでしょうか?
inputs:
message_array: string[]
まず、ここでのメインワークフローの入力には、文字列の配列が必要であることに注目してください。
steps:
echo:
run: hello_world.cwl
scatter: message
in:
message: message_array
out: []
ここでは、ステップecho
に、scatter
という新しいフィールドを追加しました。このフィールドは、CWL runnerに、このステップの入力を分散して実行したいことを伝えます。scatterの後に記載されている入力名は、ワークフローレベルの入力名ではなく、ステップの入力名であることに注意してください。
最初のスキャッターでは、これと同じくらい簡単です!このツールは出力を回収しないので、outputs: []
を使います。しかし、ワークフローの最終出力に収集する複数の出力があることが予想される場合、それを配列型に更新するようにしてください!
以下の入力ファイルを使用します:
scatter-job.yml
#message_array:
- Hello world!
- Hola mundo!
- Bonjour le monde!
- Hallo welt!
注意点として、hello_world.cwl
は単にメッセージに対してecho
というコマンドを呼び出します。コマンドラインで cwltool scatter-workflow.cwl scatter-job.yml
を呼び出します:
$ cwltool scatter-workflow.cwl scatter-job.yml
INFO /opt/hostedtoolcache/Python/3.9.19/x64/bin/cwltool 3.1.20240508115724
INFO Resolved 'scatter-workflow.cwl' to 'file:///home/runner/work/user_guide/user_guide/src/_includes/cwl/workflows/scatter-workflow.cwl'
INFO [workflow ] start
INFO [workflow ] starting step echo
INFO [step echo] start
INFO [job echo] /tmp/kkgojjhp$ echo \
'Hello world!' > /tmp/kkgojjhp/4e58fa7cfe8505476c857c252be2f391ddbf1187
INFO [job echo] completed success
INFO [step echo] start
INFO [job echo_2] /tmp/6pce0lfg$ echo \
'Hola mundo!' > /tmp/6pce0lfg/4e58fa7cfe8505476c857c252be2f391ddbf1187
INFO [job echo_2] completed success
INFO [step echo] start
INFO [job echo_3] /tmp/z902b96y$ echo \
'Bonjour le monde!' > /tmp/z902b96y/4e58fa7cfe8505476c857c252be2f391ddbf1187
INFO [job echo_3] completed success
INFO [step echo] start
INFO [job echo_4] /tmp/luv8o7cd$ echo \
'Hallo welt!' > /tmp/luv8o7cd/4e58fa7cfe8505476c857c252be2f391ddbf1187
INFO [job echo_4] completed success
INFO [step echo] completed success
INFO [workflow ] completed success
{}INFO Final process status is success
ワークフローは、message_array
の各要素毎に、echo を呼び出していることがわかります。では、ワークフローの2つのステップに分散させたい場合はどうでしょうか?
上記のような簡単なエコーを実行します、outputs: []
の代わりに次の行を追加してstdout
をキャプチャしてみましょう
hello_world_to_stdout.cwl
#outputs:
echo_out:
type: stdout
そして、wc
を使って各ファイルの文字数をカウントする第2ステップを追加します。以下のツールを見てください:
wc-tool.cwl
##!/usr/bin/env cwl-runner
cwlVersion: v1.2
class: CommandLineTool
baseCommand: wc
arguments: ["-c"]
inputs:
input_file:
type: File
inputBinding:
position: 1
outputs: []
さて、scatterはどのように取り入れるのでしょうか?scatterフィールドは各ステップの下にあることを思い出してください:
scatter-two-steps.cwl
##!/usr/bin/env cwl-runner
cwlVersion: v1.2
class: Workflow
requirements:
ScatterFeatureRequirement: {}
inputs:
message_array: string[]
steps:
echo:
run: hello_world_to_stdout.cwl
scatter: message
in:
message: message_array
out: [echo_out]
wc:
run: wc-tool.cwl
scatter: input_file
in:
input_file: echo/echo_out
out: []
outputs: []
ここでは、各ステップの下にscatterフィールドを配置しました。この例では素早く実行されるので問題ありませんが、より複雑なワークフローのために多くのサンプルを実行する場合は、別の方法を検討することをお勧めします。ここでは、各ステップで独立してscatterを実行していますが、2番目のステップはすべての言語を完了する最初のステップに依存していないため、scatter機能を効率的に使用することはできません。第2ステップは第1ステップからの入力として配列を期待するので、何かをする前に第1ステップのすべてが終了するまで待つことになります。echo Hello World!
の実行に1分、出力のwc -c
に3分、echo Hallo welt!
の実行に5分、出力のwc
に3分かかると仮定してください。echo Hello World!
が4分で終了しても、最初のステップはecho Hallo welt!
を待つ必要があるので、実際には8分で終了することになります。このように、うまくスケールしないことがおわかりいただけると思います。
では、他のサンプルと独立して進行できるステップを分散させるにはどうすればいいのでしょうか?Nested Workflowsで、ワークフロー全体を別のワークフローの1ステップにすることができることを思い出しましょう!2ステップのワークフローを1ステップのサブワークフローに変換してみましょう:
scatter-nested-workflow.cwl
##!/usr/bin/env cwl-runner
cwlVersion: v1.2
class: Workflow
requirements:
ScatterFeatureRequirement: {}
SubworkflowFeatureRequirement: {}
inputs:
message_array: string[]
steps:
subworkflow:
run:
class: Workflow
inputs:
message: string
outputs: []
steps:
echo:
run: hello_world_to_stdout.cwl
in:
message: message
out: [echo_out]
wc:
run: wc-tool.cwl
in:
input_file: echo/echo_out
out: []
scatter: message
in:
message: message_array
out: []
outputs: []
今、scatter は1つのステップに作用しますが、そのステップは2つのステップで構成されているので、各ステップは並行して実行されます。
2.10.4. 条件分岐ワークフロー#
このワークフローは、条件付きステップを含み、入力に基づき実行されます。これにより、ワークフローは、プログラムの開始時に与えられた入力パラメータや以前のステップによって、追加のステップをスキップできます。
conditional-workflow.cwl
#class: Workflow
cwlVersion: v1.2
inputs:
val: int
steps:
step1:
in:
in1: val
a_new_var: val
run: foo.cwl
when: $(inputs.in1 < 1)
out: [out1]
step2:
in:
in1: val
a_new_var: val
run: foo.cwl
when: $(inputs.a_new_var > 2)
out: [out1]
outputs:
out1:
type: string
outputSource:
- step1/out1
- step2/out1
pickValue: first_non_null
requirements:
InlineJavascriptRequirement: {}
MultipleInputFeatureRequirement: {}
まず、このワークフローはCWL仕様のバージョン1.2以降から対応しているがわかります。
class: Workflow
cwlVersion: v1.2
ワークフローの最初のステップ(step1)には2つの入力プロパティがあり、条件を満たしたときにfoo.cwlを実行します。新しいプロパティwhen
は、条件の検証が行われる場所です。この場合、ワークフローからin1
に値< 1
が含まれるときのみ、このステップが実行されます。
steps:
step1:
in:
in1: val
a_new_var: val
run: foo.cwl
when: $(inputs.in1 < 1)
out: [out1]
次のコマンドcwltool cond-wf-003.1.cwl --val 0
を実行します。この値(val)は最初の条件ステップを通過するため実行され、ログにはINFO [step step1] start
で示されています。一方、2番目のステップはINFO [step step2] will be skipped
で示されているようにスキップされ ます。
INFO [workflow ] start
INFO [workflow ] starting step step1
INFO [step step1] start
INFO [job step1] /private/tmp/docker_tmpdcyoto2d$ echo
INFO [job step1] completed success
INFO [step step1] completed success
INFO [workflow ] starting step step2
INFO [step step2] will be skipped
INFO [step step2] completed skipped
INFO [workflow ] completed success
{
"out1": "foo 0"
}
INFO Final process status is success
cwltool cond-wf-003.1.cwl --val 3
のように値(val)として3が与えられた場合、最初の条件ステップは実行されませんが、2番目の条件ステップは実行されます.
INFO [workflow ] start
INFO [workflow ] starting step step1
INFO [step step1] will be skipped
INFO [step step1] completed skipped
INFO [workflow ] starting step step2
INFO [step step2] start
INFO [job step2] /private/tmp/docker_tmpqwr93mxx$ echo
INFO [job step2] completed success
INFO [step step2] completed success
INFO [workflow ] completed success
{
"out1": "foo 3"
}
INFO Final process status is success
--val 2
を使用した場合など、条件を満たさない場合は、ワークフローはpermanentFailを発生させ、失敗となります。
$ cwltool cond-wf-003.1.cwl --val 2
INFO [workflow ] start
INFO [workflow ] starting step step1
INFO [step step1] will be skipped
INFO [step step1] completed skipped
INFO [workflow ] starting step step2
INFO [step step2] will be skipped
INFO [step step2] completed skipped
ERROR [workflow ] Cannot collect workflow output: All sources for 'out1' are null
INFO [workflow ] completed permanentFail
WARNING Final process status is permanentFail