2.10. ワークフロー#

Workflowは、CommandLineTool、ExpressionTool、またはWorkflow(サブワークフロー)をステップ として実行する CWL 処理単位です。CWL定義にはinputsoutputssteps を定義する必要があります。

digraph G { compound=true; rankdir="LR"; fontname="Verdana"; fontsize="10"; graph [splines=ortho]; node [fontname="Verdana", fontsize="10", shape=box]; edge [fontname="Verdana", fontsize="10"]; subgraph cluster_0 { node [width = 1.75]; steps_0[style="filled" label="Command-line tools"]; steps_1[style="filled" label="Expression tools"]; steps_2[style="filled" label="Sub-workflows"]; label="steps"; fill=gray; } inputs -> steps_1 [lhead=cluster_0]; steps_1 -> outputs [ltail=cluster_0]; }

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]

1st-workflow.cwlの可視化

1st-workflow.cwlを可視化したもの

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 /home/docs/checkouts/readthedocs.org/user_builds/common-workflow-language-user-guide-ja/envs/latest/bin/cwltool 3.1.20240508115724
INFO Resolved '1st-workflow.cwl' to 'file:///home/docs/checkouts/readthedocs.org/user_builds/common-workflow-language-user-guide-ja/checkouts/latest/src/_includes/cwl/workflows/1st-workflow.cwl'
INFO [workflow ] start
INFO [workflow ] starting step untar
INFO [step untar] start
INFO [job untar] /tmp/kcw969pf$ tar \
    --extract \
    --file \
    /tmp/xzzjg1zn/stgdaddb452-5f81-4ed5-a629-a6b2b8871d8f/hello.tar \
    Hello.java
INFO [job untar] completed success
INFO [step untar] completed success
INFO [workflow ] starting step compile
INFO [step compile] start
ERROR Workflow error, try again with --debug for more information:
Docker is not available for this tool, try --no-container to disable Docker, or install a user space Docker replacement like uDocker with --user-space-docker-cmd.: docker executable is not available

どうなっているのでしょう? 分解してみましょう:

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つの入力パラメータ、tarfileextractfile と1つの出力パラメータextracted_file を持ちます。

ワークフローステップのin セクションは、これらの2つの入力パラメータを、source を使用してワークフローの入力であるtarball およびname_of_file_to_extract に接続しています。 つまり、ワークフローステップが実行されると、tarballname_of_file_to_extract に割り当てられた値が、ツールを実行するためにtarfileextractfile のパラメータに使用されることになり ます。

ワークフローステップのout セクションには、ツールから期待される出力パラメータが記載されています。

  compile:
    run: arguments.cwl
    in:
      src: untar/extracted_file
    out: [classfile]

第2ステップ compile は、第1ステップの結果に依存し、入力パラメータsrcuntar の出力パラメータに接続し、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 として与えられているのがわかります。

Visualization of nestedworkflows.cwl Visualization of 1st-workflow.cwl

CWLWorkflow は、CommandLineToolstep として使用することができ、その 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.tarHello.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 /home/docs/checkouts/readthedocs.org/user_builds/common-workflow-language-user-guide-ja/envs/latest/bin/cwltool 3.1.20240508115724
INFO Resolved 'scatter-workflow.cwl' to 'file:///home/docs/checkouts/readthedocs.org/user_builds/common-workflow-language-user-guide-ja/checkouts/latest/src/_includes/cwl/workflows/scatter-workflow.cwl'
INFO [workflow ] start
INFO [workflow ] starting step echo
INFO [step echo] start
INFO [job echo] /tmp/vb8ok148$ echo \
    'Hello world!' > /tmp/vb8ok148/d868094b5a34a9a46c157674ef49b904a124e3b3
INFO [job echo] completed success
INFO [step echo] start
INFO [job echo_2] /tmp/oh1jfos6$ echo \
    'Hola mundo!' > /tmp/oh1jfos6/d868094b5a34a9a46c157674ef49b904a124e3b3
INFO [job echo_2] completed success
INFO [step echo] start
INFO [job echo_3] /tmp/ng3zuqe0$ echo \
    'Bonjour le monde!' > /tmp/ng3zuqe0/d868094b5a34a9a46c157674ef49b904a124e3b3
INFO [job echo_3] completed success
INFO [step echo] start
INFO [job echo_4] /tmp/3zhtu8w3$ echo \
    'Hallo welt!' > /tmp/3zhtu8w3/d868094b5a34a9a46c157674ef49b904a124e3b3
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