Skip to content

feat(python): workflow context propagation quickstart#1309

Open
nelson-parente wants to merge 4 commits into
dapr:release-1.18from
nelson-parente:feat/wf-ctx-propagation-python
Open

feat(python): workflow context propagation quickstart#1309
nelson-parente wants to merge 4 commits into
dapr:release-1.18from
nelson-parente:feat/wf-ctx-propagation-python

Conversation

@nelson-parente
Copy link
Copy Markdown
Contributor

@nelson-parente nelson-parente commented May 19, 2026

Summary

Python sibling of @cicoyle's canonical Go quickstart for workflow history propagation (Dapr 1.18). Same patient intake / e-prescribing scenario, same 3-level hierarchy, same workflow/activity names — so the Python and Go examples can be compared 1:1.

Scenario

Patient intake / e-prescribing pipeline. A compliance audit and a pharmacy dispense step refuse to act unless the propagated history proves the required upstream checks (insurance, allergies, drug interactions) actually ran.

PatientIntake (root)
  └─ VerifyInsurance       (activity, no propagation)
  └─ PrescribeMedication   (child wf, PropagationScope.LINEAGE)
        └─ CheckAllergies         (activity, no propagation)
        └─ ScreenDrugInteractions (activity, no propagation)
        └─ ComplianceAudit        (grandchild wf, PropagationScope.LINEAGE)
        |      reads PatientIntake + PrescribeMedication events
        └─ DispenseMedication     (activity, PropagationScope.OWN_HISTORY)
               reads PrescribeMedication events only
               refuses to dispense if the screening lineage is missing

ComplianceAudit uses LINEAGE to verify the full ancestor chain. DispenseMedication uses OWN_HISTORY as a trust boundary — the pharmacy step doesn't get to see the upstream patient-intake chain.

Two scenarios (matches the Go demo)

The app schedules both back-to-back and exits on its own — no Ctrl+C needed:

  1. Lineage forwardedPrescribeMedication calls DispenseMedication with PropagationScope.OWN_HISTORY; pharmacy verifies and dispenses.
  2. Lineage withheldPrescribeMedication calls DispenseMedication without propagation; pharmacy receives no history and refuses with a refused DispenseResult.

Python API used (from dapr/python-sdk#1025)

  • PropagationScope.LINEAGE / PropagationScope.OWN_HISTORY
  • propagation= kwarg on ctx.call_child_workflow() and ctx.call_activity()
  • ctx.get_propagated_history()PropagatedHistory | None
  • PropagatedHistory.get_last_workflow_by_name(name)WorkflowResult
  • WorkflowResult.get_last_activity_by_name(name)ActivityResult (with .completed, .output)
  • PropagationNotFoundError

Replay safety

All print() calls inside workflows are guarded by if not ctx.is_replaying: so they only fire on the live execution, not on each durable replay.

Files

tutorials/workflow/python/history-propagation/
├── README.md          # mirrors the Go README, adapted for Python
├── dapr.yaml          # dapr run -f . config (appID=patient-app)
├── makefile           # wires the example into `make validate`
├── app.py             # registry + worker setup, schedules both scenarios
├── models.py          # PatientRecord, ComplianceResult, DispenseResult
├── workflow.py        # workflow + activity definitions, history helpers
└── requirements.txt   # Python deps (dapr-ext-workflow==1.18.0rc0)

Test plan

  • pip3 install -r requirements.txt against dapr-ext-workflow==1.18.0rc0
  • dapr run -f . against a Dapr 1.18 RC sidecar
  • Scenario 1: ComplianceAudit reads full ancestor history via LINEAGE (sees PatientIntake + PrescribeMedication); DispenseMedication sees only PrescribeMedication events via OWN_HISTORY and dispenses
  • Scenario 2: DispenseMedication receives no propagated history and returns refused, PrescribeMedication reports pharmacy refused to dispense
  • Older sidecar (pre-1.18) gracefully returns None from get_propagated_history() without crash

References

Ports Cassie Coyle's Go-SDK reference example (dapr/go-sdk#823) to Python.
Demonstrates PropagationScope.LINEAGE and PropagationScope.OWN_HISTORY in a
fraud-detection payment scenario with a 3-level workflow hierarchy.

Requires Dapr 1.18+ (dapr/dapr#9810) and dapr-ext-workflow 1.18+ (dapr/python-sdk#1025).

Signed-off-by: Nelson Parente <nelson_parente@live.com.pt>
Aligns the Python workflow history propagation quickstart with the canonical
Go reference (dapr/go-sdk#823, dapr#1315) so all SDK quickstarts
share the same patient intake / e-prescribing scenario.

- Swap credit-card/fraud scenario for patient-intake/e-prescribing
- Adopt PatientIntake -> PrescribeMedication -> ComplianceAudit hierarchy
- Add is_replaying guards around all print() calls inside workflows
- Use Cassie's PascalCase activity/workflow names for cross-SDK consistency

Signed-off-by: Nelson Parente <nelson_parente@live.com.pt>
@nelson-parente nelson-parente marked this pull request as ready for review May 21, 2026 09:54
@nelson-parente nelson-parente requested review from a team as code owners May 21, 2026 09:54
@nelson-parente nelson-parente changed the title [DRAFT] feat(python): workflow context propagation quickstart feat(python): workflow context propagation quickstart May 21, 2026
Aligns the Python workflow history propagation quickstart with Cassie's
canonical Go version that merged into release-1.18
(dapr#1315, tutorials/workflow/go/history-propagation).

Changes to match the canonical structure:

- Move from workflows/python/sdk-context-propagation/order-processor/ to
  tutorials/workflow/python/history-propagation/, matching the Go sibling
  at tutorials/workflow/go/history-propagation/
- Split the monolithic app.py into app.py / workflow.py / models.py,
  mirroring main.go / workflow.go / models.go
- Add ForwardLineage flag to PatientRecord and run both scenarios
  back-to-back (happy path + negative), purging state after each so the
  app exits on its own
- Make DispenseMedication refuse to dispense when no propagated history
  is received, returning a refused DispenseResult with a Reason field
- Match the Go README: title, propagation-scope table, STEP markers,
  expected output for both scenarios
- dapr.yaml: appID=patient-app (was order-processor),
  resourcesPath=../../resources (was ../../components),
  appLogDestination/daprdLogDestination=console to match sister Python
  tutorials and Cassie's Go example
- Use correct Python SDK API names: get_last_workflow_by_name /
  get_last_activity_by_name (the earlier draft used get_workflow_by_name
  / get_activity_by_name, which were renamed in dapr/python-sdk#1025)

Signed-off-by: Nelson Parente <nelson_parente@live.com.pt>
@nelson-parente nelson-parente changed the base branch from master to release-1.18 May 28, 2026 21:26
@nelson-parente
Copy link
Copy Markdown
Contributor Author

Updated to align with @cicoyle's now-merged Go quickstart at tutorials/workflow/go/history-propagation (#1315). Summary of what changed since the last revision:

Location & target branch

  • Moved files from workflows/python/sdk-context-propagation/order-processor/tutorials/workflow/python/history-propagation/, matching the Go sibling.
  • Retargeted PR base from masterrelease-1.18 (where Cassie's PR landed).

File layout

  • Split the previous single app.py into three files mirroring main.go / workflow.go / models.go:
    • app.py — registry + worker setup, schedules both scenarios.
    • workflow.py — workflow and activity definitions, history helpers.
    • models.pyPatientRecord, ComplianceResult, DispenseResult dataclasses.

Behavioural alignment with the Go canonical version

  • Added forward_lineage: bool field to PatientRecord to control whether PrescribeMedication propagates its history to DispenseMedication.
  • The app now runs two scenarios back-to-back (was: a single happy path):
    1. forward_lineage=True → pharmacy verifies the propagated history and dispenses.
    2. forward_lineage=False → pharmacy receives no propagated history and refuses with a refused DispenseResult (added reason field).
  • After each scenario, the workflow state is purged so the demo exits on its own.

dapr.yaml

  • appID: order-processorappID: patient-app (matches the Go example's patient-app).
  • resourcesPath: ../../componentsresourcesPath: ../../resources (matches the convention used by sibling Python tutorials under tutorials/workflow/python/).
  • Added appLogDestination: console and daprdLogDestination: console.

Python SDK API

  • Switched to the correct method names from dapr-ext-workflow 1.18: get_last_workflow_by_name / get_last_activity_by_name (the previous draft used get_workflow_by_name / get_activity_by_name, which were renamed in Add history propagation python-sdk#1025).

README

  • Rewrote to mirror the Go README's structure: title, propagation-scope table, key-demonstration block, both scenarios documented, <!-- STEP --> markers for make validate, expected-output snippets for both scenarios, file map at the bottom.

No SDK changes were required — the dependency stays at dapr-ext-workflow==1.18.0rc0 from #1307.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant