From d532bb157669651005ae7c76c475ec0798474fc1 Mon Sep 17 00:00:00 2001 From: Ben Sherman Date: Tue, 15 Jul 2025 11:39:38 -0500 Subject: [PATCH 1/3] Workflow onComplete and onError sections Signed-off-by: Ben Sherman --- docs/config.md | 10 +++- docs/developer/plugins.md | 2 + docs/migrations/25-10.md | 25 ++++++++++ docs/notifications.md | 31 ++++++++++++ docs/reference/syntax.md | 22 +++++---- modules/nf-lang/src/main/antlr/ScriptLexer.g4 | 2 + .../nf-lang/src/main/antlr/ScriptParser.g4 | 28 ++++++----- .../script/ast/ScriptVisitorSupport.java | 2 + .../nextflow/script/ast/WorkflowNode.java | 10 +++- .../script/control/ScriptResolveVisitor.java | 2 + .../script/control/ScriptToGroovyVisitor.java | 36 +++++++------- .../script/control/VariableScopeVisitor.java | 3 ++ .../formatter/ScriptFormattingVisitor.java | 16 ++++++- .../script/parser/ScriptAstBuilder.java | 39 ++++++++------- .../formatter/ScriptFormatterTest.groovy | 18 +++++++ tests/checks/.IGNORE-PARSER-V2 | 1 + tests/workflow-oncomplete-v2.nf | 48 +++++++++++++++++++ validation/test.sh | 1 + 18 files changed, 238 insertions(+), 58 deletions(-) create mode 100644 tests/workflow-oncomplete-v2.nf diff --git a/docs/config.md b/docs/config.md index 8c872c09b0..3c29a3d406 100644 --- a/docs/config.md +++ b/docs/config.md @@ -321,9 +321,15 @@ process { This limitation can be avoided by using the {ref}`strict config syntax `. ::: +(config-workflow-handlers)= + ## Workflow handlers -Workflow event handlers can be defined in the config file, which is useful for handling pipeline events without having to modify the pipeline code: +:::{deprecated} 25.10.0 +Use a {ref}`trace observer ` in a plugin to add custom workflow handlers to a pipeline via configuration. +::: + +Workflow event handlers can be defined in the config file: ```groovy workflow.onComplete = { @@ -337,4 +343,4 @@ workflow.onError = { } ``` -See {ref}`workflow-handlers` for more information. +While these handlers can also be defined in the pipeline code, this approach is useful for handling workflow events without modifying the pipeline code. See {ref}`workflow-handlers` for more information. diff --git a/docs/developer/plugins.md b/docs/developer/plugins.md index f9593ff35c..9cd30aa599 100644 --- a/docs/developer/plugins.md +++ b/docs/developer/plugins.md @@ -327,6 +327,8 @@ class MyExecutor extends Executor { } ``` +(plugins-trace-observers)= + ### Trace observers :::{versionchanged} 25.04 diff --git a/docs/migrations/25-10.md b/docs/migrations/25-10.md index 97e8c313e8..d01f0c3524 100644 --- a/docs/migrations/25-10.md +++ b/docs/migrations/25-10.md @@ -8,6 +8,31 @@ This page summarizes the upcoming changes in Nextflow 25.10, which will be relea This page is a work in progress and will be updated as features are finalized. It should not be considered complete until the 25.10 release. ::: +## Enhancements + +

New syntax for workflow handlers

+ +The workflow `onComplete` and `onError` handlers were previously defined by calling `workflow.onComplete` and `workflow.onError` in the pipeline script. These handlers can now be defined as `onComplete` and `onError` sections in an entry workflow: + +```nextflow +workflow { + main: + // ... + + onComplete: + println "workflow complete" + + onError: + println "error: ${workflow.errorMessage}" +} +``` + +This syntax is simpler and easier to use with the {ref}`strict syntax `. See {ref}`workflow-handlers` for details. + ## Breaking changes - The AWS Java SDK used by Nextflow was upgraded from v1 to v2, which introduced some breaking changes to the `aws.client` config options. See {ref}`the guide ` for details. + +## Deprecations + +- The use of workflow handlers in the configuration file has been deprecated. These workflow handlers should be defined in the pipeline script or a plugin instead. See {ref}`config-workflow-handlers` for details. diff --git a/docs/notifications.md b/docs/notifications.md index 9f58f56fd4..433d32dffb 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -23,6 +23,22 @@ workflow.onComplete { } ``` +:::{versionadded} 25.10.0 +::: + +Entry workflows can define an `onComplete` section instead of using `workflow.onComplete`: + +```nextflow +workflow { + main: + // ... + + onComplete: + println "Pipeline completed at: $workflow.complete" + println "Execution status: ${ workflow.success ? 'OK' : 'failed' }" +} +``` + (metadata-error-handler)= ### Error handler @@ -39,6 +55,21 @@ workflow.onError { Both the `onError` and `onComplete` handlers are invoked when an error condition is encountered. The first is called as soon as the error is raised, while the second is called just before the pipeline execution is about to terminate. When using the `finish` {ref}`process-error-strategy`, there may be a significant gap between the two, depending on the time required to complete any pending job. ::: +:::{versionadded} 25.10.0 +::: + +Entry workflows can define an `onError` section instead of using `workflow.onError`: + +```nextflow +workflow { + main: + // ... + + onError: + println "Error: Pipeline execution stopped with the following message: ${workflow.errorMessage}" +} +``` + ## Mail The built-in function `sendMail` allows you to send a mail message from a workflow script. diff --git a/docs/reference/syntax.md b/docs/reference/syntax.md index 688cd52f47..1aec93e106 100644 --- a/docs/reference/syntax.md +++ b/docs/reference/syntax.md @@ -123,7 +123,7 @@ Parameters supplied via command line options, params files, and config files tak A workflow can be a *named workflow* or an *entry workflow*. -A *named workflow* consists of a name and a body, and may consist of a *take*, *main*, *emit*, and *publish* section: +A *named workflow* consists of a name and a body, and may consist of a *take*, *main*, and *emit* section: ```nextflow workflow greet { @@ -146,28 +146,34 @@ workflow greet { - The emit section consists of one or more *emit statements*. An emit statement can be a [variable name](#variable), an [assignment](#assignment), or an [expression statement](#expression-statement). If an emit statement is an expression statement, it must be the only emit. -- The publish section can be specified but is intended to be used in the entry workflow (see below). - -An *entry workflow* has no name and may consist of a *main* and *publish* section: +An *entry workflow* has no name and may consist of a *main*, *publish*, *onComplete*, and *onError* section: ```nextflow workflow { main: greetings = channel.of('Bonjour', 'Ciao', 'Hello', 'Hola') messages = greetings.map { v -> "$v world!" } - greetings.view { it -> '$it world!' } + greetings.view { v -> "$v world!" } publish: - messages >> 'messages' + messages = messages + + onComplete: + log.info 'Workflow completed successfully!' + + onError: + log.error 'Workflow failed.' } ``` - Only one entry workflow may be defined in a script. -- The `main:` section label can be omitted if the publish section is not specified. +- The `main:` section label can be omitted if the other sections not specified. + +- The publish section consists of one or more *publish statements*. A publish statement is an [assignment](#assignment), where the assignment target is the name of a workflow output. -- The publish section consists of one or more *publish statements*. A publish statement is a [right-shift expression](#binary-expressions), where the left-hand side is an expression that refers to a value in the workflow body, and the right-hand side is an expression that returns a string. +- The onComplete and onError sections consist of one or more [statements](#statements). In order for a script to be executable, it must either define an entry workflow or be a code snippet as described [above](#script-declarations). diff --git a/modules/nf-lang/src/main/antlr/ScriptLexer.g4 b/modules/nf-lang/src/main/antlr/ScriptLexer.g4 index 1861bf7278..1f111cbe89 100644 --- a/modules/nf-lang/src/main/antlr/ScriptLexer.g4 +++ b/modules/nf-lang/src/main/antlr/ScriptLexer.g4 @@ -355,6 +355,8 @@ WHEN : 'when'; WORKFLOW : 'workflow'; EMIT : 'emit'; MAIN : 'main'; +ONCOMPLETE : 'onComplete'; +ONERROR : 'onError'; PUBLISH : 'publish'; TAKE : 'take'; diff --git a/modules/nf-lang/src/main/antlr/ScriptParser.g4 b/modules/nf-lang/src/main/antlr/ScriptParser.g4 index f35740d7b9..ad8a36d6f3 100644 --- a/modules/nf-lang/src/main/antlr/ScriptParser.g4 +++ b/modules/nf-lang/src/main/antlr/ScriptParser.g4 @@ -227,29 +227,27 @@ workflowDef workflowBody // explicit main block with optional take/emit blocks - : (sep TAKE COLON nls workflowTakes)? - sep MAIN COLON nls workflowMain - (sep EMIT COLON nls workflowEmits)? - (sep PUBLISH COLON nls workflowPublishers)? + : (sep TAKE COLON nls take=workflowTakes)? + sep MAIN COLON nls main=blockStatements + (sep EMIT COLON nls emit=workflowEmits)? + (sep PUBLISH COLON nls publish=workflowPublishers)? + (sep ONCOMPLETE COLON nls onComplete=blockStatements)? + (sep ONERROR COLON nls onError=blockStatements)? // explicit emit block with optional take/main blocks - | (sep TAKE COLON nls workflowTakes)? - (sep MAIN COLON nls workflowMain)? - sep EMIT COLON nls workflowEmits - (sep PUBLISH COLON nls workflowPublishers)? + | (sep TAKE COLON nls take=workflowTakes)? + (sep MAIN COLON nls main=blockStatements)? + sep EMIT COLON nls emit=workflowEmits + (sep PUBLISH COLON nls publish=workflowPublishers)? // implicit main block - | sep? workflowMain + | sep? main=blockStatements ; workflowTakes : identifier (sep identifier)* ; -workflowMain - : blockStatements - ; - workflowEmits : statement (sep statement)* ; @@ -517,6 +515,8 @@ identifier | WORKFLOW | EMIT | MAIN + | ONCOMPLETE + | ONERROR | PUBLISH | TAKE ; @@ -709,6 +709,8 @@ keywords | WORKFLOW | EMIT | MAIN + | ONCOMPLETE + | ONERROR | PUBLISH | TAKE | NullLiteral diff --git a/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitorSupport.java b/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitorSupport.java index bbb47dd6b8..d0e5ef900b 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitorSupport.java +++ b/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitorSupport.java @@ -66,6 +66,8 @@ public void visitWorkflow(WorkflowNode node) { visit(node.main); visit(node.emits); visit(node.publishers); + visit(node.onComplete); + visit(node.onError); } @Override diff --git a/modules/nf-lang/src/main/java/nextflow/script/ast/WorkflowNode.java b/modules/nf-lang/src/main/java/nextflow/script/ast/WorkflowNode.java index 9e3453488d..06649b252f 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/ast/WorkflowNode.java +++ b/modules/nf-lang/src/main/java/nextflow/script/ast/WorkflowNode.java @@ -43,13 +43,21 @@ public class WorkflowNode extends MethodNode { public final Statement main; public final Statement emits; public final Statement publishers; + public final Statement onComplete; + public final Statement onError; - public WorkflowNode(String name, Statement takes, Statement main, Statement emits, Statement publishers) { + public WorkflowNode(String name, Statement takes, Statement main, Statement emits, Statement publishers, Statement onComplete, Statement onError) { super(name, 0, dummyReturnType(emits), dummyParams(takes), ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE); this.takes = takes; this.main = main; this.emits = emits; this.publishers = publishers; + this.onComplete = onComplete; + this.onError = onError; + } + + public WorkflowNode(String name, Statement main) { + this(name, EmptyStatement.INSTANCE, main, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE); } public boolean isEntry() { diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptResolveVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptResolveVisitor.java index 67f6813db7..090303f97b 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptResolveVisitor.java +++ b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptResolveVisitor.java @@ -88,6 +88,8 @@ public void visitWorkflow(WorkflowNode node) { resolver.visit(node.main); resolver.visit(node.emits); resolver.visit(node.publishers); + resolver.visit(node.onComplete); + resolver.visit(node.onError); } @Override diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyVisitor.java index 08aa004029..0734565149 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyVisitor.java +++ b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyVisitor.java @@ -125,14 +125,17 @@ public void visitParam(ParamNode node) { @Override public void visitWorkflow(WorkflowNode node) { visitWorkflowTakes(node.takes); - visit(node.main); - visitWorkflowEmits(node.emits, node.main); - visitWorkflowPublishers(node.publishers, node.main); + + var main = node.main instanceof BlockStatement block ? block : new BlockStatement(); + visitWorkflowEmits(node.emits, main); + visitWorkflowPublishers(node.publishers, main); + visitWorkflowHandler(node.onComplete, "onComplete", main); + visitWorkflowHandler(node.onError, "onError", main); var bodyDef = stmt(createX( "nextflow.script.BodyDef", args( - closureX(null, node.main), + closureX(null, main), constX(getSourceText(node)), constX("workflow") ) @@ -157,8 +160,7 @@ private void visitWorkflowTakes(Statement takes) { } } - private void visitWorkflowEmits(Statement emits, Statement main) { - var code = (BlockStatement)main; + private void visitWorkflowEmits(Statement emits, BlockStatement main) { for( var stmt : asBlockStatements(emits) ) { var es = (ExpressionStatement)stmt; var emit = es.getExpression(); @@ -167,37 +169,39 @@ private void visitWorkflowEmits(Statement emits, Statement main) { } else if( emit instanceof AssignmentExpression ae ) { var target = (VariableExpression)ae.getLeftExpression(); - code.addStatement(assignS(target, emit)); + main.addStatement(assignS(target, emit)); es.setExpression(callThisX("_emit_", args(constX(target.getName())))); - code.addStatement(es); + main.addStatement(es); } else { var target = varX("$out"); - code.addStatement(assignS(target, emit)); + main.addStatement(assignS(target, emit)); es.setExpression(callThisX("_emit_", args(constX(target.getName())))); - code.addStatement(es); + main.addStatement(es); } } } - private void visitWorkflowPublishers(Statement publishers, Statement main) { - var code = (BlockStatement)main; + private void visitWorkflowPublishers(Statement publishers, BlockStatement main) { for( var stmt : asBlockStatements(publishers) ) { var es = (ExpressionStatement)stmt; var publish = (BinaryExpression)es.getExpression(); var target = asVarX(publish.getLeftExpression()); es.setExpression(callThisX("_publish_", args(constX(target.getName()), publish.getRightExpression()))); - code.addStatement(es); + main.addStatement(es); } } + private void visitWorkflowHandler(Statement code, String name, BlockStatement main) { + if( code instanceof BlockStatement block ) + main.addStatement(stmt(callX(varX("workflow"), name, args(closureX(null, block))))); + } + @Override public void visitProcess(ProcessNode node) { visitProcessDirectives(node.directives); visitProcessInputs(node.inputs); visitProcessOutputs(node.outputs); - visit(node.exec); - visit(node.stub); if( "script".equals(node.type) ) node.exec.visit(new TaskCmdXformVisitor(sourceUnit)); @@ -380,7 +384,7 @@ private Statement processStub(Statement stub) { @Override public void visitFunction(FunctionNode node) { if( RESERVED_NAMES.contains(node.getName()) ) { - syntaxError(node, "`${node.getName()}` is not allowed as a function name because it is reserved for internal use"); + syntaxError(node, "`" + node.getName() + "` is not allowed as a function name because it is reserved for internal use"); return; } moduleNode.getScriptClassDummy().addMethod(node); diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/VariableScopeVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/control/VariableScopeVisitor.java index d46da88e28..679166e458 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/control/VariableScopeVisitor.java +++ b/modules/nf-lang/src/main/java/nextflow/script/control/VariableScopeVisitor.java @@ -185,6 +185,9 @@ public void visitWorkflow(WorkflowNode node) { visitWorkflowOutputs(node.emits, "emit"); visitWorkflowOutputs(node.publishers, "output"); + visit(node.onComplete); + visit(node.onError); + currentDefinition = null; vsc.popScope(); } diff --git a/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java index 5ed37052d0..72d988b5fe 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java +++ b/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java @@ -221,9 +221,11 @@ public void visitWorkflow(WorkflowNode node) { fmt.appendIndent(); fmt.append("take:\n"); visitWorkflowTakes(asBlockStatements(node.takes)); - fmt.appendNewLine(); } if( node.main instanceof BlockStatement ) { + if( node.takes instanceof BlockStatement ) { + fmt.appendNewLine(); + } if( node.takes instanceof BlockStatement || node.emits instanceof BlockStatement || node.publishers instanceof BlockStatement ) { fmt.appendIndent(); fmt.append("main:\n"); @@ -242,6 +244,18 @@ public void visitWorkflow(WorkflowNode node) { fmt.append("publish:\n"); fmt.visit(node.publishers); } + if( node.onComplete instanceof BlockStatement ) { + fmt.appendNewLine(); + fmt.appendIndent(); + fmt.append("onComplete:\n"); + fmt.visit(node.onComplete); + } + if( node.onError instanceof BlockStatement ) { + fmt.appendNewLine(); + fmt.appendIndent(); + fmt.append("onError:\n"); + fmt.visit(node.onError); + } fmt.decIndent(); fmt.append("}\n"); } diff --git a/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java b/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java index e66fe36734..9a5f766108 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java +++ b/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java @@ -507,41 +507,40 @@ private WorkflowNode workflowDef(WorkflowDefContext ctx) { var name = ctx.name != null ? ctx.name.getText() : null; if( ctx.body == null ) { - var result = ast( new WorkflowNode(name, null, null, null, null), ctx ); + var result = ast( new WorkflowNode(name, EmptyStatement.INSTANCE), ctx ); groovydocManager.handle(result, ctx); return result; } - var takes = workflowTakes(ctx.body.workflowTakes()); - var emits = workflowEmits(ctx.body.workflowEmits()); - var publishers = workflowPublishers(ctx.body.workflowPublishers()); - var main = blockStatements( - ctx.body.workflowMain() != null - ? ctx.body.workflowMain().blockStatements() - : null - ); + var takes = workflowTakes(ctx.body.take); + var main = blockSection(ctx.body.main); + var emits = workflowEmits(ctx.body.emit); + var publishers = workflowPublishers(ctx.body.publish); + var onComplete = blockSection(ctx.body.onComplete); + var onError = blockSection(ctx.body.onError); if( name == null ) { - if( takes instanceof BlockStatement ) + if( ctx.body.take != null ) collectSyntaxError(new SyntaxException("Entry workflow cannot have a take section", takes)); - if( emits instanceof BlockStatement ) + if( ctx.body.emit != null ) collectSyntaxError(new SyntaxException("Entry workflow cannot have an emit section", emits)); } if( name != null ) { - if( publishers instanceof BlockStatement ) + if( ctx.body.publish != null ) collectSyntaxError(new SyntaxException("Named workflow cannot have a publish section", publishers)); + if( ctx.body.onComplete != null ) + collectSyntaxError(new SyntaxException("Named workflow cannot have an onComplete section", onComplete)); + if( ctx.body.onError != null ) + collectSyntaxError(new SyntaxException("Named workflow cannot have an onError section", onComplete)); } - var result = ast( new WorkflowNode(name, takes, main, emits, publishers), ctx ); + var result = ast( new WorkflowNode(name, takes, main, emits, publishers, onComplete, onError), ctx ); groovydocManager.handle(result, ctx); return result; } private WorkflowNode workflowDef(BlockStatement main) { - var takes = EmptyStatement.INSTANCE; - var emits = EmptyStatement.INSTANCE; - var publishers = EmptyStatement.INSTANCE; - return new WorkflowNode(null, takes, main, emits, publishers); + return new WorkflowNode(null, main); } private Statement workflowTakes(WorkflowTakesContext ctx) { @@ -560,6 +559,12 @@ private Statement workflowTake(IdentifierContext ctx) { return result; } + private Statement blockSection(BlockStatementsContext ctx) { + if( ctx == null ) + return EmptyStatement.INSTANCE; + return blockStatements(ctx); + } + private Statement workflowEmits(WorkflowEmitsContext ctx) { if( ctx == null ) return EmptyStatement.INSTANCE; diff --git a/modules/nf-lang/src/test/groovy/nextflow/script/formatter/ScriptFormatterTest.groovy b/modules/nf-lang/src/test/groovy/nextflow/script/formatter/ScriptFormatterTest.groovy index effbd7311e..2076aea24d 100644 --- a/modules/nf-lang/src/test/groovy/nextflow/script/formatter/ScriptFormatterTest.groovy +++ b/modules/nf-lang/src/test/groovy/nextflow/script/formatter/ScriptFormatterTest.groovy @@ -132,6 +132,24 @@ class ScriptFormatterTest extends Specification { } ''' ) + + checkFormat( + '''\ + workflow hello{ + take: x ; y ; emit: result = x * y + } + ''', + '''\ + workflow hello { + take: + x + y + + emit: + result = x * y + } + ''' + ) } def 'should format a process definition' () { diff --git a/tests/checks/.IGNORE-PARSER-V2 b/tests/checks/.IGNORE-PARSER-V2 index 24e47da582..c97215480e 100644 --- a/tests/checks/.IGNORE-PARSER-V2 +++ b/tests/checks/.IGNORE-PARSER-V2 @@ -1 +1,2 @@ # TESTS THAT SHOULD ONLY BE RUN BY THE V2 PARSER +workflow-oncomplete-v2.nf \ No newline at end of file diff --git a/tests/workflow-oncomplete-v2.nf b/tests/workflow-oncomplete-v2.nf new file mode 100644 index 0000000000..27927f9052 --- /dev/null +++ b/tests/workflow-oncomplete-v2.nf @@ -0,0 +1,48 @@ +#!/usr/bin/env nextflow +/* + * Copyright 2013-2024, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * This test verify the `workflow.onComplete` defined in the config file + * works OK. + * + * See file `$PWD/checks/workflow-oncomplete.nf/.config` for details + */ + +params.command = 'echo' + +process sayHello { + debug true + + input: + val x + + script: + """ + ${params.command} '${x} world!' + """ +} + +workflow { + main: + channel.of('Bojour', 'Ciao', 'Hello', 'Hola', 'Γεια σου') | sayHello + + onComplete: + log.info('Workflow completed successfully!') + + onError: + log.error('Workflow failed.') +} diff --git a/validation/test.sh b/validation/test.sh index 0096b2984c..c7a40b7857 100755 --- a/validation/test.sh +++ b/validation/test.sh @@ -49,6 +49,7 @@ test_e2e() { # Integration tests # if [[ $TEST_MODE == 'test_integration' ]]; then + export NXF_SYNTAX_PARSER=v1 test_integration ../tests/ test_integration ../tests-v1/ test_e2e From 47130aac84be0fb4237d66ff0e498eaee2ceb7e4 Mon Sep 17 00:00:00 2001 From: Ben Sherman Date: Tue, 5 Aug 2025 13:54:20 -0500 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Chris Hakkaart Signed-off-by: Ben Sherman --- docs/config.md | 4 ++-- docs/migrations/25-10.md | 4 ++-- docs/reference/syntax.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/config.md b/docs/config.md index 3c29a3d406..ed7d303bb4 100644 --- a/docs/config.md +++ b/docs/config.md @@ -329,7 +329,7 @@ This limitation can be avoided by using the {ref}`strict config syntax ` in a plugin to add custom workflow handlers to a pipeline via configuration. ::: -Workflow event handlers can be defined in the config file: +You can define workflow event handlers in the config file: ```groovy workflow.onComplete = { @@ -343,4 +343,4 @@ workflow.onError = { } ``` -While these handlers can also be defined in the pipeline code, this approach is useful for handling workflow events without modifying the pipeline code. See {ref}`workflow-handlers` for more information. +This approach is useful for handling workflow events without modifying the pipeline code. See {ref}`workflow-handlers` for more information. diff --git a/docs/migrations/25-10.md b/docs/migrations/25-10.md index d01f0c3524..95c82b7c2d 100644 --- a/docs/migrations/25-10.md +++ b/docs/migrations/25-10.md @@ -12,7 +12,7 @@ This page is a work in progress and will be updated as features are finalized. I

New syntax for workflow handlers

-The workflow `onComplete` and `onError` handlers were previously defined by calling `workflow.onComplete` and `workflow.onError` in the pipeline script. These handlers can now be defined as `onComplete` and `onError` sections in an entry workflow: +The workflow `onComplete` and `onError` handlers were previously defined by calling `workflow.onComplete` and `workflow.onError` in the pipeline script. You can now define handlers as `onComplete` and `onError` sections in an entry workflow: ```nextflow workflow { @@ -35,4 +35,4 @@ This syntax is simpler and easier to use with the {ref}`strict syntax Date: Wed, 6 Aug 2025 08:34:07 -0500 Subject: [PATCH 3/3] Update docs/reference/syntax.md [ci fast] Co-authored-by: Chris Hakkaart Signed-off-by: Ben Sherman --- docs/reference/syntax.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/syntax.md b/docs/reference/syntax.md index 0a7d5474be..696ef6b42a 100644 --- a/docs/reference/syntax.md +++ b/docs/reference/syntax.md @@ -169,7 +169,7 @@ workflow { - Only one entry workflow may be defined in a script. -- The `main:` section label can be omitted if the other sections not specified. +- The `main:` section label can be omitted if the other sections are not specified. - The publish section consists of one or more *publish statements*. A publish statement is an [assignment](#assignment), where the assignment target is the name of a workflow output.