diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 42a07c878..efeb39391 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -216,4 +216,10 @@ jobs:
run: |
mm.py README.md
env:
- DOCKER_HOST: ${{steps.setup_docker.outputs.sock}}
+ DOCKER_HOST: ${{steps.setup_docker.outputs.sock}}
+ - name: Validate Spring Boot Workflow examples
+ working-directory: ./spring-boot-examples/workflows
+ run: |
+ mm.py README.md
+ env:
+ DOCKER_HOST: ${{steps.setup_docker.outputs.sock}}
diff --git a/spring-boot-examples/pom.xml b/spring-boot-examples/pom.xml
index 84ebb6c51..0b333b703 100644
--- a/spring-boot-examples/pom.xml
+++ b/spring-boot-examples/pom.xml
@@ -21,6 +21,7 @@
producer-app
consumer-app
+ workflows
diff --git a/spring-boot-examples/workflows/README.md b/spring-boot-examples/workflows/README.md
new file mode 100644
index 000000000..a2004b2db
--- /dev/null
+++ b/spring-boot-examples/workflows/README.md
@@ -0,0 +1,403 @@
+# Dapr Spring Boot Workflow Examples
+
+This application allows you to run different workflow patterns including:
+- Chained Activities
+- Parent/Child Workflows
+- Continue workflow by sending External Events
+- Fan Out/In activities for parallel execution
+
+## Running these examples from source code
+
+To run these examples you will need:
+- Java SDK
+- Maven
+- Docker or a container runtime such as Podman
+
+From the `spring-boot-examples/workflows` directory you can start the service using the test configuration that uses
+[Testcontainers](https://testcontainers.com) to boostrap [Dapr](https://dapr.io) by running the following command:
+
+
+
+
+```sh
+../../mvnw spring-boot:test-run
+```
+
+
+
+Once the application is running you can trigger the different patterns by sending the following requests:
+
+### Chaining Activities Workflow example
+
+The `io.dapr.springboot.examples.wfp.chain.ChainWorkflow` executes three chained activities. For this example the
+`ToUpperCaseActivity.java` is used to transform to upper case three strings from an array.
+
+```mermaid
+graph LR
+ SW((Start
+ Workflow))
+ A1[Activity1]
+ A2[Activity2]
+ A3[Activity3]
+ EW((End
+ Workflow))
+ SW --> A1
+ A1 --> A2
+ A2 --> A3
+ A3 --> EW
+```
+
+
+
+
+To start the workflow with the three chained activities you can run:
+
+```sh
+curl -X POST localhost:8080/wfp/chain -H 'Content-Type: application/json'
+```
+
+
+
+
+As result from executing the request you should see:
+
+```bash
+TOKYO, LONDON, SEATTLE
+```
+
+In the application output you should see the workflow activities being executed.
+
+```bash
+io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.chain.ChainWorkflow
+i.d.s.e.w.WorkflowPatternsRestController : Workflow instance 7625b4af-8c04-408a-93dc-bad753466e43 started
+i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity
+i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: Tokyo
+i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: TOKYO
+i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity
+i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: London
+i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: LONDON
+i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity
+i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: Seattle
+i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: SEATTLE
+io.dapr.workflows.WorkflowContext : Workflow finished with result: TOKYO, LONDON, SEATTLE
+```
+
+### Parent / Child Workflows example
+
+In this example we start a Parent workflow that calls a child workflow that execute one activity that reverse the .
+
+The Parent workflow looks like this:
+
+```mermaid
+graph LR
+ SW((Start
+ Workflow))
+ subgraph for each word in the input
+ GWL[Call child workflow]
+ end
+ EW((End
+ Workflow))
+ SW --> GWL
+ GWL --> EW
+```
+
+The Child workflow looks like this:
+
+```mermaid
+graph LR
+ SW((Start
+ Workflow))
+ A1[Activity1]
+ EW((End
+ Workflow))
+ SW --> A1
+ A1 --> EW
+```
+
+To start the parent workflow you can run:
+
+
+
+
+To start the workflow with the three chained activities you can run:
+
+```sh
+curl -X POST localhost:8080/wfp/child -H 'Content-Type: application/json'
+```
+
+
+
+
+As result from executing the request you should see:
+
+```bash
+!wolfkroW rpaD olleH
+```
+
+In the application output you should see the workflow activities being executed.
+
+```bash
+io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.child.ParentWorkflow
+io.dapr.workflows.WorkflowContext : calling childworkflow with input: Hello Dapr Workflow!
+i.d.s.e.w.WorkflowPatternsRestController : Workflow instance f3ec9566-a0fc-4d28-8912-3f3ded3cd8a9 started
+io.dapr.workflows.WorkflowContext : Starting ChildWorkflow: io.dapr.springboot.examples.wfp.child.ChildWorkflow
+io.dapr.workflows.WorkflowContext : ChildWorkflow received input: Hello Dapr Workflow!
+io.dapr.workflows.WorkflowContext : ChildWorkflow is calling Activity: io.dapr.springboot.examples.wfp.child.ReverseActivity
+i.d.s.e.wfp.child.ReverseActivity : Starting Activity: io.dapr.springboot.examples.wfp.child.ReverseActivity
+i.d.s.e.wfp.child.ReverseActivity : Message Received from input: Hello Dapr Workflow!
+i.d.s.e.wfp.child.ReverseActivity : Sending message to output: !wolfkroW rpaD olleH
+io.dapr.workflows.WorkflowContext : ChildWorkflow finished with: !wolfkroW rpaD olleH
+io.dapr.workflows.WorkflowContext : childworkflow finished with: !wolfkroW rpaD olleH
+```
+
+### ContinueAsNew Workflows example
+
+In this example we start a workflow that every 3 seconds schedule a new workflow consistently. This workflow executes
+one activity called CleanUpActivity that takes 2 seconds to complete. This loops repeat consistently for 5 times.
+
+To start the workflow you can run:
+
+
+
+
+To start the workflow you can run:
+
+```sh
+curl -X POST localhost:8080/wfp/continueasnew -H 'Content-Type: application/json'
+```
+
+
+
+As result from executing the request you should see:
+
+```bash
+{"cleanUpTimes":5}
+````
+
+In the application output you should see the workflow activities being executed.
+
+```bash
+io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow
+io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up
+i.d.s.e.w.WorkflowPatternsRestController : Workflow instance b808e7d6-ab47-4eba-8188-dc9ff8780764 started
+i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity
+i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:48:45
+io.dapr.workflows.WorkflowContext : CleanUpActivity finished
+io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up
+io.dapr.workflows.WorkflowContext : Let's do more cleaning.
+io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow
+io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up
+i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity
+i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:48:50
+io.dapr.workflows.WorkflowContext : CleanUpActivity finished
+io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up
+io.dapr.workflows.WorkflowContext : Let's do more cleaning.
+io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow
+io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up
+i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity
+i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:48:55
+io.dapr.workflows.WorkflowContext : CleanUpActivity finished
+io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up
+io.dapr.workflows.WorkflowContext : Let's do more cleaning.
+io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow
+io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up
+i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity
+i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:49:0
+io.dapr.workflows.WorkflowContext : CleanUpActivity finished
+io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up
+io.dapr.workflows.WorkflowContext : Let's do more cleaning.
+io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow
+io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up
+i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity
+i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:49:5
+io.dapr.workflows.WorkflowContext : CleanUpActivity finished
+io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up
+io.dapr.workflows.WorkflowContext : We did enough cleaning
+```
+
+### External Event Workflow example
+
+In this example we start a workflow that as part of its execution waits for an external event to continue. To correlate
+workflows and events we use the parameter `orderId`
+
+To start the workflow you can run:
+
+
+
+
+To start the workflow you can run:
+
+```sh
+curl -X POST "localhost:8080/wfp/externalevent?orderId=123" -H 'Content-Type: application/json'
+```
+
+
+
+
+In the application output you should see the workflow activities being executed.
+
+```bash
+io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.externalevent.ExternalEventWorkflow
+io.dapr.workflows.WorkflowContext : Waiting for approval...
+i.d.s.e.w.WorkflowPatternsRestController : Workflow instance 8a55cf6d-9059-49b1-8c83-fbe17567a02e started
+```
+
+You should see the Workflow ID that was created, in this example you don't need to remember this id,
+as you can use the orderId to find the right instance.
+When you are ready to approve the order you can send the following request:
+
+
+
+
+To send the event you can run:
+
+```sh
+curl -X POST "localhost:8080/wfp/externalevent-continue?orderId=123&decision=true" -H 'Content-Type: application/json'
+```
+
+
+
+```bash
+{"approved":true}
+```
+
+In the application output you should see the workflow activities being executed.
+
+```bash
+i.d.s.e.w.WorkflowPatternsRestController : Workflow instance e86bc464-6166-434d-8c91-d99040d6f54e continue
+io.dapr.workflows.WorkflowContext : approval granted - do the approved action
+i.d.s.e.w.externalevent.ApproveActivity : Starting Activity: io.dapr.springboot.examples.wfp.externalevent.ApproveActivity
+i.d.s.e.w.externalevent.ApproveActivity : Running approval activity...
+io.dapr.workflows.WorkflowContext : approval-activity finished
+```
+
+### Fan Out/In Workflow example
+
+In this example we start a workflow that takes an ArrayList of strings and calls one activity per item in the ArrayList. The activities
+are executed and the workflow waits for all of them to complete to aggregate the results.
+
+```mermaid
+graph LR
+ SW((Start
+ Workflow))
+ subgraph for each word in the input
+ GWL[GetWordLength]
+ end
+ ALL[Wait until all tasks
+ are completed]
+ EW((End
+ Workflow))
+ SW --> GWL
+ GWL --> ALL
+ ALL --> EW
+```
+
+To start the workflow you can run:
+
+
+
+
+To start the workflow you can run:
+
+```sh
+curl -X POST localhost:8080/wfp/fanoutin -H 'Content-Type: application/json' -d @body.json
+```
+
+
+
+As result from executing the request you should see:
+
+```bash
+{"wordCount":60}
+```
+
+In the application output you should see the workflow activities being executed.
+
+```bash
+io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.fanoutin.FanOutInWorkflow
+i.d.s.e.w.WorkflowPatternsRestController : Workflow instance a771a7ba-f9fb-4399-aaee-a2fb0b102e5d started
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 2.
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 11.
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 17.
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 21.
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 9.
+i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished
+io.dapr.workflows.WorkflowContext : Workflow finished with result: 60
+```
+
+
+## Testing workflow executions
+
+Workflow execution can be tested using Testcontainers and you can find all the tests for the patterns covered in this
+application [here](test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java).
\ No newline at end of file
diff --git a/spring-boot-examples/workflows/body.json b/spring-boot-examples/workflows/body.json
new file mode 100644
index 000000000..55471e370
--- /dev/null
+++ b/spring-boot-examples/workflows/body.json
@@ -0,0 +1,5 @@
+["Hello, world!",
+ "The quick brown fox jumps over the lazy dog.",
+ "If a tree falls in the forest and there is no one there to hear it, does it make a sound?",
+ "The greatest glory in living lies not in never falling, but in rising every time we fall.",
+ "Always remember that you are absolutely unique. Just like everyone else."]
\ No newline at end of file
diff --git a/spring-boot-examples/workflows/pom.xml b/spring-boot-examples/workflows/pom.xml
new file mode 100644
index 000000000..4711b799b
--- /dev/null
+++ b/spring-boot-examples/workflows/pom.xml
@@ -0,0 +1,68 @@
+
+
+ 4.0.0
+
+
+ io.dapr
+ spring-boot-examples
+ 0.16.0-SNAPSHOT
+
+
+ workflows
+ workflows
+ Spring Boot, Testcontainers and Dapr Integration Examples :: Workflows
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter-test
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
+
+ true
+
+
+
+
+
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsApplication.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsApplication.java
new file mode 100644
index 000000000..378295a2f
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsApplication.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2025 The Dapr 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.dapr.springboot.examples.wfp;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+public class WorkflowPatternsApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(WorkflowPatternsApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java
new file mode 100644
index 000000000..cf1998907
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2025 The Dapr 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.dapr.springboot.examples.wfp;
+
+import io.dapr.spring.workflows.config.EnableDaprWorkflows;
+import io.dapr.springboot.examples.wfp.chain.ChainWorkflow;
+import io.dapr.springboot.examples.wfp.child.ParentWorkflow;
+import io.dapr.springboot.examples.wfp.continueasnew.CleanUpLog;
+import io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow;
+import io.dapr.springboot.examples.wfp.externalevent.Decision;
+import io.dapr.springboot.examples.wfp.externalevent.ExternalEventWorkflow;
+import io.dapr.springboot.examples.wfp.fanoutin.FanOutInWorkflow;
+import io.dapr.springboot.examples.wfp.fanoutin.Result;
+import io.dapr.workflows.client.DaprWorkflowClient;
+import io.dapr.workflows.client.WorkflowInstanceStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeoutException;
+
+@RestController
+@EnableDaprWorkflows
+public class WorkflowPatternsRestController {
+
+ private final Logger logger = LoggerFactory.getLogger(WorkflowPatternsRestController.class);
+
+ @Autowired
+ private DaprWorkflowClient daprWorkflowClient;
+
+ private Map ordersToApprove = new HashMap<>();
+
+ @Bean
+ public CleanUpLog cleanUpLog(){
+ return new CleanUpLog();
+ }
+
+ /**
+ * Run Chain Demo Workflow
+ * @return the output of the ChainWorkflow execution
+ */
+ @PostMapping("wfp/chain")
+ public String chain() throws TimeoutException {
+ String instanceId = daprWorkflowClient.scheduleNewWorkflow(ChainWorkflow.class);
+ logger.info("Workflow instance " + instanceId + " started");
+ return daprWorkflowClient
+ .waitForInstanceCompletion(instanceId, Duration.ofSeconds(2), true)
+ .readOutputAs(String.class);
+ }
+
+
+ /**
+ * Run Child Demo Workflow
+ * @return confirmation that the workflow instance was created for the workflow pattern child
+ */
+ @PostMapping("wfp/child")
+ public String child() throws TimeoutException {
+ String instanceId = daprWorkflowClient.scheduleNewWorkflow(ParentWorkflow.class);
+ logger.info("Workflow instance " + instanceId + " started");
+ return daprWorkflowClient
+ .waitForInstanceCompletion(instanceId, Duration.ofSeconds(2), true)
+ .readOutputAs(String.class);
+ }
+
+
+ /**
+ * Run Fan Out/in Demo Workflow
+ * @return confirmation that the workflow instance was created for the workflow pattern faninout
+ */
+ @PostMapping("wfp/fanoutin")
+ public Result faninout(@RequestBody List listOfStrings) throws TimeoutException {
+
+ String instanceId = daprWorkflowClient.scheduleNewWorkflow(FanOutInWorkflow.class, listOfStrings);
+ logger.info("Workflow instance " + instanceId + " started");
+
+ // Block until the orchestration completes. Then print the final status, which includes the output.
+ WorkflowInstanceStatus workflowInstanceStatus = daprWorkflowClient.waitForInstanceCompletion(
+ instanceId,
+ Duration.ofSeconds(30),
+ true);
+ logger.info("workflow instance with ID: %s completed with result: %s%n", instanceId,
+ workflowInstanceStatus.readOutputAs(Result.class));
+ return workflowInstanceStatus.readOutputAs(Result.class);
+ }
+
+ /**
+ * Run External Event Workflow Pattern
+ * @return confirmation that the workflow instance was created for the workflow pattern externalevent
+ */
+ @PostMapping("wfp/externalevent")
+ public String externalevent(@RequestParam("orderId") String orderId) {
+ String instanceId = daprWorkflowClient.scheduleNewWorkflow(ExternalEventWorkflow.class);
+ ordersToApprove.put(orderId, instanceId);
+ logger.info("Workflow instance " + instanceId + " started");
+ return instanceId;
+ }
+
+ @PostMapping("wfp/externalevent-continue")
+ public Decision externaleventContinue(@RequestParam("orderId") String orderId, @RequestParam("decision") Boolean decision)
+ throws TimeoutException {
+ String instanceId = ordersToApprove.get(orderId);
+ logger.info("Workflow instance " + instanceId + " continue");
+ daprWorkflowClient.raiseEvent(instanceId, "Approval", decision);
+ WorkflowInstanceStatus workflowInstanceStatus = daprWorkflowClient
+ .waitForInstanceCompletion(instanceId, null, true);
+ return workflowInstanceStatus.readOutputAs(Decision.class);
+ }
+
+ @PostMapping("wfp/continueasnew")
+ public CleanUpLog continueasnew()
+ throws TimeoutException {
+ String instanceId = daprWorkflowClient.scheduleNewWorkflow(ContinueAsNewWorkflow.class);
+ logger.info("Workflow instance " + instanceId + " started");
+
+ WorkflowInstanceStatus workflowInstanceStatus = daprWorkflowClient.waitForInstanceCompletion(instanceId, null, true);
+ System.out.printf("workflow instance with ID: %s completed.", instanceId);
+ return workflowInstanceStatus.readOutputAs(CleanUpLog.class);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ChainWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ChainWorkflow.java
new file mode 100644
index 000000000..d1967247f
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ChainWorkflow.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 The Dapr 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.dapr.springboot.examples.wfp.chain;
+
+import io.dapr.workflows.Workflow;
+import io.dapr.workflows.WorkflowStub;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ChainWorkflow implements Workflow {
+ @Override
+ public WorkflowStub create() {
+ return ctx -> {
+ ctx.getLogger().info("Starting Workflow: " + ctx.getName());
+
+ String result = "";
+ result += ctx.callActivity(ToUpperCaseActivity.class.getName(), "Tokyo", String.class).await() + ", ";
+ result += ctx.callActivity(ToUpperCaseActivity.class.getName(), "London", String.class).await() + ", ";
+ result += ctx.callActivity(ToUpperCaseActivity.class.getName(), "Seattle", String.class).await();
+
+ ctx.getLogger().info("Workflow finished with result: " + result);
+ ctx.complete(result);
+ };
+ }
+}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ToUpperCaseActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ToUpperCaseActivity.java
new file mode 100644
index 000000000..6280bcf71
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ToUpperCaseActivity.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 The Dapr 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.dapr.springboot.examples.wfp.chain;
+
+import io.dapr.workflows.WorkflowActivity;
+import io.dapr.workflows.WorkflowActivityContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ToUpperCaseActivity implements WorkflowActivity {
+
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ Logger logger = LoggerFactory.getLogger(ToUpperCaseActivity.class);
+ logger.info("Starting Activity: " + ctx.getName());
+
+ var message = ctx.getInput(String.class);
+ var newMessage = message.toUpperCase();
+
+ logger.info("Message Received from input: " + message);
+ logger.info("Sending message to output: " + newMessage);
+
+ return newMessage;
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ChildWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ChildWorkflow.java
new file mode 100644
index 000000000..3267337b1
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ChildWorkflow.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Dapr 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.dapr.springboot.examples.wfp.child;
+
+import io.dapr.workflows.Workflow;
+import io.dapr.workflows.WorkflowStub;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ChildWorkflow implements Workflow {
+ @Override
+ public WorkflowStub create() {
+ return ctx -> {
+ ctx.getLogger().info("Starting ChildWorkflow: " + ctx.getName());
+
+ var childWorkflowInput = ctx.getInput(String.class);
+ ctx.getLogger().info("ChildWorkflow received input: " + childWorkflowInput);
+
+ ctx.getLogger().info("ChildWorkflow is calling Activity: " + ReverseActivity.class.getName());
+ String result = ctx.callActivity(ReverseActivity.class.getName(), childWorkflowInput, String.class).await();
+
+ ctx.getLogger().info("ChildWorkflow finished with: " + result);
+ ctx.complete(result);
+ };
+ }
+}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ParentWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ParentWorkflow.java
new file mode 100644
index 000000000..d5d58d3c9
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ParentWorkflow.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Dapr 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.dapr.springboot.examples.wfp.child;
+
+import io.dapr.workflows.Workflow;
+import io.dapr.workflows.WorkflowStub;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ParentWorkflow implements Workflow {
+ @Override
+ public WorkflowStub create() {
+ return ctx -> {
+ ctx.getLogger().info("Starting Workflow: " + ctx.getName());
+
+ var childWorkflowInput = "Hello Dapr Workflow!";
+ ctx.getLogger().info("calling childworkflow with input: " + childWorkflowInput);
+
+ var childWorkflowOutput =
+ ctx.callChildWorkflow(ChildWorkflow.class.getName(), childWorkflowInput, String.class).await();
+
+ ctx.getLogger().info("childworkflow finished with: " + childWorkflowOutput);
+ ctx.complete(childWorkflowOutput);
+ };
+ }
+}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ReverseActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ReverseActivity.java
new file mode 100644
index 000000000..af483cdfb
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ReverseActivity.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Dapr 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.dapr.springboot.examples.wfp.child;
+
+import io.dapr.workflows.WorkflowActivity;
+import io.dapr.workflows.WorkflowActivityContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ReverseActivity implements WorkflowActivity {
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ Logger logger = LoggerFactory.getLogger(ReverseActivity.class);
+ logger.info("Starting Activity: " + ctx.getName());
+
+ var message = ctx.getInput(String.class);
+ var newMessage = new StringBuilder(message).reverse().toString();
+
+ logger.info("Message Received from input: " + message);
+ logger.info("Sending message to output: " + newMessage);
+
+ return newMessage;
+ }
+}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java
new file mode 100644
index 000000000..cb9c7f7aa
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 The Dapr 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.dapr.springboot.examples.wfp.continueasnew;
+
+import io.dapr.workflows.WorkflowActivity;
+import io.dapr.workflows.WorkflowActivityContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.concurrent.TimeUnit;
+
+@Component
+public class CleanUpActivity implements WorkflowActivity {
+
+ @Autowired
+ private CleanUpLog cleanUpLog;
+
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ Logger logger = LoggerFactory.getLogger(CleanUpActivity.class);
+ logger.info("Starting Activity: " + ctx.getName());
+
+ LocalDateTime now = LocalDateTime.now();
+ String cleanUpTimeString = now.getHour() + ":" + now.getMinute() + ":" + now.getSecond();
+ logger.info("start clean up work, it may take few seconds to finish... Time:" + cleanUpTimeString);
+
+ //Sleeping for 2 seconds to simulate long running operation
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ cleanUpLog.increment();
+
+ return "clean up finish.";
+ }
+}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java
new file mode 100644
index 000000000..9eb7c3fe9
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java
@@ -0,0 +1,15 @@
+package io.dapr.springboot.examples.wfp.continueasnew;
+
+public class CleanUpLog {
+ private Integer cleanUpTimes = 0;
+
+ public CleanUpLog() {
+ }
+ public void increment() {
+ this.cleanUpTimes += 1;
+ }
+
+ public Integer getCleanUpTimes() {
+ return cleanUpTimes;
+ }
+}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java
new file mode 100644
index 000000000..ad05ec4c3
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 The Dapr 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.dapr.springboot.examples.wfp.continueasnew;
+
+import io.dapr.workflows.Workflow;
+import io.dapr.workflows.WorkflowStub;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+
+@Component
+public class ContinueAsNewWorkflow implements Workflow {
+ /*
+ Compared with a CRON schedule, this periodic workflow example will never overlap.
+ For example, a CRON schedule that executes a cleanup every hour will execute it at 1:00, 2:00, 3:00 etc.
+ and could potentially run into overlap issues if the cleanup takes longer than an hour.
+ In this example, however, if the cleanup takes 30 minutes, and we create a timer for 1 hour between cleanups,
+ then it will be scheduled at 1:00, 2:30, 4:00, etc. and there is no chance of overlap.
+ */
+
+ @Autowired
+ private CleanUpLog cleanUpLog;
+
+ @Override
+ public WorkflowStub create() {
+ return ctx -> {
+ ctx.getLogger().info("Starting Workflow: " + ctx.getName());
+
+ ctx.getLogger().info("call CleanUpActivity to do the clean up");
+ ctx.callActivity(CleanUpActivity.class.getName(), cleanUpLog).await();
+ ctx.getLogger().info("CleanUpActivity finished");
+
+ ctx.getLogger().info("wait 5 seconds for next clean up");
+ ctx.createTimer(Duration.ofSeconds(3)).await();
+
+ if(cleanUpLog.getCleanUpTimes() < 5) {
+ // continue the workflow.
+ ctx.getLogger().info("Let's do more cleaning.");
+ ctx.continueAsNew(null);
+ } else{
+ ctx.getLogger().info("We did enough cleaning");
+ ctx.complete(cleanUpLog);
+ }
+ };
+ }
+}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ApproveActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ApproveActivity.java
new file mode 100644
index 000000000..e8ea8f7c8
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ApproveActivity.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 The Dapr 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.dapr.springboot.examples.wfp.externalevent;
+
+import io.dapr.workflows.WorkflowActivity;
+import io.dapr.workflows.WorkflowActivityContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+@Component
+public class ApproveActivity implements WorkflowActivity {
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ Logger logger = LoggerFactory.getLogger(ApproveActivity.class);
+ logger.info("Starting Activity: " + ctx.getName());
+
+ logger.info("Running approval activity...");
+ //Sleeping for 5 seconds to simulate long running operation
+ try {
+ TimeUnit.SECONDS.sleep(5);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ return new Decision(true);
+ }
+}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/Decision.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/Decision.java
new file mode 100644
index 000000000..2f8d2fee9
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/Decision.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2025 The Dapr 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.dapr.springboot.examples.wfp.externalevent;
+
+public class Decision {
+ private Boolean approved;
+
+ public Decision() {
+ }
+
+ public Decision(Boolean approved) {
+ this.approved = approved;
+ }
+
+ public Boolean getApproved() {
+ return approved;
+ }
+
+ public void setApproved(Boolean approved) {
+ this.approved = approved;
+ }
+}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/DenyActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/DenyActivity.java
new file mode 100644
index 000000000..bd4079376
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/DenyActivity.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 The Dapr 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.dapr.springboot.examples.wfp.externalevent;
+
+import io.dapr.workflows.WorkflowActivity;
+import io.dapr.workflows.WorkflowActivityContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+@Component
+public class DenyActivity implements WorkflowActivity {
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ Logger logger = LoggerFactory.getLogger(DenyActivity.class);
+ logger.info("Starting Activity: " + ctx.getName());
+
+ logger.info("Running denied activity...");
+ //Sleeping for 5 seconds to simulate long running operation
+ try {
+ TimeUnit.SECONDS.sleep(5);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ return new Decision(false);
+ }
+}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ExternalEventWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ExternalEventWorkflow.java
new file mode 100644
index 000000000..952d5eede
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ExternalEventWorkflow.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 The Dapr 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.dapr.springboot.examples.wfp.externalevent;
+
+import io.dapr.workflows.Workflow;
+import io.dapr.workflows.WorkflowStub;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ExternalEventWorkflow implements Workflow {
+ @Override
+ public WorkflowStub create() {
+ return ctx -> {
+ ctx.getLogger().info("Starting Workflow: " + ctx.getName());
+
+ ctx.getLogger().info("Waiting for approval...");
+ Boolean approved = ctx.waitForExternalEvent("Approval", boolean.class).await();
+ Decision decision = null;
+ if (approved) {
+ ctx.getLogger().info("approval granted - do the approved action");
+ decision = ctx.callActivity(ApproveActivity.class.getName(), Decision.class).await();
+
+ ctx.getLogger().info("approval-activity finished");
+ } else {
+ ctx.getLogger().info("approval denied - send a notification");
+ decision = ctx.callActivity(DenyActivity.class.getName(), Decision.class).await();
+ ctx.getLogger().info("denied-activity finished");
+ }
+ ctx.complete(decision);
+ };
+ }
+}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/CountWordsActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/CountWordsActivity.java
new file mode 100644
index 000000000..5863d5692
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/CountWordsActivity.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 The Dapr 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.dapr.springboot.examples.wfp.fanoutin;
+
+import io.dapr.workflows.WorkflowActivity;
+import io.dapr.workflows.WorkflowActivityContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.util.StringTokenizer;
+
+@Component
+public class CountWordsActivity implements WorkflowActivity {
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ Logger logger = LoggerFactory.getLogger(CountWordsActivity.class);
+ logger.info("Starting Activity: {}", ctx.getName());
+
+ String input = ctx.getInput(String.class);
+ StringTokenizer tokenizer = new StringTokenizer(input);
+ int result = tokenizer.countTokens();
+
+ logger.info("Activity returned: {}.", result);
+ logger.info("Activity finished");
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java
new file mode 100644
index 000000000..09f3edc57
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 The Dapr 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.dapr.springboot.examples.wfp.fanoutin;
+
+import io.dapr.durabletask.Task;
+import io.dapr.workflows.Workflow;
+import io.dapr.workflows.WorkflowStub;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Component
+public class FanOutInWorkflow implements Workflow {
+ @Override
+ public WorkflowStub create() {
+ return ctx -> {
+
+ ctx.getLogger().info("Starting Workflow: " + ctx.getName());
+
+
+ // The input is a list of objects that need to be operated on.
+ // In this example, inputs are expected to be strings.
+ List> inputs = ctx.getInput(List.class);
+
+ // Fan-out to multiple concurrent activity invocations, each of which does a word count.
+ List> tasks = inputs.stream()
+ .map(input -> ctx.callActivity(CountWordsActivity.class.getName(), input.toString(), Integer.class))
+ .collect(Collectors.toList());
+
+ // Fan-in to get the total word count from all the individual activity results.
+ List allWordCountResults = ctx.allOf(tasks).await();
+ int totalWordCount = allWordCountResults.stream().mapToInt(Integer::intValue).sum();
+
+ ctx.getLogger().info("Workflow finished with result: " + totalWordCount);
+ // Save the final result as the orchestration output.
+ ctx.complete(new Result(totalWordCount));
+ };
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/Result.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/Result.java
new file mode 100644
index 000000000..015484014
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/Result.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2025 The Dapr 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.dapr.springboot.examples.wfp.fanoutin;
+
+public class Result {
+ private Integer wordCount;
+
+ public Result() {
+ }
+
+ public Result(Integer wordCount) {
+ this.wordCount = wordCount;
+ }
+
+ public Integer getWordCount() {
+ return wordCount;
+ }
+
+ public void setWordCount(Integer wordCount) {
+ this.wordCount = wordCount;
+ }
+};
diff --git a/spring-boot-examples/workflows/src/main/resources/application.properties b/spring-boot-examples/workflows/src/main/resources/application.properties
new file mode 100644
index 000000000..7fd4ccd5b
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/resources/application.properties
@@ -0,0 +1 @@
+spring.application.name=workflow-patterns-app
diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java
new file mode 100644
index 000000000..a0e3a087c
--- /dev/null
+++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 The Dapr 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.dapr.springboot.examples.wfp;
+
+import io.dapr.testcontainers.Component;
+import io.dapr.testcontainers.DaprContainer;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Bean;
+
+import java.util.Collections;
+
+import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
+
+@TestConfiguration(proxyBeanMethods = false)
+public class DaprTestContainersConfig {
+
+ @Bean
+ @ServiceConnection
+ public DaprContainer daprContainer() {
+
+ return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
+ .withAppName("workflow-patterns-app")
+ .withComponent(new Component("kvstore", "state.in-memory", "v1", Collections.singletonMap("actorStateStore", String.valueOf(true))))
+ .withAppPort(8080)
+ .withAppHealthCheckPath("/actuator/health")
+ .withAppChannelAddress("host.testcontainers.internal");
+ }
+
+
+
+}
diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java
new file mode 100644
index 000000000..8459f8d50
--- /dev/null
+++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2025 The Dapr 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.dapr.springboot.examples.wfp;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+public class TestWorkflowPatternsApplication {
+
+ public static void main(String[] args) {
+
+ SpringApplication.from(WorkflowPatternsApplication::main)
+ .with(DaprTestContainersConfig.class)
+ .run(args);
+ org.testcontainers.Testcontainers.exposeHostPorts(8080);
+ }
+
+}
diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java
new file mode 100644
index 000000000..4ca36cb58
--- /dev/null
+++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2025 The Dapr 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.dapr.springboot.examples.wfp;
+
+import io.dapr.client.DaprClient;
+import io.dapr.springboot.DaprAutoConfiguration;
+import io.dapr.springboot.examples.wfp.continueasnew.CleanUpLog;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@SpringBootTest(classes = {TestWorkflowPatternsApplication.class, DaprTestContainersConfig.class,
+ DaprAutoConfiguration.class, },
+ webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+class WorkflowPatternsAppTests {
+
+ @Autowired
+ private DaprClient daprClient;
+
+ @BeforeEach
+ void setUp() {
+ RestAssured.baseURI = "http://localhost:" + 8080;
+ org.testcontainers.Testcontainers.exposeHostPorts(8080);
+ }
+
+
+ @Test
+ void testChainWorkflow() {
+ given().contentType(ContentType.JSON)
+ .body("")
+ .when()
+ .post("/wfp/chain")
+ .then()
+ .statusCode(200).body(containsString("TOKYO, LONDON, SEATTLE"));
+ }
+
+ @Test
+ void testChildWorkflow() {
+ given().contentType(ContentType.JSON)
+ .body("")
+ .when()
+ .post("/wfp/child")
+ .then()
+ .statusCode(200).body(containsString("!wolfkroW rpaD olleH"));
+ }
+
+ @Test
+ void testFanOutIn() {
+ List listOfStrings = Arrays.asList(
+ "Hello, world!",
+ "The quick brown fox jumps over the lazy dog.",
+ "If a tree falls in the forest and there is no one there to hear it, does it make a sound?",
+ "The greatest glory in living lies not in never falling, but in rising every time we fall.",
+ "Always remember that you are absolutely unique. Just like everyone else.");
+
+ given().contentType(ContentType.JSON)
+ .body(listOfStrings)
+ .when()
+ .post("/wfp/fanoutin")
+ .then()
+ .statusCode(200).body("wordCount",equalTo(60));
+ }
+
+ @Test
+ void testExternalEventApprove() {
+
+ given()
+ .queryParam("orderId", "123")
+ .when()
+ .post("/wfp/externalevent")
+ .then()
+ .statusCode(200).extract().asString();
+
+
+
+ given()
+ .queryParam("orderId", "123")
+ .queryParam("decision", true)
+ .when()
+ .post("/wfp/externalevent-continue")
+ .then()
+ .statusCode(200).body("approved", equalTo(true));
+ }
+
+ @Test
+ void testExternalEventDeny() {
+
+ given()
+ .queryParam("orderId", "123")
+ .when()
+ .post("/wfp/externalevent")
+ .then()
+ .statusCode(200).extract().asString();
+
+
+
+ given()
+ .queryParam("orderId", "123")
+ .queryParam("decision", false)
+ .when()
+ .post("/wfp/externalevent-continue")
+ .then()
+ .statusCode(200).body("approved", equalTo(false));
+ }
+
+
+ @Test
+ void testContinueAsNew() {
+ //This call blocks until all the clean up activities are executed
+ CleanUpLog cleanUpLog = given().contentType(ContentType.JSON)
+ .body("")
+ .when()
+ .post("/wfp/continueasnew")
+ .then()
+ .statusCode(200).extract().as(CleanUpLog.class);
+
+ assertEquals(5, cleanUpLog.getCleanUpTimes());
+ }
+
+}
diff --git a/spring-boot-examples/workflows/src/test/resources/application.properties b/spring-boot-examples/workflows/src/test/resources/application.properties
new file mode 100644
index 000000000..20d5fc037
--- /dev/null
+++ b/spring-boot-examples/workflows/src/test/resources/application.properties
@@ -0,0 +1 @@
+spring.application.name=workflow-patterns-app
\ No newline at end of file