2.3. Expressions#

If you need to manipulate input parameters, include the requirement InlineJavascriptRequirement and then anywhere a parameter reference is legal you can provide a fragment of Javascript that will be evaluated by the CWL runner.

Important

JavaScript expressions should only be used when absolutely necessary. When manipulating file names, extensions, paths etc, consider whether one of the built in File properties like basename, nameroot, nameext, etc, could be used instead. See the list of best practices.

expression.cwl#
#!/usr/bin/env cwl-runner
cwlVersion: v1.2
class: CommandLineTool
baseCommand: echo

requirements:
  InlineJavascriptRequirement: {}

inputs: []
outputs:
  example_out:
    type: stdout
stdout: output.txt
arguments:
  - prefix: -A
    valueFrom: $(1+1)
  - prefix: -B
    valueFrom: $("/foo/bar/baz".split('/').slice(-1)[0])
  - prefix: -C
    valueFrom: |
      ${
        var r = [];
        for (var i = 10; i >= 1; i--) {
          r.push(i);
        }
        return r;
      }

As this tool does not require any inputs we can run it with an (almost) empty job file:

empty.yml#
{}

empty.yml contains a description of an empty JSON object. JSON objects descriptions are contained inside curly brackets {}, so an empty object is represented simply by a set of empty brackets.

We can then run expression.cwl:

Running expression.cwl#
$ cwltool expression.cwl empty.yml
INFO /opt/hostedtoolcache/Python/3.9.19/x64/bin/cwltool 3.1.20240508115724
INFO Resolved 'expression.cwl' to 'file:///home/runner/work/user_guide/user_guide/src/_includes/cwl/expressions/expression.cwl'
INFO [job expression.cwl] /tmp/nq4jf2nx$ echo \
    -A \
    2 \
    -B \
    baz \
    -C \
    10 \
    9 \
    8 \
    7 \
    6 \
    5 \
    4 \
    3 \
    2 \
    1 > /tmp/nq4jf2nx/output.txt
INFO [job expression.cwl] completed success
{
    "example_out": {
        "location": "file:///home/runner/work/user_guide/user_guide/src/_includes/cwl/expressions/output.txt",
        "basename": "output.txt",
        "class": "File",
        "checksum": "sha1$a739a6ff72d660d32111265e508ed2fc91f01a7c",
        "size": 36,
        "path": "/home/runner/work/user_guide/user_guide/src/_includes/cwl/expressions/output.txt"
    }
}INFO Final process status is success
$ cat output.txt
-A 2 -B baz -C 10 9 8 7 6 5 4 3 2 1

Note that requirements can be provided with the map syntax, as in the example above:

requirements:
  InlineJavascriptRequirement: {}

Or as an array, with each entry (in this case, only class: InlineJavascriptRequirement) marked by a -. The same syntax is used to describe the additional command line arguments.

requirements:
  - class: InlineJavascriptRequirement

Where are JavaScript expressions allowed?

Just like parameter references, you can use JavaScript Expressions only in certain fields. These are:

2.3.1. Using External Libraries and Inline JavaScript Code with expressionLib#

The requirement InlineJavascriptRequirement supports an expressionLib attribute that allows users to load external JavaScript files, or to provide inline JavaScript code.

Entries added to the expressionLib attribute are parsed with the JavaScript engine of a CWL runner. This can be used to include external files or to create JavaScript functions that can be called in other parts of the CWL document.

Note

The CWL standards (versions 1.0 through 1.2) states that the only version of JavaScript valid in CWL expressions is ECMAScript 5.1. This means that any code that you include or write in your CWL Document must be compliant with ECMAScript 5.1.

For example, we can use InlineJavascriptRequirement and write a JavaScript function inline in expressionLib. That function can then be used in other parts of the CWL document:

hello-world-expressionlib-inline.cwl#
cwlVersion: v1.2
class: CommandLineTool
requirements:
  - class: InlineJavascriptRequirement
    expressionLib:
      - |
        /**
         * Capitalize each word passed. Will split the text by spaces.
         * For instance, given "hello world", it returns "Hello World".
         *
         * @param {String} message - The input message.
         * @return {String} the message with each word with its initial letter capitalized.
         */
        function capitalizeWords (message) {
          if (message === undefined || message === null || typeof message !== 'string' || message.trim().length === 0) {
            return '';
          }
          return message
            .split(' ')
            .map(function (token) {
              return token.charAt(0).toUpperCase() + token.slice(1);
            })
            .join(' ');
        }

baseCommand: echo

inputs:
  message:
    type: string

arguments: [$( capitalizeWords(inputs.message) )]

outputs: []

Running this CWL workflow will invoke the JavaScript function and result in the echo command printing the input message with capital initial letters:

Running hello-world-expressionlib-inline.cwl.#
$ cwltool hello-world-expressionlib-inline.cwl --message "hello world"
INFO /opt/hostedtoolcache/Python/3.9.19/x64/bin/cwltool 3.1.20240508115724
INFO Resolved 'hello-world-expressionlib-inline.cwl' to 'file:///home/runner/work/user_guide/user_guide/src/_includes/cwl/expressions/hello-world-expressionlib-inline.cwl'
INFO [job hello-world-expressionlib-inline.cwl] /tmp/oh8ii7_r$ echo \
    'Hello World'
Hello World
INFO [job hello-world-expressionlib-inline.cwl] completed success
{}INFO Final process status is success

Let’s move the capitalizeWords function to an external file, custom-functions.js, and import it in our CWL document:

custom-functions.js#
/**
 * Capitalize each word passed. Will split the text by spaces.
 * For instance, given "hello world", it returns "Hello World".
 *
 * @param {String} message - The input message.
 * @return {String} the message with each word with its initial letter capitalized.
 */
function capitalizeWords (message) {
  if (message === undefined || message === null || typeof message !== 'string' || message.trim().length === 0) {
    return '';
  }
  return message
    .split(' ')
    .map(function (token) {
      return token.charAt(0).toUpperCase() + token.slice(1);
    })
    .join(' ');
}
hello-world-expressionlib-external.cwl#
cwlVersion: v1.2
class: CommandLineTool
requirements:
  - class: InlineJavascriptRequirement
    expressionLib:
      - { $include: custom-functions.js }

baseCommand: echo

inputs:
  message:
    type: string

arguments: [$( capitalizeWords(inputs.message) )]

outputs: []

The custom-functions.js file is included in the CWL document with the $include: custom-functions.js statement. That makes the functions and variables available to be used in other parts of the CWL document.

Running hello-world-expressionlib-external.cwl.#
$ cwltool hello-world-expressionlib-external.cwl --message "hello world"
INFO /opt/hostedtoolcache/Python/3.9.19/x64/bin/cwltool 3.1.20240508115724
INFO Resolved 'hello-world-expressionlib-external.cwl' to 'file:///home/runner/work/user_guide/user_guide/src/_includes/cwl/expressions/hello-world-expressionlib-external.cwl'
INFO [job hello-world-expressionlib-external.cwl] /tmp/5_k7tcyk$ echo \
    'Hello World'
Hello World
INFO [job hello-world-expressionlib-external.cwl] completed success
{}INFO Final process status is success

Finally, note that you can have both inline and external JavaScript code in your CWL document. In this final example we have added another entry to the expressionLib attribute with the new function createHelloWorldMessage, that calls the capitalizeWords function from the external file custom-functions.js.

hello-world-expressionlib.cwl#
cwlVersion: v1.2
class: CommandLineTool
requirements:
  - class: InlineJavascriptRequirement
    expressionLib:
      - { $include: custom-functions.js }
      - |
        /**
         * A merely illustrative example function that uses a function
         * from the included custom-functions.js file to create a
         * Hello World message.
         *
         * @param {Object} message - CWL document input message
         */
        var createHelloWorldMessage = function (message) {
          return capitalizeWords(message);
        };

baseCommand: echo

inputs:
  message:
    type: string

arguments: [$( createHelloWorldMessage(inputs.message) )]

outputs: []
Running hello-world-expressionlib.cwl.#
$ cwltool hello-world-expressionlib.cwl --message "hello world"
INFO /opt/hostedtoolcache/Python/3.9.19/x64/bin/cwltool 3.1.20240508115724
INFO Resolved 'hello-world-expressionlib.cwl' to 'file:///home/runner/work/user_guide/user_guide/src/_includes/cwl/expressions/hello-world-expressionlib.cwl'
INFO [job hello-world-expressionlib.cwl] /tmp/jdasbk5z$ echo \
    'Hello World'
Hello World
INFO [job hello-world-expressionlib.cwl] completed success
{}INFO Final process status is success

Note

The $include statement can be used to include a file from the local disk or from a remote location. It works with both relative and absolute paths. Read the text about $include from the CWL specification to learn more about it.