PHB Code Contributions¶
Theiagen Genomics’ Public Health Bioinformatics (PHB) workflows are written in WDL, a language for specifying data processing workflows with a human-readable and writable syntax. Contributions to the workflows contained in the repository are warmly welcomed.
This document gives coding conventions for the WDL code comprising the workflow and task development for PHB. This style guide evolves over time as additional conventions are identified and past conventions are rendered obsolete by changes in the language itself.
Style guide inspired by Scott Frazer’s WDL Best Practices Style Guide.
General Guidelines¶
Modularity and Metadata
- Best Practice: Place tasks and workflows in separate files to maintain modularity and clarity.
-
Add a
meta
block to every task and workflow to provide a brief description of its purpose.
Docker Containers
-
Use a specific Docker container version instead of 'latest' to ensure reproducibility and prevent unexpected changes in container behavior.
-
Preferentially use containers
Google's Artifact Registry
rather than those fromquay.io
ordockerhub
Indentation and Whitespace
-
Use 2-space indentation for all blocks. Avoid using tabs to ensure uniform formatting across editors:
-
Use a single space when defining variables (
this = that
notthis= that
(unless a bash variable wherethis=that
is required))
Bracket and Spacing Conventions
- Avoid line breaks for opening braces. Keep them on the same line as the declaration. i.e
input {
instead ofinput\n{
- Use single space when defining input/output variables & runtime attributes (
output {
instead ofoutput{
) - Separate non-indented constructs (like input and output sections) with a single-line break for readability.
Command Block Syntax
-
Enclose command blocks in triple angle brackets (<<< ... >>>) for consistency and easier handling of multi-line scripts. It also avoids issues with unescaped special characters in the command block:
Task Blocks¶
A WDL task block defines a discrete, reusable step in a workflow. To ensure readability and consistency, follow these conventions when writing task blocks. Include single spaces between the input, command, output, and runtime sections and their enclosing curly brackets.
The input
block¶
- The following conventions are used to expose docker, CPU, memory, and disk size:
input {
Int cpu = 4 # Number of CPUs
Int disk_size = 100 # Disk space in GB
String docker = "us-docker.pkg.dev/example:1.0.0" # Docker container for the task
Int memory = 16 # Memory in GB
}
-
Include optional tool parameters as inputs to the task
-
Input and output lists should not be formatted to have the equal sign aligned, but instead use a single space before and after the
=
-
Expose Docker as an input, an output (if versioning information not available), and runtime variable:
The command
block¶
-
Ensure use of line breaks between different sections of code to improve readability
-
Split command calls into multiple lines if they have user input variables and/or if the length of the command is very long to avoid text wrapping and/or side-scrolling, e.g.
- Use backslashes for continuation and indentation to clarify structure:
-
Add comments that
- Explain what the optional parameters are
- Provide links to the tool documentation so future readers of the code know where to find that information
-
Explain what non-intuitive bash/python text wrangling actions do, e.g.
The output
block¶
- The output block specifies the files or variables produced by the task. Follow these conventions:
output {
File result_csv = "output.csv" # CSV file generated
File result_log = "log.txt" # Log file
}
-
Ensure the docker container is exposed as an output string, e.g.
The runtime
block¶
- The runtime block defines the compute resources and environment for the task.
-
Always specify a Docker:
Workflow Blocks¶
A WDL workflow block orchestrates the execution of tasks and subworkflows. It defines the inputs, calls tasks or subworkflows, and specifies the final outputs. To ensure readability and consistency, follow these conventions when writing workflow blocks:
The import
section¶
- Include a block of
import
statements (sorted in alphabetical order). -
When a workflow imports a task, ensure it is imported under a unique name to avoid conflicts.
-
Order import statements alphabetically by the path of the imported file.
The input
block¶
- Optional inputs that should be able to be edited by the user, such as docker containers should be exposed on the workflow level as in the example
- In the case of subworkflows, all optional inputs should be exposed on the workflow level so that they can be modified by users on Terra
input {
String input
String task1_docker = "us-docker.pkg.dev/general-theiagen/tool:version"
String? task1_optional_argument
}
The call
sections¶
- Import task files as something other than the included task nam in order to avoid namespace conflicts
The output
block¶
- Define all workflow outputs in this section.
- Use descriptive names for each output variable.
- Order outputs alphabetically by the name of the output variable
output {
# Task 1 outputs
File task1_out_csv = task1.output_csv
String task1_version = task1.version
# Subworkflow outputs
File subworkflow_out_tsv = subworkflow.task3_out_tsv
String subworkflow_version = subworkflow.task3_version
}
Example Workflow formats¶
wf_example_wf.wdl
import "../tasks/task_task1.wdl" as task1_task
import "../tasks/task_task2.wdl" as task2_task
import "../workflows/wf_subworkflow.wdl" as subworkflow
workflow example_wf {
input {
String input
String task1_docker = "us-docker.pkg.dev/general-theiagen/task_1:version"
String task2_docker = "us-docker.pkg.dev/general-theiagen//task_2:version"
String? hidden_task3_argument
String? hidden_task3_docker
String? hidden_task4_docker
}
call task1_task.task1 {
input:
input = input,
docker = task1_docker
}
call task2_task.task2 {
input:
input = input,
docker = task2_docker
}
call subworkflow.subworkflow {
input:
input = input,
task3_argument = hidden_task3_argument,
task3_docker = hidden_task3_docker
task4_docker = hidden_task4_docker
}
output {
# Task 1 outputs
File task1_out_csv = task1.output_csv
String task1_version = task1.version
String task1_docker = task1.docker
# Task 2 outputs
File task2_out_tsv = task2.output_tsv
String task2_version = task2.version
String task2_docker = task2.docker
# Subworkflow outputs for task 3
File task3_out_tsv = subworkflow.task3_out_tsv
String task3_version = subworkflow.task3_version
String task3_docker = subworkflow.task3_docker
# Subworkflow outputs for task 4
String task4_output = subworkflow.task4_output
String task4_version = subworkflow.task4_version
}
}
wf_subworkflow.wdl
import "../tasks/task_task3.wdl" as task3_task
import "../tasks/task_task4.wdl" as task4_task
workflow subworkflow {
input {
String input
# optional inputs for tasks inside subworkflows cannot
# be seen on Terra, so make them available at the subworkflow
# level so they can be modified by a Terra user
String? task3_argument
String? task3_docker
String? task4_docker
}
call task3_task.task3 {
input:
input = input,
args = task3_argument,
docker = task3_docker
}
call task4_task.task4 {
input:
input = task3.output_tsv,
docker = task4_docker
}
output {
File task3_out_tsv = task3.output_tsv
String task3_version = task3.version
String task3_docker = task3.docker
String task4_output = task4.output
String task4_version = task4.version
}
}