Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions examples/workflow-async/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Dapr Workflow Async Examples (Python)

These examples mirror `examples/workflow/` but author orchestrators with `async def` using the
async workflow APIs. Activities remain regular functions unless noted.

How to run:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls add a venv reference in here too

- Ensure a Dapr sidecar is running locally. If needed, set `DURABLETASK_GRPC_ENDPOINT`, or
`DURABLETASK_GRPC_HOST/PORT`.
- Install requirements: `pip install -r requirements.txt`
- Run any example: `python simple.py`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't these need to run with a dapr run cmd then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed but it surely will make i more predicatble that daprd is running, let me think on this if the change is small


Notes:
- Orchestrators use `await ctx.activity(...)`, `await ctx.sleep(...)`, `await ctx.when_all/when_any(...)`, etc.
- No event loop is started manually; the Durable Task worker drives the async orchestrators.
- You can also launch instances using `DaprWorkflowClient` as in the non-async examples.
47 changes: 47 additions & 0 deletions examples/workflow-async/child_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-

"""
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 specific language governing permissions and
limitations under the License.
"""

from dapr.ext.workflow import (
AsyncWorkflowContext,
DaprWorkflowClient,
WorkflowRuntime,
)

wfr = WorkflowRuntime()


@wfr.async_workflow(name='child_async')
async def child(ctx: AsyncWorkflowContext, n: int) -> int:
return n * 2


@wfr.async_workflow(name='parent_async')
async def parent(ctx: AsyncWorkflowContext, n: int) -> int:
r = await ctx.call_child_workflow(child, input=n)
print(f'Child workflow returned {r}')
return r + 1


def main():
wfr.start()
client = DaprWorkflowClient()
instance_id = 'parent_async_instance'
client.schedule_new_workflow(workflow=parent, input=5, instance_id=instance_id)
client.wait_for_workflow_completion(instance_id, timeout_in_seconds=60)
wfr.shutdown()


if __name__ == '__main__':
main()
49 changes: 49 additions & 0 deletions examples/workflow-async/fan_out_fan_in.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
"""
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 specific language governing permissions and
limitations under the License.
"""

from dapr.ext.workflow import (
AsyncWorkflowContext,
DaprWorkflowClient,
WorkflowActivityContext,
WorkflowRuntime,
)

wfr = WorkflowRuntime()


@wfr.activity(name='square')
def square(ctx: WorkflowActivityContext, x: int) -> int:
return x * x


@wfr.async_workflow(name='fan_out_fan_in_async')
async def orchestrator(ctx: AsyncWorkflowContext):
tasks = [ctx.call_activity(square, input=i) for i in range(1, 6)]
results = await ctx.when_all(tasks)
total = sum(results)
return total


def main():
wfr.start()
client = DaprWorkflowClient()
instance_id = 'fofi_async'
client.schedule_new_workflow(workflow=orchestrator, instance_id=instance_id)
wf_state = client.wait_for_workflow_completion(instance_id, timeout_in_seconds=60)
print(f'Workflow state: {wf_state}')
wfr.shutdown()


if __name__ == '__main__':
main()
47 changes: 47 additions & 0 deletions examples/workflow-async/human_approval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-

"""
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 specific language governing permissions and
limitations under the License.
"""

from dapr.ext.workflow import AsyncWorkflowContext, DaprWorkflowClient, WorkflowRuntime

wfr = WorkflowRuntime()


@wfr.async_workflow(name='human_approval_async')
async def orchestrator(ctx: AsyncWorkflowContext, request_id: str):
decision = await ctx.when_any(
[
ctx.wait_for_external_event(f'approve:{request_id}'),
ctx.wait_for_external_event(f'reject:{request_id}'),
ctx.create_timer(300.0),
]
)
if isinstance(decision, dict) and decision.get('approved'):
return 'APPROVED'
if isinstance(decision, dict) and decision.get('rejected'):
return 'REJECTED'
return 'TIMEOUT'


def main():
wfr.start()
client = DaprWorkflowClient()
instance_id = 'human_approval_async_1'
client.schedule_new_workflow(workflow=orchestrator, input='REQ-1', instance_id=instance_id)
# In a real scenario, raise approve/reject event from another service.
wfr.shutdown()


if __name__ == '__main__':
main()
2 changes: 2 additions & 0 deletions examples/workflow-async/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dapr-ext-workflow-dev>=1.15.0.dev
dapr-dev>=1.15.0.dev
Comment on lines +1 to +2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the latest release version the .dev?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder tho if we should not pin and let it default to latest bc thats what we do in the dapr agents requirement files for the quickstarts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the problem is if we don't have anything people have an older version somehow and the requirements do not tell them to upgrade and it fails.

136 changes: 136 additions & 0 deletions examples/workflow-async/simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
"""
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 specific language governing permissions and
limitations under the License.
"""

from datetime import timedelta
from time import sleep

from dapr.ext.workflow import (
AsyncWorkflowContext,
DaprWorkflowClient,
RetryPolicy,
WorkflowActivityContext,
WorkflowRuntime,
)

counter = 0
retry_count = 0
child_orchestrator_string = ''
instance_id = 'asyncExampleInstanceID'
child_instance_id = 'asyncChildInstanceID'
workflow_name = 'async_hello_world_wf'
child_workflow_name = 'async_child_wf'
input_data = 'Hi Async Counter!'
event_name = 'event1'
event_data = 'eventData'

retry_policy = RetryPolicy(
first_retry_interval=timedelta(seconds=1),
max_number_of_attempts=3,
backoff_coefficient=2,
max_retry_interval=timedelta(seconds=10),
retry_timeout=timedelta(seconds=100),
)

wfr = WorkflowRuntime()


@wfr.async_workflow(name=workflow_name)
async def hello_world_wf(ctx: AsyncWorkflowContext, wf_input):
# activities
result_1 = await ctx.call_activity(hello_act, input=1)
print(f'Activity 1 returned {result_1}')
result_2 = await ctx.call_activity(hello_act, input=10)
print(f'Activity 2 returned {result_2}')
result_3 = await ctx.call_activity(hello_retryable_act, retry_policy=retry_policy)
print(f'Activity 3 returned {result_3}')
result_4 = await ctx.call_child_workflow(child_retryable_wf, retry_policy=retry_policy)
print(f'Child workflow returned {result_4}')

# Event vs timeout using when_any
first = await ctx.when_any(
[
ctx.wait_for_external_event(event_name),
ctx.create_timer(timedelta(seconds=30)),
]
)

# Proceed only if event won
if isinstance(first, dict) and 'event' in first:
await ctx.call_activity(hello_act, input=100)
await ctx.call_activity(hello_act, input=1000)
return 'Completed'
return 'Timeout'


@wfr.activity(name='async_hello_act')
def hello_act(ctx: WorkflowActivityContext, wf_input):
global counter
counter += wf_input
return f'Activity returned {wf_input}'


@wfr.activity(name='async_hello_retryable_act')
def hello_retryable_act(ctx: WorkflowActivityContext):
global retry_count
if (retry_count % 2) == 0:
retry_count += 1
raise ValueError('Retryable Error')
retry_count += 1
return f'Activity returned {retry_count}'


@wfr.async_workflow(name=child_workflow_name)
async def child_retryable_wf(ctx: AsyncWorkflowContext):
# Call activity with retry and simulate retryable workflow failure until certain state
child_activity_result = await ctx.call_activity(
act_for_child_wf, input='x', retry_policy=retry_policy
)
print(f'Child activity returned {child_activity_result}')
# In a real sample, you might check state and raise to trigger retry
return 'ok'


@wfr.activity(name='async_act_for_child_wf')
def act_for_child_wf(ctx: WorkflowActivityContext, inp):
global child_orchestrator_string
child_orchestrator_string += inp


def main():
wfr.start()
wf_client = DaprWorkflowClient()

wf_client.schedule_new_workflow(
workflow=hello_world_wf, input=input_data, instance_id=instance_id
)

wf_client.wait_for_workflow_start(instance_id)

# Let initial activities run
sleep(5)

# Raise event to continue
wf_client.raise_workflow_event(
instance_id=instance_id, event_name=event_name, data={'ok': True}
)

# Wait for completion
state = wf_client.wait_for_workflow_completion(instance_id, timeout_in_seconds=60)
print(f'Workflow status: {state.runtime_status.name}')

wfr.shutdown()


if __name__ == '__main__':
main()
48 changes: 48 additions & 0 deletions examples/workflow-async/task_chaining.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
"""
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 specific language governing permissions and
limitations under the License.
"""

from dapr.ext.workflow import (
AsyncWorkflowContext,
DaprWorkflowClient,
WorkflowActivityContext,
WorkflowRuntime,
)

wfr = WorkflowRuntime()


@wfr.activity(name='sum')
def sum_act(ctx: WorkflowActivityContext, nums):
return sum(nums)


@wfr.async_workflow(name='task_chaining_async')
async def orchestrator(ctx: AsyncWorkflowContext):
a = await ctx.call_activity(sum_act, input=[1, 2])
b = await ctx.call_activity(sum_act, input=[a, 3])
c = await ctx.call_activity(sum_act, input=[b, 4])
return c


def main():
wfr.start()
client = DaprWorkflowClient()
instance_id = 'task_chain_async'
client.schedule_new_workflow(workflow=orchestrator, instance_id=instance_id)
client.wait_for_workflow_completion(instance_id, timeout_in_seconds=60)
wfr.shutdown()


if __name__ == '__main__':
main()
2 changes: 2 additions & 0 deletions examples/workflow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ This directory contains examples of using the [Dapr Workflow](https://docs.dapr.
You can install dapr SDK package using pip command:

```sh
python3 -m venv .venv
source .venv/bin/activate
pip3 install -r requirements.txt
```

Expand Down
39 changes: 39 additions & 0 deletions examples/workflow/aio/async_activity_sequence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-

"""
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 specific language governing permissions and
limitations under the License.
"""

from dapr.ext.workflow import AsyncWorkflowContext, WorkflowRuntime


def main():
rt = WorkflowRuntime()

@rt.activity(name='add')
def add(ctx, xy):
return xy[0] + xy[1]

@rt.workflow(name='sum_three')
async def sum_three(ctx: AsyncWorkflowContext, nums):
a = await ctx.call_activity(add, input=[nums[0], nums[1]])
b = await ctx.call_activity(add, input=[a, nums[2]])
return b

rt.start()
print("Registered async workflow 'sum_three' and activity 'add'")

# This example registers only; use Dapr client to start instances externally.


if __name__ == '__main__':
main()
Loading