Skip to main content

Preparing for strict syntax

This page explains how to update Nextflow scripts and config files to adhere to the Nextflow language specification, which is enforced by the strict syntax parser.

Overview

The strict syntax parser is a strict implementation of DSL2. While the legacy DSL2 parser allows any Groovy syntax, the strict parser allows only a subset of Groovy syntax for Nextflow scripts and config files. It is used by the language server and nextflow lint to provide more specific error messages when checking Nextflow code.

In Nextflow 25.04 and 25.10, the strict parser is disabled by default when running Nextflow code. You can enable it by setting the NXF_SYNTAX_PARSER environment variable to v2:

export NXF_SYNTAX_PARSER=v2

In Nextflow 26.04 and later, the strict parser is enabled by default. You can disable it by setting NXF_SYNTAX_PARSER to v1:

export NXF_SYNTAX_PARSER=v1

In the future, the legacy parser will be removed and the strict parser will become the only way to run Nextflow code. You can prepare for the strict parser by rewriting unsupported code with supported patterns -- code that runs with the strict parser will also run with the legacy parser.

Starting in Nextflow 25.10, new language features can only be used with the strict parser. Therefore, it is also important to prepare for the strict parser in order to use new language features (e.g., static typing).

This page describes how to migrate the most common unsupported patterns to comply with the strict parser. The extent of required changes will vary depending on the amount of custom Groovy code used within your scripts and config files.

Removed syntax

Import declarations

In Groovy, the import declaration can be used to import external classes:

import groovy.json.JsonSlurper

def json = new JsonSlurper().parseText(json_file.text)

In Nextflow, use the fully qualified name to reference the class:

def json = new groovy.json.JsonSlurper().parseText(json_file.text)

Class declarations

Some users use classes in Nextflow to define helper functions or custom types. Helper functions should be defined as standalone functions in Nextflow. Custom types should be moved to the lib directory.

note

Enums, a special type of class, are supported, but they cannot be included across modules at this time.

note

Record types will be addressed in a future version of the Nextflow language specification.

Mixing script declarations and statements

A Nextflow script may contain any of the following top-level declarations:

  • Feature flags
  • Include declarations
  • Parameter declarations
  • Workflows
  • Processes
  • Functions
  • Type definitions
  • Output block

Alternatively, a script may contain only statements, also known as a code snippet:

println 'Hello world!'

Code snippets are treated as an implicit entry workflow:

workflow {
println 'Hello world!'
}

Script declarations and statements cannot be mixed at the same level. All statements must reside within script declarations unless the script is a code snippet:

process hello {
// ...
}

// incorrect -- move into entry workflow
// println 'Hello world!'

// correct
workflow {
println 'Hello world!'
}
note

Mixing statements and script declarations was necessary in DSL1 and optional in DSL2. However, this pattern is not supported by the strict parser in order to ensure that top-level statements are not executed when the script is included as a module.

Assignment expressions

In Groovy, variables can be assigned in an expression:

hello(x = 1, y = 2)

In Nextflow, assignments are allowed only as statements:

x = 1
y = 2
hello(x, y)

In Groovy, variables can be incremented and decremented in an expression:

hello(x++, y--)

In Nextflow, use += and -= instead:

x += 1
y -= 1
hello(x, y)

For and while loops

In Groovy, loop statements, such as for and while, are supported:

for (rseqc_module in ['read_distribution', 'inner_distance', 'tin']) {
if (rseqc_modules.contains(rseqc_module))
rseqc_modules.remove(rseqc_module)
}

In Nextflow, use higher-order functions, such as the each method, instead:

['read_distribution', 'inner_distance', 'tin'].each { rseqc_module ->
if (rseqc_modules.contains(rseqc_module))
rseqc_modules.remove(rseqc_module)
}

Lists, maps, and sets provide several functions (e.g., collect, find, findAll, inject) for iteration. See Groovy standard library for more information.

Switch statements

In Groovy, switch statements are used for pattern matching on a value:

switch (aligner) {
case 'bowtie2':
// ...
break
case 'bwamem':
// ...
break
case 'dragmap':
// ...
break
case 'snap':
// ...
break
default:
// ...
}

In Nextflow, use if-else statements instead:

if (aligner == 'bowtie2') {
// ...
} else if (aligner == 'bwamem') {
// ...
} else if (aligner == 'dragmap') {
// ...
} else if (aligner == 'snap') {
// ...
} else {
// ...
}

Spread operator

In Groovy, the spread operator can be used to flatten a nested list:

ch.map { meta, bambai -> [meta, *bambai] }

In Nextflow, enumerate the list elements explicitly:

// alternative 1
ch.map { meta, bambai -> [meta, bambai[0], bambai[1]] }

// alternative 2
ch.map { meta, bambai ->
def (bam, bai) = bambai
[meta, bam, bai]
}

Implicit environment variables

In Nextflow DSL1 and DSL2, environment variables can be referenced directly in strings:

println "PWD = ${PWD}"

Use System.getenv() instead:

println "PWD = ${System.getenv('PWD')}"
Added in version 24.04.0

The env() function should be used instead of System.getenv():

println "PWD = ${env('PWD')}"

Restricted syntax

The following patterns are still supported but have been restricted. That is, some syntax variants have been removed.

Include declarations

In Nextflow DSL2, include declarations can have an addParams or params clause:

params.message = 'Hola'
params.target = 'Mundo'

include { sayHello } from './some/module' addParams(message: 'Ciao')

workflow {
sayHello()
}

These clauses are no longer supported by the strict parser. Params should be passed to workflows, processes, and functions as explicit inputs:

include { sayHello } from './some/module'

params.message = 'Hola'
params.target = 'Mundo'

workflow {
sayHello('Ciao', params.target)
}

Where the sayHello workflow is defined as follows:

workflow sayHello {
take:
message
target

main:
// ...
}

Variable declarations

In Groovy, variables can be declared in many different ways:

def a = 1
final b = 2
def c = 3, d = 4
def (e, f) = [5, 6]
String str = 'hello'
def Map meta = [:]

In Nextflow, variables must be declared with def and must not specify a type:

def a = 1
def b = 2
def (c, d) = [3, 4]
def (e, f) = [5, 6]
def str = 'hello'
def meta = [:]

< AddedInVersion version="25.10.0" />

Local variables can be declared with a type annotation:

def a: Integer = 1
def b: Integer = 2
def (c: Integer, d: Integer) = [3, 4]
def (e: Integer, f: Integer) = [5, 6]
def str: String = 'hello'
def meta: Map = [:]

Groovy-style type annotations are still supported. However, the language server and nextflow lint will automatically convert them to Nextflow-style type annotations when formatting code. Groovy-style type annotations will not be supported in a future version.

Strings

Groovy supports a wide variety of strings, including multi-line strings, dynamic strings, slashy strings, multi-line dynamic slashy strings, and more.

Nextflow supports single- and double-quoted strings, multi-line strings, and slashy strings.

Slashy strings cannot be interpolated:

def id = 'SRA001'
assert 'SRA001.fastq' ~= /${id}\.f(?:ast)?q/

Use a double-quoted string instead:

def id = 'SRA001'
assert 'SRA001.fastq' ~= "${id}\\.f(?:ast)?q"

Slashy strings cannot span multiple lines:

/
Patterns in the code,
Symbols dance to match and find,
Logic unconfined.
/

Use a multi-line string instead:

"""
Patterns in the code,
Symbols dance to match and find,
Logic unconfined.
"""

Dollar slashy strings are not supported:

$/
echo "Hello world!"
/$

Use a multi-line string instead:

"""
echo "Hello world!"
"""

Type conversions

In Groovy, there are two ways to perform type conversions or casts:

def map = (Map) readJson(json)  // soft cast
def map = readJson(json) as Map // hard cast

In Nextflow, only hard casts are supported. Use an explicit method to cast a value to a different type if one is available. For example, to parse a string as a number:

def x = '42' as Integer
def x = '42'.toInteger() // preferred

Process env inputs and outputs

In Nextflow DSL2, the name of a process env input/output can be specified with or without quotes:

process my_task {
input:
env FOO
env 'BAR'

// ...
}

The strict parser requires the name to be specified with quotes:

process my_task {
input:
env 'FOO'
env 'BAR'

// ...
}

Implicit process script section

In Nextflow DSL1 and DSL2, the process script: section label can almost always be omitted:

process greet {
input:
val greeting

"""
echo '${greeting}!'
"""
}

The strict parser requires the script: label to be specified unless there are no other sections:

process hello {
"""
echo 'Hello world!'
"""
}

process greet {
input:
val greeting

script:
"""
echo '${greeting}!'
"""
}

Workflow onComplete/onError handlers

Workflow handlers (i.e. workflow.onComplete and workflow.onError) can be defined in several different ways in a script, but are typically defined as top-level statements and without an equals sign:

workflow.onComplete {
println "Pipeline completed at: $workflow.complete"
println "Execution status: ${ workflow.success ? 'OK' : 'failed' }"
}

The strict parser does not allow statements to be mixed with script declarations, so workflow handlers must be defined in the entry workflow:

workflow {
// ...

workflow.onComplete = {
println "Pipeline completed at: $workflow.complete"
println "Execution status: ${ workflow.success ? 'OK' : 'failed' }"
}
}
Added in version 25.10.0

Workflow handlers can be specified as sections in the entry workflow:

workflow {
main:
// ...

onComplete:
println "Pipeline completed at: $workflow.complete"
println "Execution status: ${ workflow.success ? 'OK' : 'failed' }"
}

See Workflow handlers for details.

Deprecated syntax

The following patterns are deprecated, and the strict parser reports warnings for them. These warnings will become errors in the future.

channel vs Channel

Channel factories should be accessed using the channel namespace instead of the Channel type:

Channel.of(1, 2, 3) // incorrect
channel.of(1, 2, 3) // correct

See channel and Channel<E> for more information.

Implicit closure parameter

In Groovy, a closure with no parameters is assumed to have a single parameter named it:

ch.map { it * 2 }

In Nextflow, the closure parameter should be explicitly declared:

ch.map { v -> v * 2 }   // correct
ch.map { it -> it * 2 } // also correct

Process shell section

The process shell section is deprecated. Use the script section instead. The strict parser provides error checking to help distinguish between Nextflow variables and Bash variables.

Best practices

The following patterns are discouraged and may become warnings or errors in future Nextflow versions. The language server can detect these patterns, but does not report them by default.

To enable these checks, set Nextflow > Error reporting mode to paranoid in the extension settings.

Using legacy parameter declarations

Added in version 25.10.0

Legacy parameters can automatically cast CLI parameters to numbers and booleans:

params.save_intermeds = true

workflow {
println "save_intermeds = ${params.save_intermeds ? 'true' : 'false'}"
}
$ NXF_SYNTAX_PARSER=v1 nextflow run main.nf --save_intermeds false
save_intermeds = false

However, this type detection is disabled when using the strict parser. In the above example, params.save_intermeds will be set to 'false' instead of false, causing it to be truthy:

$ NXF_SYNTAX_PARSER=v2 nextflow run main.nf --save_intermeds false
save_intermeds = true

Legacy parameters should not rely on CLI type detection when using the strict parser. Parameters that may be supplied on the command line should be treated as strings:

params.save_intermeds = 'true'

workflow {
println "save_intermeds = ${params.save_intermeds.toBoolean() ? 'true' : 'false'}"
}
$ NXF_SYNTAX_PARSER=v2 nextflow run main.nf --save_intermeds false
save_intermeds = false

Alternatively, use the params block to convert CLI parameters based on their type annotations:

params {
save_intermeds: Boolean = true
}

workflow {
println "save_intermeds = ${params.save_intermeds ? 'true' : 'false'}"
}

See Typed parameters for details.

Using params outside the entry workflow

While params can be used anywhere in the pipeline code, they are only intended to be used in the entry workflow and the output block.

As a best practice, processes and workflows should receive params as explicit inputs:

process myproc {
input:
val myproc_args

// ...
}

workflow myflow {
take:
myflow_args

// ...
}

workflow {
myproc(params.myproc_args)
myflow(params.myflow_args)
}

Process when section

The process when section is discouraged. As a best practice, conditional logic should be implemented in the calling workflow (e.g. using an if statement or filter operator) instead of the process definition.

Configuration syntax

See Configuration for a comprehensive description of the configuration language.

Mixing config statements and scripting statements

The legacy parser treats config files as Groovy scripts, allowing the use of scripting constructs like variables, helper functions, try-catch blocks, and conditional logic for dynamic configuration:

def getHostname() {
// ...
}

def hostname = getHostname()
if (hostname == 'small') {
params.max_memory = 32.GB
params.max_cpus = 8
}
else if (hostname == 'large') {
params.max_memory = 128.GB
params.max_cpus = 32
}

The strict parser only allows config assignments, config blocks, and config includes. Function declarations are not supported. Statements (e.g., variables and if statements) can only be used within closures. The same dynamic configuration can be achieved using a dynamic include:

includeConfig ({
def hostname = // ...
if (hostname == 'small')
return 'small.config'
else if (hostname == 'large')
return 'large.config'
else
return '/dev/null'
}())

The include source is a closure that is immediately invoked. It includes a different config file based on the return value of the closure. Including /dev/null is equivalent to including nothing.

Each conditional configuration is defined in a separate config file:

// small.config
params.max_memory = 32.GB
params.max_cpus = 8
// large.config
params.max_memory = 128.GB
params.max_cpus = 32

Referencing config settings as variables

The legacy parser allows config settings to be referenced like variables:

google.location = "us-west1"
google.batch.subnetwork = "regions/${google.location}/subnetworks/default"

The strict parser does not support this. Only params can be referenced as variables:

params.location = "us-west1"

google.location = params.location
google.batch.subnetwork = "regions/${params.location}/subnetworks/default"

Preserving Groovy code

There are two ways to preserve Groovy code:

  • Move the code to the lib directory
  • Create a plugin

Any Groovy code can be moved into the lib directory, which supports the full Groovy language. This approach is useful for temporarily preserving some Groovy code until it can be updated later and incorporated into a Nextflow script. See The lib directory for more information.

For Groovy code that is complicated or if it depends on third-party libraries, it may be better to create a plugin. Plugins can define custom functions that can be included by Nextflow scripts like a module. Furthermore, plugins can be easily re-used across different pipelines. See Developing plugins for more information on how to develop plugins.