From 16763317875fcffce02a0beebf2ea7db5671a113 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) --- .../impl/WorkflowInstance.java | 4 ++ .../impl/WorkflowInstanceData.java | 4 -- .../WorkflowExecutionListenerOutputTest.java | 69 +++++++++++++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) 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 3d17108ea..e9d9ef0c1 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java @@ -25,4 +25,8 @@ public interface WorkflowInstance extends WorkflowInstanceData { boolean cancel(); boolean resume(); + + WorkflowModel output(); + + T outputAs(Class clazz); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstanceData.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstanceData.java index 10a2e0b44..f283f148f 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstanceData.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstanceData.java @@ -28,9 +28,5 @@ public interface WorkflowInstanceData { WorkflowStatus status(); - WorkflowModel output(); - WorkflowModel context(); - - 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 b686d1811dc6b57470c3a23227a894b951de79cd 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> --- .../impl/WorkflowInstance.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 e9d9ef0c1..0efbace21 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java @@ -26,7 +26,27 @@ public interface WorkflowInstance extends WorkflowInstanceData { boolean resume(); + /** + * 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); }