From 757d56d3d7b6b5a70836aeaf828a810ee56ed2e9 Mon Sep 17 00:00:00 2001 From: Mark Proctor Date: Thu, 30 Apr 2026 11:46:21 +0100 Subject: [PATCH 1/2] fix: move output() from WorkflowInstanceData to WorkflowInstance (#1357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit output() and outputAs() are blocking operations (they join the workflow future) intended for callers outside the event chain — e.g. after instance.start().join(). Exposing them on WorkflowInstanceData, which is what WorkflowExecutionListener implementations see via event.workflowContext().instanceData(), misleads implementors into calling a blocking join from inside a callback. The correct API for accessing output inside onWorkflowCompleted is event.output(), which is populated directly from the task result before the event is published. Removes output() and outputAs() from WorkflowInstanceData. Declares them explicitly on WorkflowInstance, which extends WorkflowInstanceData and is the right type for post-completion callers. Adds WorkflowExecutionListenerOutputTest asserting that event.output() carries the correct value inside onWorkflowCompleted. Closes #1357 Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: fjtirado --- .../impl/WorkflowInstance.java | 4 ++ .../WorkflowExecutionListenerOutputTest.java | 69 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 impl/test/src/test/java/io/serverlessworkflow/impl/test/WorkflowExecutionListenerOutputTest.java diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java index 52312188e..595a49012 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java @@ -29,4 +29,8 @@ public interface WorkflowInstance extends WorkflowInstanceData { boolean cancel(); boolean resume(); + + WorkflowModel output(); + + T outputAs(Class clazz); } diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/WorkflowExecutionListenerOutputTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/WorkflowExecutionListenerOutputTest.java new file mode 100644 index 000000000..792d6d601 --- /dev/null +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/WorkflowExecutionListenerOutputTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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. + */ +package io.serverlessworkflow.impl.test; + +import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowModel; +import io.serverlessworkflow.impl.lifecycle.WorkflowCompletedEvent; +import io.serverlessworkflow.impl.lifecycle.WorkflowExecutionListener; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; + +/** + * Verifies that {@code event.output()} in {@code onWorkflowCompleted} carries the workflow's final + * output. {@code WorkflowCompletedEvent.output()} is the correct API for accessing output inside + * the hook — it is populated directly from the task result before the event is published. + * + *

{@code instanceData().output()} is intentionally absent from {@link + * io.serverlessworkflow.impl.WorkflowInstanceData}: it is a blocking join on the workflow future + * intended for callers outside the event chain (e.g. after {@code instance.start().join()}). + */ +class WorkflowExecutionListenerOutputTest { + + @Test + void eventOutputIsPopulatedInOnWorkflowCompleted() throws IOException { + AtomicReference capturedOutput = new AtomicReference<>(); + + WorkflowExecutionListener listener = + new WorkflowExecutionListener() { + @Override + public void onWorkflowCompleted(WorkflowCompletedEvent event) { + capturedOutput.set(event.output()); + } + }; + + try (WorkflowApplication app = WorkflowApplication.builder().withListener(listener).build()) { + WorkflowModel result = + app.workflowDefinition( + readWorkflowFromClasspath("workflows-samples/simple-expression.yaml")) + .instance(Map.of()) + .start() + .join(); + + assertThat(capturedOutput.get()) + .as("event.output() must be non-null in onWorkflowCompleted") + .isNotNull(); + assertThat(capturedOutput.get().asMap()) + .as("event.output() must equal the workflow's final output") + .isEqualTo(result.asMap()); + } + } +} From 8840a6e17b0ddda5c3c722280e5fa376fe3d3a3a Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti <65240126+fjtirado@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:51:47 +0200 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: fjtirado --- .../impl/WorkflowInstance.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java index 595a49012..a1b92d047 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java @@ -20,8 +20,28 @@ public interface WorkflowInstance extends WorkflowInstanceData { CompletableFuture start(); + /** + * Returns the workflow output. + * + *

This method may block until the workflow execution has completed. Callers should not invoke + * it from lifecycle callbacks, listener threads, or other execution contexts where blocking is + * not safe. + * + * @return the workflow output + */ WorkflowModel output(); + /** + * Returns the workflow output converted to the requested type. + * + *

This method may block until the workflow execution has completed. Callers should not invoke + * it from lifecycle callbacks, listener threads, or other execution contexts where blocking is + * not safe. + * + * @param clazz the target output type + * @param the target output type + * @return the workflow output converted to {@code clazz} + */ T outputAs(Class clazz); boolean suspend(); @@ -29,8 +49,4 @@ public interface WorkflowInstance extends WorkflowInstanceData { boolean cancel(); boolean resume(); - - WorkflowModel output(); - - T outputAs(Class clazz); }