-
Notifications
You must be signed in to change notification settings - Fork 548
feat(java): workflow context propagation quickstart #1319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
alicejgibbons
merged 6 commits into
dapr:release-1.18
from
javier-aliaga:feat/wf-ctx-propagation-java
Jun 9, 2026
Merged
Changes from 3 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
539fe6a
feat(java): workflow context propagation quickstart
javier-aliaga 53fa08a
refactor(java): port history-propagation to tutorials/workflow Spring…
javier-aliaga a9c1d1c
feat(java): add failed-history scenario and address review feedback
javier-aliaga c64a598
docs(java): address review feedback - docs link + mm STEP blocks
javier-aliaga 06a4378
Merge branch 'release-1.18' into feat/wf-ctx-propagation-java
alicejgibbons af42cd8
Merge branch 'release-1.18' into feat/wf-ctx-propagation-java
alicejgibbons File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| # Workflow History Propagation | ||
|
|
||
| This tutorial demonstrates **workflow history propagation**, a Dapr 1.18+ feature that lets a parent workflow share its execution history with child workflows and activities. Downstream services can inspect the propagated history to make trust-aware decisions — without any external state store or custom messaging. See the [Dapr docs](https://github.com/dapr/docs/pull/5153) for the feature reference. | ||
|
|
||
| ## Scenario: Patient intake / e-prescribing | ||
|
|
||
| A `ComplianceAuditWorkflow` and a `DispenseMedicationActivity` refuse to act unless the propagated history proves the required upstream checks (insurance, allergies, drug interactions) actually ran. | ||
|
|
||
| ```mermaid | ||
| graph LR | ||
| SI((Start)) | ||
| PI[PatientIntakeWorkflow] | ||
| VI[VerifyInsurance] | ||
| PM[PrescribeMedicationWorkflow] | ||
| CA[CheckAllergies] | ||
| SD[ScreenDrugInteractions] | ||
| CO[ComplianceAuditWorkflow] | ||
| DM[DispenseMedication] | ||
| EW((End)) | ||
| SI --> PI | ||
| PI -->|no propagation| VI | ||
| PI -->|LINEAGE| PM | ||
| PM -->|no propagation| CA | ||
| PM -->|no propagation| SD | ||
| PM -->|LINEAGE| CO | ||
| PM -->|OWN_HISTORY| DM | ||
| CO --> DM | ||
| DM --> EW | ||
| ``` | ||
|
|
||
| - `PatientIntakeWorkflow` (root) calls `VerifyInsuranceActivity` (no propagation), then invokes `PrescribeMedicationWorkflow` with `propagateLineage()`. | ||
| - `PrescribeMedicationWorkflow` receives `scope=LINEAGE` with **1 ancestor** (PatientIntake). It runs allergy and interaction checks, then calls `ComplianceAuditWorkflow` with `propagateLineage()` and `DispenseMedicationActivity` with `propagateOwnHistory()`. | ||
| - `ComplianceAuditWorkflow` receives `scope=LINEAGE` with **2 ancestors** (PatientIntake + PrescribeMedication). It uses `getLastActivityByName(...)` to verify each required upstream activity completed, then approves. | ||
| - `DispenseMedicationActivity` receives `scope=OWN_HISTORY` with **1 ancestor** (PrescribeMedication only). The grandparent PatientIntake is intentionally not visible (trust boundary). | ||
|
|
||
| ## Java API surface | ||
|
|
||
| ```java | ||
| import io.dapr.durabletask.ActivityResult; | ||
| import io.dapr.durabletask.HistoryPropagationScope; | ||
| import io.dapr.durabletask.PropagatedHistory; | ||
| import io.dapr.durabletask.WorkflowResult; | ||
| import io.dapr.workflows.WorkflowTaskOptions; | ||
|
|
||
| // Parent — propagate LINEAGE when calling a child workflow | ||
| AuditResult audit = ctx.callChildWorkflow( | ||
| ComplianceAuditWorkflow.class.getName(), | ||
| rec, | ||
| /* instanceId */ null, | ||
| WorkflowTaskOptions.propagateLineage(), | ||
| AuditResult.class).await(); | ||
|
|
||
| // Parent — propagate OWN_HISTORY when calling an activity | ||
| DispenseResult dispense = ctx.callActivity( | ||
| DispenseMedicationActivity.class.getName(), | ||
| rec, | ||
| WorkflowTaskOptions.propagateOwnHistory(), | ||
| DispenseResult.class).await(); | ||
|
|
||
| // Receiver (child workflow or activity) — read the propagated history | ||
| Optional<PropagatedHistory> historyOpt = ctx.getPropagatedHistory(); | ||
| historyOpt.ifPresent(history -> { | ||
| history.getScope(); // HistoryPropagationScope (LINEAGE | OWN_HISTORY) | ||
| history.getWorkflows(); // List<WorkflowResult> — ancestor first, then own | ||
|
|
||
| Optional<WorkflowResult> intake = history.getLastWorkflowByName( | ||
| PatientIntakeWorkflow.class.getName()); | ||
| intake.flatMap(wf -> wf.getLastActivityByName(VerifyInsuranceActivity.class.getName())) | ||
| .map(ActivityResult::isCompleted); | ||
| }); | ||
| ``` | ||
|
|
||
| ## Run the tutorial | ||
|
|
||
| 1. Use a terminal to navigate to the `tutorials/workflow/java/history-propagation` folder. | ||
| 2. Build and run the project using Maven. This spins up a Dapr sidecar via Testcontainers. | ||
|
|
||
| ```bash | ||
| mvn spring-boot:test-run | ||
| ``` | ||
|
|
||
| ### Scenario 1 (happy path): lineage forwarded — pharmacy dispenses | ||
|
|
||
| 3. Use the first POST request in the [`history-propagation.http`](./history-propagation.http) file, or use this cURL command: | ||
|
|
||
| ```bash | ||
| curl -i --request POST \ | ||
| --url http://localhost:8080/start \ | ||
| --header 'content-type: application/json' \ | ||
| --data '{ | ||
| "patientId": "P-1042", | ||
| "name": "Jane Doe", | ||
| "condition": "bacterial sinusitis", | ||
| "medication": "amoxicillin", | ||
| "dosage": 500, | ||
| "propagateHistory": true | ||
| }' | ||
| ``` | ||
|
|
||
| The app logs should show the propagation markers proving the feature works: | ||
|
|
||
| ```text | ||
| i.d.s.e.h.PatientIntakeWorkflow : PROPAGATION-DEMO: root workflow received no propagated history (expected) | ||
| i.d.s.e.h.PrescribeMedicationWorkflow : PROPAGATION-DEMO: scope=LINEAGE workflows=1 | ||
| i.d.s.e.h.ComplianceAuditWorkflow : PROPAGATION-DEMO: scope=LINEAGE workflows=2 | ||
| i.d.s.e.h.ComplianceAuditWorkflow : upstream activity VerifyInsurance: completed=true | ||
| i.d.s.e.h.ComplianceAuditWorkflow : upstream activity CheckAllergies: completed=true | ||
| i.d.s.e.h.ComplianceAuditWorkflow : upstream activity ScreenDrugInteractions: completed=true | ||
| i.d.s.e.h.ComplianceAuditWorkflow : APPROVED (risk=0.10, 2 workflow(s) verified) | ||
| i.d.s.e.h.a.DispenseMedicationActivity : PROPAGATION-DEMO: scope=OWN_HISTORY workflows=1 | ||
| i.d.s.e.h.a.DispenseMedicationActivity : DISPENSED: rx-P-1042-... (amoxicillin 500mg) for patient P-1042 | ||
| ``` | ||
|
|
||
| 4. Fetch the output with the GET request in the .http file, or: | ||
|
|
||
| ```bash | ||
| curl --request GET --url http://localhost:8080/output | ||
| ``` | ||
|
|
||
| Expected: | ||
|
|
||
| ```json | ||
| {"dispensed":true,"dispenseId":"rx-P-1042-<ts>","patientId":"P-1042","medication":"amoxicillin"} | ||
| ``` | ||
|
|
||
| ### Scenario 2 (failure): lineage withheld — pharmacy refuses | ||
|
|
||
| When `propagateHistory` is `false`, `PatientIntakeWorkflow` invokes `PrescribeMedicationWorkflow` **without** propagation options. `ComplianceAuditWorkflow` then receives no PatientIntake events in its propagated history, can't verify `VerifyInsurance` ran, and blocks the prescription. | ||
|
|
||
| 5. Use the second POST request in the .http file, or: | ||
|
|
||
| ```bash | ||
| curl -i --request POST \ | ||
| --url http://localhost:8080/start \ | ||
| --header 'content-type: application/json' \ | ||
| --data '{ | ||
| "patientId": "P-2087", | ||
| "name": "John Roe", | ||
| "condition": "strep throat", | ||
| "medication": "penicillin", | ||
| "dosage": 500, | ||
| "propagateHistory": false | ||
| }' | ||
| ``` | ||
|
|
||
| The app logs show the audit failing because it cannot find the upstream `VerifyInsurance` activity in the propagated history: | ||
|
|
||
| ```text | ||
| i.d.s.e.h.PatientIntakeWorkflow : Calling PrescribeMedicationWorkflow WITHOUT propagation (failure scenario) | ||
| i.d.s.e.h.PrescribeMedicationWorkflow : Starting prescription: penicillin 500mg for strep throat | ||
| i.d.s.e.h.ComplianceAuditWorkflow : PROPAGATION-DEMO: scope=LINEAGE workflows=1 | ||
| i.d.s.e.h.ComplianceAuditWorkflow : upstream activity VerifyInsurance: completed=false | ||
| i.d.s.e.h.ComplianceAuditWorkflow : upstream activity CheckAllergies: completed=true | ||
| i.d.s.e.h.ComplianceAuditWorkflow : upstream activity ScreenDrugInteractions: completed=true | ||
| i.d.s.e.h.ComplianceAuditWorkflow : BLOCKED - missing upstream checks: insurance=false allergies=true interactions=true | ||
| i.d.s.e.h.PrescribeMedicationWorkflow : Audit blocked dispensing - aborting prescription | ||
| ``` | ||
|
|
||
| 6. Fetch the output: | ||
|
|
||
| ```bash | ||
| curl --request GET --url http://localhost:8080/output | ||
| ``` | ||
|
|
||
| Expected: | ||
|
|
||
| ```json | ||
| {"dispensed":false,"dispenseId":null,"patientId":"P-2087","medication":"penicillin"} | ||
| ``` | ||
|
|
||
| 7. Stop the application by pressing `Ctrl+C`. | ||
30 changes: 30 additions & 0 deletions
30
tutorials/workflow/java/history-propagation/history-propagation.http
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| @apphost=http://localhost:8080 | ||
|
|
||
| ### Scenario 1 (happy path): lineage forwarded - pharmacy dispenses | ||
| POST {{ apphost }}/start | ||
| Content-Type: application/json | ||
|
|
||
| { | ||
| "patientId": "P-1042", | ||
| "name": "Jane Doe", | ||
| "condition": "bacterial sinusitis", | ||
| "medication": "amoxicillin", | ||
| "dosage": 500, | ||
| "propagateHistory": true | ||
| } | ||
|
|
||
| ### Scenario 2 (failure): lineage withheld - ComplianceAudit refuses, dispensed=false | ||
| POST {{ apphost }}/start | ||
| Content-Type: application/json | ||
|
|
||
| { | ||
| "patientId": "P-2087", | ||
| "name": "John Roe", | ||
| "condition": "strep throat", | ||
| "medication": "penicillin", | ||
| "dosage": 500, | ||
| "propagateHistory": false | ||
| } | ||
|
|
||
| ### Get the workflow output (most recent /start) | ||
| GET {{ apphost }}/output |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
| <modelVersion>4.0.0</modelVersion> | ||
|
|
||
| <parent> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-starter-parent</artifactId> | ||
| <version>3.4.5</version> | ||
| <relativePath/> <!-- lookup parent from repository --> | ||
| </parent> | ||
|
|
||
| <artifactId>history-propagation</artifactId> | ||
| <name>history-propagation</name> | ||
| <description>Workflow History Propagation Example</description> | ||
|
|
||
| <dependencyManagement> | ||
| <dependencies> | ||
| <dependency> | ||
| <groupId>io.dapr.spring</groupId> | ||
| <artifactId>dapr-spring-bom</artifactId> | ||
| <version>1.18.0-rc-2</version> | ||
| <type>pom</type> | ||
| <scope>import</scope> | ||
| </dependency> | ||
| </dependencies> | ||
| </dependencyManagement> | ||
|
|
||
| <dependencies> | ||
| <dependency> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-starter-actuator</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-starter-web</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-starter-test</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>io.dapr.spring</groupId> | ||
| <artifactId>dapr-spring-boot-starter</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>io.dapr.spring</groupId> | ||
| <artifactId>dapr-spring-boot-starter-test</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>io.rest-assured</groupId> | ||
| <artifactId>rest-assured</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| </dependencies> | ||
|
|
||
| <build> | ||
| <plugins> | ||
| <plugin> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-maven-plugin</artifactId> | ||
| </plugin> | ||
| </plugins> | ||
| </build> | ||
| </project> |
25 changes: 25 additions & 0 deletions
25
...-propagation/src/main/java/io/dapr/springboot/examples/HistoryPropagationApplication.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /* | ||
| * Copyright 2026 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; | ||
|
|
||
| import org.springframework.boot.SpringApplication; | ||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
|
|
||
| @SpringBootApplication | ||
| public class HistoryPropagationApplication { | ||
|
|
||
| public static void main(String[] args) { | ||
| SpringApplication.run(HistoryPropagationApplication.class, args); | ||
| } | ||
| } |
34 changes: 34 additions & 0 deletions
34
...ropagation/src/main/java/io/dapr/springboot/examples/HistoryPropagationConfiguration.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| /* | ||
| * Copyright 2026 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; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import org.springframework.boot.web.client.RestTemplateBuilder; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.web.client.RestTemplate; | ||
|
|
||
| @Configuration | ||
| public class HistoryPropagationConfiguration { | ||
|
|
||
| @Bean | ||
| public RestTemplate restTemplate() { | ||
| return new RestTemplateBuilder().build(); | ||
| } | ||
|
|
||
| @Bean | ||
| public ObjectMapper mapper() { | ||
| return new ObjectMapper(); | ||
| } | ||
| } |
68 changes: 68 additions & 0 deletions
68
...opagation/src/main/java/io/dapr/springboot/examples/HistoryPropagationRestController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| /* | ||
| * Copyright 2026 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; | ||
|
|
||
| import io.dapr.spring.workflows.config.EnableDaprWorkflows; | ||
| import io.dapr.springboot.examples.historypropagation.PatientIntakeWorkflow; | ||
| import io.dapr.springboot.examples.historypropagation.models.PatientRecord; | ||
| import io.dapr.springboot.examples.historypropagation.models.PrescriptionResult; | ||
| import io.dapr.workflows.client.DaprWorkflowClient; | ||
| import io.dapr.workflows.client.WorkflowInstanceStatus; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import java.util.concurrent.TimeoutException; | ||
|
|
||
| @RestController | ||
| @EnableDaprWorkflows | ||
| public class HistoryPropagationRestController { | ||
|
|
||
| @Autowired | ||
| private DaprWorkflowClient daprWorkflowClient; | ||
|
|
||
| /* | ||
| * For example purposes only. In production, map workflowInstanceIds to your | ||
| * own business identifiers. | ||
| */ | ||
| private String instanceId; | ||
|
|
||
| /** | ||
| * Start the PatientIntake workflow with the given patient record. | ||
| * | ||
| * @param record the patient record | ||
| * @return the workflow instance id | ||
| */ | ||
| @PostMapping("start") | ||
| public String start(@RequestBody PatientRecord record) { | ||
| instanceId = daprWorkflowClient.scheduleNewWorkflow(PatientIntakeWorkflow.class, record); | ||
| return instanceId; | ||
| } | ||
|
|
||
| /** | ||
| * Get the output of the last started PatientIntake workflow. | ||
| * | ||
| * @return the prescription result, or a placeholder if not yet available | ||
| */ | ||
| @GetMapping("output") | ||
| public PrescriptionResult output() throws TimeoutException { | ||
| WorkflowInstanceStatus state = daprWorkflowClient.getInstanceState(instanceId, true); | ||
| if (state != null) { | ||
| return state.readOutputAs(PrescriptionResult.class); | ||
| } | ||
| return new PrescriptionResult(false, null, null, null); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.