Skip to main content

Scripts

Nextflow is a workflow language that runs on the Java virtual machine (JVM). Nextflow's syntax is very similar to Groovy, a scripting language for the JVM. However, Nextflow is specialized for writing computational pipelines in a declarative manner.

This page is a practical introduction to the Nextflow language. See Syntax for the language grammar, Semantics for the meaning and behavior of each language construct, and Standard library for the built-in types and functions available to every script.

warning

Nextflow uses UTF-8 as the default character encoding for source files. Make sure to use UTF-8 encoding when editing Nextflow scripts with your preferred text editor.

warning

Nextflow scripts have a maximum size of 64 KiB. To avoid this limit for large pipelines, consider moving pipeline components into separate files and including them in the main script.

Hello world

You can use the println function to print to the console:

println 'Hello, World!'

Variables

Variables are declared using the def keyword:

def num = 1
def str = "Hi"

A variable can hold any type of value, and it can be reassigned:

def x = 1
x = 'hello'

Strings

Strings can be defined using single or double quotes (' or " characters):

println "he said 'cheese' once"
println 'he said "cheese!" again'

Strings can be concatenated with +:

def a = "world"
println "hello " + a

The important difference between single- and double-quoted strings is that double-quoted strings support interpolation: the value of a variable can be inserted by prefixing its name with $, and the value of any expression can be inserted with ${expression}:

def foxtype = 'quick'
def foxcolor = ['b', 'r', 'o', 'w', 'n']
println "The $foxtype ${foxcolor.join()} fox"
// prints: The quick brown fox

println '$foxtype is not interpolated'
// prints: $foxtype is not interpolated

A block of text that spans multiple lines can be defined by delimiting it with triple single or double quotes:

def text = """
hello there James
how are you today?
"""

See Strings for more about string behavior, including multi-line strings and escaping.

Lists

Lists are defined using square brackets:

def myList = [1776, -1, 33, 99, 0, 928734928763]

You can access an item in a list with square-bracket notation (indexes start at 0), and get the length of a list with the size method:

println myList[0]
println myList.size()

See List<E> for the set of available list operations.

Maps

Maps are used to store key-value pairs. They are unordered collections of named values, which can each be of a different type:

def scores = ["Brett": 100, "Pete": "Did not finish", "Andrew": 86.87934]

You can access the values in a map by key in two ways:

println scores["Pete"]
println scores.Pete

To add or modify an entry, assign to a key:

scores["Pete"] = 3
scores["Cedric"] = 120
tip

You can also use the + operator to add two maps together, which produces a new map rather than modifying either input. This is a safer way to modify maps when passing them through channels, because any references to the original map won't be affected. See Maps for details.

See Map<K,V> for the set of available map operations.

Records and tuples

Records store a set of related fields, where each field can have its own type. They are created with the record function and accessed by field name:

def person = record(name: 'Alice', age: 42, is_alive: true)
println person.name

Tuples store a fixed sequence of heterogeneous values. They are created with the tuple function and accessed by index, and can be destructured in an assignment:

def coords = tuple(1, 2)
def (x, y) = coords
println "x=$x, y=$y"

Records and tuples are both immutable -- once created, they cannot be modified. See Records and Tuples for more information.

Operators

Operators are symbols that perform specific functions on one or more values. This section highlights some of the most commonly used operators. See Operators for the full set.

note

Operators in this context are different from channel operators, which are the methods used to work with channels. See Dataflow for more information.

The == and != operators test whether two values are equal (or not equal):

assert 2 + 2 == 4
assert [2, 2] != [4]
tip

The assert keyword tests a condition and raises an error if the condition is false. Every assert on this page succeeds when executed.

Comparison operators compare two values:

assert 3 < 3.14             // numbers are compared as numbers
assert 'hello' < 'world' // strings are compared alphabetically

Logical operators perform Boolean logic:

assert (true && false) == false   // logical AND
assert (true || false) == true // logical OR
assert (!true) == false // logical NOT

The in operator tests membership, i.e., whether a collection contains a value:

assert 2 in [1, 2, 3]

Arithmetic operators do math:

assert 2 + 2 == 4
assert 2 * 3 == 6
assert 2 ** 8 == 256 // exponent
assert 7 % 2 == 1 // modulo (division remainder)

Some arithmetic operators can be used with other types of values. For example, + can also concatenate lists, maps, and strings:

assert [1, 2, 3] + [4] == [1, 2, 3, 4]

Conditional execution

If-else statements can be used to execute different code under different conditions:

def x = Math.random()
if( x < 0.5 ) {
println 'You lost.'
}
else {
println 'You won!'
}

In some cases, a conditional can be expressed more concisely as a conditional expression (also known as a ternary expression):

def message = Math.random() < 0.5
? 'You lost.'
: 'You won!'
println message

A shortened version of the conditional expression, known as the Elvis operator, returns the first value if it is truthy, or falls back to a second value otherwise:

def scores = ['A': 1, 'B': 2]
assert (scores['C'] ?: 0) == 0

Regular expressions

Regular expressions can be used to match and extract patterns from strings.

Use =~ to check whether a pattern occurs anywhere in a string, and ==~ to check whether a string matches a pattern exactly:

assert 'hello world' =~ /hello/
assert 'hello' ==~ /hello/

Use the replaceFirst and replaceAll methods to replace matching occurrences in a string:

assert 'colour'.replaceFirst(/ou/, 'o') == 'color'
assert 'cheesecheese'.replaceAll(/cheese/, 'nice') == 'nicenice'

You can use the matcher object returned by =~ to extract capture groups from a string:

def version = '2.7.3-beta'
def m = version =~ /(\d+)\.(\d+)\.(\d+)-?(.+)/

assert m[0] == ['2.7.3-beta', '2', '7', '3', 'beta']
assert m[0][1] == '2'
assert m[0][2] == '7'
assert m[0][3] == '3'
assert m[0][4] == 'beta'

See Regex operators for more information.

Closures

A closure is a function that can be used like a regular value. Typically, closures are passed as arguments to higher-order functions to express computations in a declarative manner.

For example, the following closure takes one parameter named v and returns the square of v:

{ v -> v * v }

This closure can be used with the collect method of a list, which uses it to transform each value in the list and produce a new list:

assert [1, 2, 3, 4].collect { v -> v * v } == [1, 4, 9, 16]

Another example is the each method of a map, which takes a closure with two parameters corresponding to the key and value of each map entry:

["Yue": "Wu", "Mark": "Williams", "Sudha": "Kumari"].each { key, value ->
println "$key $value"
}

Prints:

Yue Wu
Mark Williams
Sudha Kumari

See Closures and Iteration for more about closures and the higher-order functions used to process collections.

Script definitions

The previous sections covered the basic building blocks of Nextflow code: variables, strings, collections, and closures.

In practice, Nextflow scripts are composed of workflows, processes, functions, and types (collectively known as definitions), and can include definitions from other scripts.

To turn a code snippet into a workflow script, wrap it in a workflow block:

workflow {
println 'Hello!'
}

This block is called the entry workflow. It is the entrypoint when the script is executed. Whenever a script contains only simple statements like println 'Hello!', Nextflow treats it as an entry workflow.

You can break up code into functions, for example:

def sayHello() {
println 'Hello!'
}

def add(a, b) {
a + b
}

workflow {
sayHello()
println "2 + 2 = ${add(2, 2)}!"
}

You can organize these definitions further by moving them to a separate script and including them in the main script:

// functions.nf
def sayHello() {
println 'Hello!'
}

def add(a, b) {
a + b
}
// main.nf
include { sayHello; add } from './functions.nf'

workflow {
sayHello()
println "2 + 2 = ${add(2, 2)}!"
}

See Processes, Workflows, and Scripts for more information about how to use these features in your Nextflow scripts.