Concise Common Workflow Language

1 Introduction

CWL (Common Workflow Language) is an open standard for describing analysis workflows and tools in a way that makes them portable and scalable across a variety of software and hardware environments.

ccwl (Concise Common Workflow Language) is a concise syntax to express CWL workflows. It is implemented as an EDSL (Embedded Domain Specific Language) in the Scheme programming language, a minimalist dialect of the Lisp family of programming languages.

ccwl is a compiler to generate CWL workflows from concise descriptions in ccwl. In the future, ccwl will also have a runtime whereby users can interactively execute workflows while developing them.


2 Tutorial

This tutorial will introduce you to writing workflows in ccwl. Some knowledge of CWL is assumed. To learn about CWL, please see the Common Workflow Language User Guide

2.1 Important concepts

The CWL and ccwl workflow languages are statically typed programming languages where functions accept multiple named inputs and return multiple named outputs. Let 's break down what that means.

2.1.1 Static typing

In CWL, the type of arguments accepted by a function and the type of outputs returned by that function are specified explicitly by the programmer, and are known at compile time even before the code has been run. Hence, we say that it is statically typed.

2.1.2 Positional arguments and named arguments

In many languages, the order of arguments passed to a function is significant. The position of each argument determines which formal argument it gets mapped to. For example, passing positional arguments in Scheme looks like (foo 1 2). However, in a language that supports named arguments (say, Scheme or Python), the order of arguments is not significant. Each argument explicitly names the formal argument it gets mapped to. For example, in Scheme, passing named arguments may look like (foo #:bar 1 #:baz 2) and is equivalent to (foo #:baz 2 #:bar 1). Likewise, in Python, passing named arguments looks like foo(bar=1, baz=2) and is equivalent to foo(baz=2, bar=1).

2.1.3 Multiple function arguments and return values

In most languages, functions accept multiple input arguments but only return a single output value. However, in CWL, a function can return multiple output values as well. These multiple outputs are unordered and are each addressed by a unique name.


2.2 First example

As is tradition, let us start with a simple Hello World workflow in ccwl. This workflow accepts a string input and prints that string.

(define print
  (command #:inputs (message #:type string)
           #:run "echo" message))

(workflow ((message #:type string))
  (print #:message message))

The first form in this code defines the print command. This form is the equivalent of defining a CommandLineTool class workflow in CWL. The arguments after #:inputs define the inputs to the workflow. The arguments after #:run specify the command that will be run. The input (message #:type 'string) defines a string type input named message. The command defined in the #:run argument is the command itself followed by a list of command arguments. One of the arguments references the input message. Notice how the command definition is very close to a shell command, only that it is slightly annotated with inputs and their types.

The second form describes the actual workflow and is the equivalent of defining a Workflow class workflow in CWL. The form ((message #:type string)) specifies the inputs of the workflow. In this case, there is only one input---message of type string. The body of the workflow specifies the commands that will be executed. The body of this workflow executes only a single command---the print command---passing the message input of the workflow as the message input to the print command.

If this workflow is written to a file hello-world.scm, we may compile it to CWL by running

$ ccwl compile hello-world.scm

This prints a big chunk of generated CWL to standard output. We have achieved quite a lot of concision already! We write the generated CWL to a file and execute it using (command "cwltool") as follows. The expected output is also shown.

$ ccwl compile hello-world.scm > hello-world.cwl
$ cwltool hello-world.cwl --message "Hello World!" 
[workflow ] start
[workflow ] starting step print
[step print] start
[job print] /tmp/2oduiq64$ echo \
    '"Hello World!"'
"Hello World!"
[job print] completed success
[step print] completed success
[workflow ] completed success
{}
Final process status is success

2.3 Capturing the standard output stream of a command

Let us return to the Hello World example in the previous section. But now, let us capture the standard output of the print command in an output object. The ccwl code is the same as earlier with only the addition of an stdout type output object to the command definition.

(define print
  (command #:inputs (message #:type string)
           #:run "echo" message
           #:outputs (printed-message #:type stdout)))

(workflow ((message #:type string))
  (print #:message message))

Let's write this code to a file capture-stdout.scm, generate CWL, write the generated CWL to capture-stdout.cwl, and run it using cwltool. We might expect something like the output below. Notice how the standard output of the print command has been captured in the file 51fe79d15e7790a9ded795304220d7a44aa84b48.

$ ccwl compile capture-stdout.scm > capture-stdout.cwl
$ cwltool capture-stdout.cwl --message "Hello World!" 
[workflow ] start
[workflow ] starting step print
[step print] start
[job print] /tmp/s1gu6usf$ echo \
    '"Hello World!"' > /tmp/s1gu6usf/800000dec226f7c192658451b24574d4f10075f8
[job print] completed success
[step print] completed success
[workflow ] completed success
{
    "printed-message": {
        "location": "file:///home/manimekalai/800000dec226f7c192658451b24574d4f10075f8",
        "basename": "800000dec226f7c192658451b24574d4f10075f8",
        "class": "File",
        "checksum": "sha1$b55dbc4ecb0cec94a207406bc5cde2f68b367f58",
        "size": 15,
        "path": "/home/manimekalai/800000dec226f7c192658451b24574d4f10075f8"
    }
}
Final process status is success

2.4 Capturing output files

In the previous section, we captured the standard output stream of a command. But, how do we capture any output files created by a command? Let us see.

Consider a tar archive hello.tar containing a file hello.txt.

$ tar --list --file hello.tar
hello.txt

Let us write a workflow to extract the file hello.txt from the archive. Everything in the following workflow except the #:binding parameter will already be familiar to you. The #:binding parameter sets the outputBinding field in the generated CWL. In the example below, we set the glob field to look for a file named hello.txt.

(define extract
  (command #:inputs (archive #:type File)
           #:run "tar" "--extract" "--file" archive
           #:outputs (extracted-file
                      #:type File
                      #:binding '((glob . "hello.txt")))))

(workflow ((archive #:type File))
  (extract #:archive archive))

Writing this workflow to capture-output-file.scm, compiling and running it gives us the following output. Notice that the file hello.txt has been captured and is now present in our current working directory.

$ ccwl compile capture-output-file.scm > capture-output-file.cwl
$ cwltool capture-output-file.cwl --archive hello.tar 
[workflow ] start
[workflow ] starting step extract
[step extract] start
[job extract] /tmp/et0pmjry$ tar \
    --extract \
    --file \
    /tmp/z11ccnff/stg82d79c09-0286-466d-ac93-7500dce74288/hello.tar
[job extract] completed success
[step extract] completed success
[workflow ] completed success
{
    "extracted-file": {
        "location": "file:///home/manimekalai/hello.txt",
        "basename": "hello.txt",
        "class": "File",
        "checksum": "sha1$a0b65939670bc2c010f4d5d6a0b3e4e4590fb92b",
        "size": 13,
        "path": "/home/manimekalai/hello.txt"
    }
}
Final process status is success

The above workflow is not awfully flexible. The name of the file to extract is hardcoded into the workflow. Let us modify the workflow to accept the name of the file to extract. We introduce extractfile, a string type input that is passed to tar and is referenced in the glob field.

(define extract-specific-file
  (command #:inputs (archive #:type File) (extractfile #:type string)
           #:run "tar" "--extract" "--file" archive extractfile
           #:outputs (extracted-file
                      #:type File
                      #:binding '((glob . "$(inputs.extractfile)")))))

(workflow ((archive #:type File) (extractfile #:type string))
  (extract-specific-file #:archive archive #:extractfile extractfile))

Compiling and running this workflow gives us the following output.

$ ccwl compile capture-output-file-with-parameter-reference.scm > capture-output-file-with-parameter-reference.cwl
$ cwltool capture-output-file-with-parameter-reference.cwl --archive hello.tar --extractfile hello.txt 
[workflow ] start
[workflow ] starting step extract-specific-file
[step extract-specific-file] start
[job extract-specific-file] /tmp/27bo3bky$ tar \
    --extract \
    --file \
    /tmp/nje6xk67/stgd115a9cc-0e9e-4c54-9f11-1bb1aad2178d/hello.tar \
    hello.txt
[job extract-specific-file] completed success
[step extract-specific-file] completed success
[workflow ] completed success
{
    "extracted-file": {
        "location": "file:///home/manimekalai/hello.txt",
        "basename": "hello.txt",
        "class": "File",
        "checksum": "sha1$a0b65939670bc2c010f4d5d6a0b3e4e4590fb92b",
        "size": 13,
        "path": "/home/manimekalai/hello.txt"
    }
}
Final process status is success

2.5 Passing input into the standard input stream of a command

Some commands read input from their standard input stream. Let us do that from ccwl. The workflow below reports the size of a file by passing it into the standard input of wc. Notice the additional #:stdin keyword that references the input file.

(define count-bytes
  (command #:inputs (file #:type File)
           #:run "wc" "-c"
           #:stdin file))

(workflow ((file #:type File))
  (count-bytes #:file file))

Compiling and running this workflow gives us the following output. Notice the file hello.txt passed into the standard input of wc, and the file size reported in bytes.

$ ccwl compile pass-stdin.scm > pass-stdin.cwl
$ cwltool pass-stdin.cwl --file hello.txt 
[workflow ] start
[workflow ] starting step count-bytes
[step count-bytes] start
[job count-bytes] /tmp/9fvg9tjp$ wc \
    -c < /tmp/y1p5_wzs/stg86aafcc0-1a80-4965-abb9-744135d4b3b9/hello.txt
13
[job count-bytes] completed success
[step count-bytes] completed success
[workflow ] completed success
{}
Final process status is success

2.6 Workflow with multiple steps

Till now, we have only written trivial workflows with a single command. If we were only interested in executing single commands, we would hardly need a workflow language! So, in this section, let us write our first multi-step workflow and learn how to connect steps together in an arbitrary topology.

2.6.1 pipe

First, the simplest of topologies---a linear chain representing sequential execution of steps. The following workflow decompresses a compressed C source file, compiles and then executes it.

(define decompress
  (command #:inputs (compressed #:type File)
           #:run "gzip" "--stdout" "--decompress" compressed
           #:outputs (decompressed #:type stdout)))

(define compile
  (command #:inputs (source #:type File)
           #:run "gcc" "-x" "c" source
           #:outputs (executable
                      #:type File
                      #:binding '((glob . "a.out")))))

(define run
  (command #:inputs executable
           #:run executable
           #:outputs (stdout #:type stdout)))

(workflow ((compressed-source #:type File))
  (pipe (decompress #:compressed compressed-source)
        (compile #:source decompressed)
        (run #:executable executable)))

Notice the pipe form in the body of the workflow. The pipe form specifies a list of steps to be executed sequentially. The inputs coming into pipe are passed into the first step. Thereafter, the outputs of each step are passed as inputs into the next. Note that this has nothing to do with the Unix pipe. The inputs/outputs passed between steps are general CWL inputs/outputs. They need not be the standard stdin and stdout streams.

Writing this worklow to decompress-compile-run.scm, compiling and running it with the compressed C source file hello.c.gz gives us the following output.

$ ccwl compile decompress-compile-run.scm > decompress-compile-run.cwl
$ cwltool decompress-compile-run.cwl --compressed-source hello.c.gz 
[workflow ] start
[workflow ] starting step decompress
[step decompress] start
[job decompress] /tmp/0ysy4dtd$ gzip \
    --stdout \
    --decompress \
    /tmp/i0v6a2lt/stga8a7cea6-4208-47c1-ae02-d39967f08f9b/hello.c.gz > /tmp/0ysy4dtd/f2136c7decfadb4bf675abf665f94162c106966d
[job decompress] completed success
[step decompress] completed success
[workflow ] starting step compile
[step compile] start
[job compile] /tmp/qraz8j5c$ gcc \
    -x \
    c \
    /tmp/ap78xvsy/stgabca2550-c752-4c7a-b672-80078ed8ef24/f2136c7decfadb4bf675abf665f94162c106966d
[job compile] Max memory used: 15MiB
[job compile] completed success
[step compile] completed success
[workflow ] starting step run
[step run] start
[job run] /tmp/g2d309l_$ /tmp/ii_vvmy_/stg67a8a6d2-2b8a-429a-b9c7-1294a554b136/a.out > /tmp/g2d309l_/b904deb4bd106a5d69e860e96f6cd3e179ded79a
[job run] completed success
[step run] completed success
[workflow ] completed success
{
    "stdout": {
        "location": "file:///home/manimekalai/b904deb4bd106a5d69e860e96f6cd3e179ded79a",
        "basename": "b904deb4bd106a5d69e860e96f6cd3e179ded79a",
        "class": "File",
        "checksum": "sha1$a0b65939670bc2c010f4d5d6a0b3e4e4590fb92b",
        "size": 13,
        "path": "/home/manimekalai/b904deb4bd106a5d69e860e96f6cd3e179ded79a"
    }
}
Final process status is success

The steps run in succession, and the stdout of the compiled executable is in c32c587f7afbdf87cf991c14a43edecf09cd93bf. Success!

2.6.2 tee

Next, the tee topology. The following workflow computes three different checksums of a given input file.

(define md5sum
  (command #:inputs (file #:type File)
           #:run "md5sum" file
           #:outputs (md5 #:type stdout)))

(define sha1sum
  (command #:inputs (file #:type File)
           #:run "sha1sum" file
           #:outputs (sha1 #:type stdout)))

(define sha256sum
  (command #:inputs (file #:type File)
           #:run "sha256sum" file
           #:outputs (sha256 #:type stdout)))

(workflow ((file #:type File))
  (tee (md5sum #:file file)
       (sha1sum #:file file)
       (sha256sum #:file file)))

Notice the tee form in the body of the workflow. The tee form specifies a list of steps that are independent of each other. The inputs coming into tee are passed into every step contained in the body of the tee. The outputs of each step are collected together and unioned as the output of the tee.

Writing this workflow to checksum.scm, compiling and running it with some file hello.txt gives us the following output.

$ ccwl compile checksum.scm > checksum.cwl
$ cwltool checksum.cwl --file hello.txt 
[workflow ] start
[workflow ] starting step sha1sum
[step sha1sum] start
[job sha1sum] /tmp/lehyi_8n$ sha1sum \
    /tmp/lof8_b01/stg10c9bf3c-0241-4ea7-9394-fce6c6f0eca2/hello.txt > /tmp/lehyi_8n/5f2856091611ccd6a3d258f87a02820ab2ffe801
[job sha1sum] completed success
[step sha1sum] completed success
[workflow ] starting step sha256sum
[step sha256sum] start
[job sha256sum] /tmp/jtcr4cbp$ sha256sum \
    /tmp/mr56b93j/stgb447cb52-8101-4e71-b5af-6a7ec9aa0218/hello.txt > /tmp/jtcr4cbp/6c284de0142fc4b534fbd63a9e1afe8d4481d20c
[job sha256sum] completed success
[step sha256sum] completed success
[workflow ] starting step md5sum
[step md5sum] start
[job md5sum] /tmp/o7ylyum2$ md5sum \
    /tmp/kppk3p02/stg8a9a8183-fa96-4924-9cfa-7d92ac93989d/hello.txt > /tmp/o7ylyum2/ddd8a6b3026f1b8fa9514eaf21288fa84a0c923d
[job md5sum] completed success
[step md5sum] completed success
[workflow ] completed success
{
    "md5": {
        "location": "file:///home/manimekalai/ddd8a6b3026f1b8fa9514eaf21288fa84a0c923d",
        "basename": "ddd8a6b3026f1b8fa9514eaf21288fa84a0c923d",
        "class": "File",
        "checksum": "sha1$4235a2e1c3faab3307a5f0ca200c0001b088cd64",
        "size": 98,
        "path": "/home/manimekalai/ddd8a6b3026f1b8fa9514eaf21288fa84a0c923d"
    },
    "sha1": {
        "location": "file:///home/manimekalai/5f2856091611ccd6a3d258f87a02820ab2ffe801",
        "basename": "5f2856091611ccd6a3d258f87a02820ab2ffe801",
        "class": "File",
        "checksum": "sha1$52610473349da6db7553c4790d1b28fd7d9210cd",
        "size": 106,
        "path": "/home/manimekalai/5f2856091611ccd6a3d258f87a02820ab2ffe801"
    },
    "sha256": {
        "location": "file:///home/manimekalai/6c284de0142fc4b534fbd63a9e1afe8d4481d20c",
        "basename": "6c284de0142fc4b534fbd63a9e1afe8d4481d20c",
        "class": "File",
        "checksum": "sha1$eff7b0a6bc2da76982f5d4e946f05dbc077cf237",
        "size": 130,
        "path": "/home/manimekalai/6c284de0142fc4b534fbd63a9e1afe8d4481d20c"
    }
}
Final process status is success

The MD5, SHA1 and SHA256 checksums are in the files 112be1054505027982e64d56b0879049c12737c6, d2f19c786fcd3feb329004c8747803fba581a02d and 0d2eaa5619c14b43326101200d0f27b0d8a1a4b1 respectively.


2.7 Let's write a spell check workflow

Finally, let's put together a complex workflow to understand how everything fits together. The workflow we will be attempting is a spell check workflow inspired by the founders of Unix1 and by dgsh2. The workflow is pictured below. Let's start by coding each of the steps required by the workflow.

The first command, split-words, splits up the input text into words, one per line. It does this by invoking the tr command to replace anything that is not an alphabetic character with a newline. In addition, it uses the --squeeze-repeats flag to prevent blank lines from appearing in its output. Notice that no type is specified for the input text. When no type is specified, ccwl assumes a File type.

(define split-words
  (command #:inputs text
           #:run "tr" "--complement" "--squeeze-repeats" "A-Za-z" "\\n"
           #:stdin text
           #:outputs (words #:type stdout)))

We want our spell check to be case-insensitive. So, we downcase all words. This is achieved using another invocation of the tr command.

(define downcase
  (command #:inputs words
           #:run "tr" "A-Z" "a-z"
           #:stdin words
           #:outputs (downcased-words #:type stdout)))

For easy comparison against a dictionary, we want both our words and our dictionary sorted and deduplicated. We achieve this by invoking the sort command with the --unique flag.

(define sort
  (command #:inputs words
           #:run "sort" "--unique"
           #:stdin words
           #:outputs (sorted #:type stdout)))

Finally, we compare the sorted word list with the sorted dictionary to identify the misspellings. We do this using the comm command.

(define find-misspellings
  (command #:inputs words dictionary
           #:run "comm" "-23" words dictionary
           #:outputs (misspellings #:type stdout)))

Now, let's wire up the workflow. First, we assemble the split-words-downcase-sort-words arm of the workflow. This arm is just a linear chain that can be assembled using pipe. We will need to invoke the sort command twice in our workflow. To distinguish the two invocations, CWL requires us to specify a unique step id for each invocation. We do this using the second element, (sort-words). To avoid name conflicts, we also need to rename the output of the sort command. The last step, rename, a special ccwl construct that, is used to achieve this. In this case, it renames the sorted output of the sort command into sorted-words.

(workflow (text-file)
  (pipe (split-words #:text text-file)
        (downcase #:words words)
        (sort (sort-words) #:words downcased-words)
        (rename #:sorted-words sorted)))

Next, we assemble the split-dictionary arm of the workflow. This arm is just a single step. Then, we connect up both the arms using a tee. Here too, we have a step id and renaming of intermediate inputs/outputs.

(workflow (text-file dictionary)
  (tee (pipe (split-words #:text text-file)
                   (downcase #:words words)
                   (sort (sort-words) #:words downcased-words)
                   (rename #:sorted-words sorted))
             (pipe (sort (sort-dictionary) #:words dictionary)
                   (rename #:sorted-dictionary sorted))))

And finally, we use the outputs of both the arms of the workflow together in the find-misspellings step.

(workflow (text-file dictionary)
  (pipe (tee (pipe (split-words #:text text-file)
                   (downcase #:words words)
                   (sort (sort-words) #:words downcased-words)
                   (rename #:sorted-words sorted))
             (pipe (sort (sort-dictionary) #:words dictionary)
                   (rename #:sorted-dictionary sorted)))
        (find-misspellings #:words sorted-words
                           #:dictionary sorted-dictionary)))

The complete workflow is as follows.

(define split-words
  (command #:inputs text
           #:run "tr" "--complement" "--squeeze-repeats" "A-Za-z" "\\n"
           #:stdin text
           #:outputs (words #:type stdout)))

(define downcase
  (command #:inputs words
           #:run "tr" "A-Z" "a-z"
           #:stdin words
           #:outputs (downcased-words #:type stdout)))

(define sort
  (command #:inputs words
           #:run "sort" "--unique"
           #:stdin words
           #:outputs (sorted #:type stdout)))
 
(define find-misspellings
  (command #:inputs words dictionary
           #:run "comm" "-23" words dictionary
           #:outputs (misspellings #:type stdout)))

(workflow (text-file dictionary)
  (pipe (tee (pipe (split-words #:text text-file)
                   (downcase #:words words)
                   (sort (sort-words) #:words downcased-words)
                   (rename #:sorted-words sorted))
             (pipe (sort (sort-dictionary) #:words dictionary)
                   (rename #:sorted-dictionary sorted)))
        (find-misspellings #:words sorted-words
                           #:dictionary sorted-dictionary)))

When compiled and run with a text file and a dictionary, the misspelt words appear at the output.

$ ccwl compile spell-check.scm > spell-check.cwl
$ cwltool spell-check.cwl --text-file spell-check-text.txt --dictionary dictionary 
[workflow ] start
[workflow ] starting step sort-dictionary
[step sort-dictionary] start
[job sort-dictionary] /tmp/n4gjbo0y$ sort \
    --unique < /tmp/l1gt6yro/stg3f04865d-5bd1-48a9-884f-f310fdb86ce0/dictionary > /tmp/n4gjbo0y/d15b4850ec96ef40f4901da08a0dc11de257034d
[job sort-dictionary] completed success
[step sort-dictionary] completed success
[workflow ] starting step split-words
[step split-words] start
[job split-words] /tmp/41f91e_z$ tr \
    --complement \
    --squeeze-repeats \
    A-Za-z \
    \n < /tmp/6g8c9bc_/stgc324006d-1dfd-4d9a-8538-0b2bb33c01ac/spell-check-text.txt > /tmp/41f91e_z/086875e06c874529bd875f0db86e6ee785664586
[job split-words] completed success
[step split-words] completed success
[workflow ] starting step downcase
[step downcase] start
[job downcase] /tmp/tysi5fhs$ tr \
    A-Z \
    a-z < /tmp/irc9w3v_/stgf6b4cf58-bc7f-45d7-9b1a-8dca11fba775/086875e06c874529bd875f0db86e6ee785664586 > /tmp/tysi5fhs/091e0f34537f6eb3552872ed4276e3c1e7d7c5cf
[job downcase] completed success
[step downcase] completed success
[workflow ] starting step sort-words
[step sort-words] start
[job sort-words] /tmp/znye11ak$ sort \
    --unique < /tmp/4hsv0o3y/stg577ae35b-53a1-4210-b15b-9a16cfe25876/091e0f34537f6eb3552872ed4276e3c1e7d7c5cf > /tmp/znye11ak/8549794b6670ebdf42c9c8c739dc0969c4aec74e
[job sort-words] completed success
[step sort-words] completed success
[workflow ] starting step find-misspellings
[step find-misspellings] start
[job find-misspellings] /tmp/bq7tgi1s$ comm \
    -23 \
    /tmp/6qyszu0o/stg6ad0cf6f-9659-4016-8a7f-7ed2971e544c/8549794b6670ebdf42c9c8c739dc0969c4aec74e \
    /tmp/6qyszu0o/stgc1a0ce01-cdf1-4921-9c32-a63c20e98349/d15b4850ec96ef40f4901da08a0dc11de257034d > /tmp/bq7tgi1s/3ab53f2a204bc2959d18ab0b97a67cc1b9194557
[job find-misspellings] completed success
[step find-misspellings] completed success
[workflow ] completed success
{
    "misspellings": {
        "location": "file:///home/manimekalai/3ab53f2a204bc2959d18ab0b97a67cc1b9194557",
        "basename": "3ab53f2a204bc2959d18ab0b97a67cc1b9194557",
        "class": "File",
        "checksum": "sha1$e701a33e7681ea185a709168c44e13217497d220",
        "size": 6,
        "path": "/home/manimekalai/3ab53f2a204bc2959d18ab0b97a67cc1b9194557"
    }
}
Final process status is success



3 Cookbook

3.1 Reuse external CWL workflows

Even though you may be a ccwl convert (hurrah!), others may not be. And, you might have to work with CWL workflows written by others. ccwl permits easy reuse of external CWL workflows, and free mixing with ccwl commands. Here is a workflow to find the string length of a message, where one of the commands, echo, is defined as an external CWL workflow. External CWL workflows are referenced in ccwl using cwl-workflow. The identifiers and types of the inputs/outputs are read from the YAML specification of the external CWL workflow.

(define echo
  (cwl-workflow "echo.cwl"))

(define string-length
  (command #:inputs file
           #:run "wc" "--chars"
           #:outputs (length #:type stdout)
           #:stdin file))

(workflow ((message #:type string))
  (pipe (echo #:message message)
        (string-length #:file output)))
echo.cwl is defined as
cwlVersion: v1.2
class: CommandLineTool
baseCommand: echo
arguments: ["-n"]
inputs:
  message:
    type: string
    inputBinding:
      position: 1
outputs:
  output:
    type: stdout



4 Contributing

ccwl is developed on GitHub at https://github.com/arunisaac/ccwl. Feedback, suggestions, feature requests, bug reports and pull requests are all welcome. Unclear and unspecific error messages are considered a bug. Do report them!





2dgsh, a shell supporting general directed graph pipelines, has a spell check example.
(made with skribilo)