Demos applications that use the Dapr Workflow building block.
Read the blog posts:
-
- Ensure that you're using v1.11 of the Dapr runtime and the CLI, since there have been breaking changes to the Workflow API from v1.10 to v1.11.
-
A REST client, such as cURL, or the VSCode REST client extension.
The VSCode REST client is configured as a recommended extension when opening this repo in VSCode.
The Hello World Workflow sample is a very basic workflow with just one activity that returns a random greeting. The workflow takes a name as input and returns a greeting with the name as output.
graph TB
A[Start]
B[CreateGreetingActivity]
C[End]
A -->|"input: {name}"| B -->|"output: {greeting} {name}"| C
-
Change to the BasicWorkflowSamples directory and build the ASP.NET app:
cd BasicWorkflowSamples dotnet build
-
Run the app using the Dapr CLI:
dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run
Ensure the --app-port is the same as the port specified in the launchSettings.json file.
-
Start the
HelloWorldWorkflow
via the Workflow HTTP API using cURL, or use the basicworkflows.http file if you're using VSCode with the REST client:curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/HelloWorldWorkflow/start?instanceID=1234a \ -H "Content-Type: application/text/plain" \ -d '"World"'
Note that
1234a
in the URL is the workflow instance ID. This can be any string you want.Expected result:
{ "instanceID": "<WORKFLOW_ID>" }
-
Check the workflow status via Workflow HTTP API:
curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234a
Expected result:
{ "instanceID": "<WORKFLOW_ID>", "workflowName": "HelloWorldWorkflow", "createdAt": "2023-06-19T13:19:18.316956600Z", "lastUpdatedAt": "2023-06-19T13:19:18.333226200Z", "runtimeStatus": "COMPLETED", "properties": { "dapr.workflow.custom_status": "", "dapr.workflow.input": "\"World\"", "dapr.workflow.output": "\"Ciao World\"" }
} ```
The Chaining Workflow sample is a workflow that chains three activities of the same type CreateGreetingActivity
, each prepending a random greeting to the input. The output of the activity is used as an input for the next activity in the chain.
graph TB
A[Start]
B1[CreateGreetingActivity]
B2[CreateGreetingActivity]
B3[CreateGreetingActivity]
C[End]
A -->|"input: {name}"| B1 -->|"output/input: {greeting1} {name}"| B2
B2 -->|"output/input: {greeting2} {greeting1} {name}"| B3
B3 -->|"output: {greeting3} {greeting2} {greeting1} {name}"| C
-
Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI:
cd BasicWorkflowSamples dotnet build dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run
-
Start the
ChainingWorkflow
via the Workflow HTTP API using cURL, or use the basicworkflows.http file if you're using VSCode with the REST client:curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/ChainingWorkflow/start?instanceID=1234b \ -H "Content-Type: application/text/plain" \ -d '"World"'
Note that
1234b
in the URL is the workflow instance ID. This can be any string you want.Expected result:
{ "instanceID": "<WORKFLOW_ID>" }
-
Check the workflow status via Workflow HTTP API:
curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234b
Expected result:
{ "instanceID": "<WORKFLOW_ID>", "workflowName": "ChainingWorkflow", "createdAt": "2023-06-19T13:21:08.611149200Z", "lastUpdatedAt": "2023-06-19T13:21:08.647482Z", "runtimeStatus": "COMPLETED", "properties": { "dapr.workflow.custom_status": "", "dapr.workflow.input": "\"World\"", "dapr.workflow.output": "\"Hello Hi Konnichiwa World\"" } }
The Fan-out / Fan-in Workflow sample is a workflow that fans out and schedules an activity (CreateGreetingActivity
) for each element in the input array and waits until all of the activities are completed. The output of the workflow is an array of greetings.
graph TB
A[Start]
A1([for each name in names])
B1[CreateGreetingActivity]
B2[CreateGreetingActivity]
B3[CreateGreetingActivity]
C([Task.WhenAll])
D[End]
A --> |"input: [{name1}, {name2}, {name3}]"| A1
A1 -->|"input: {name1}"| B1 -->|"output: {greeting1} {name1}"| C
A1 -->|"input: {name2}"| B2 -->|"output: {greeting2} {name2}"| C
A1 -->|"input: {name3}"| B3 -->|"output: {greeting3} {name3}"| C
C -->|"output: [{greeting1} {name1}, {greeting2} {name2}, {greeting3} {name3}]"| D
-
Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI:
cd BasicWorkflowSamples dotnet build dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run
-
Start the
FanOutFanInWorkflow
via the Workflow HTTP API using cURL, or use the basicworkflows.http file if you're using VSCode with the REST client:curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/FanOutFanInWorkflow/start?instanceID=1234c \ -H "Content-Type: application/json" \ -d '["Amsterdam", "Chicago", "New York"]'
Note that
1234c
in the URL is the workflow instance ID. This can be any string you want.Expected result:
{ "instanceID": "<WORKFLOW_ID>" }
-
Check the workflow status via Workflow HTTP API:
curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234c
Expected result:
{ "instanceID": "<WORKFLOW_ID>", "workflowName": "FanOutFanInWorkflow", "createdAt": "2023-06-19T13:22:42.056622400Z", "lastUpdatedAt": "2023-06-19T13:22:42.093666600Z", "runtimeStatus": "COMPLETED", "properties": { "dapr.workflow.custom_status": "", "dapr.workflow.input": "[\"Amsterdam\",\"Chicago\",\"New York\"]", "dapr.workflow.output": "[\"Hi Amsterdam\",\"Hola Chicago\",\"Guten Tag New York\"]" } }
The Monitor sample is a workflow with a numeric input (counter) and restarts the workflow (with an updated counter) until the counter reaches 10. The workflow diagram is as follows:
graph TB
A[Start]
B[CreateGreetingActivity]
C{counter < 10}
D[End]
A --> |"input: {counter}"| B
B -->|"output: {greeting} {counter}"| C
C --> |"[Yes] {counter+=1}"| A
C -->|"[No] output: {greeting} 10"| D
-
Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI:
cd BasicWorkflowSamples dotnet build dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run
-
Start the
MonitorWorkflow
via the Workflow HTTP API using cURL, or use the basicworkflows.http file if you're using VSCode with the REST client:curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/MonitorWorkflow/start?instanceID=1234d \ -H "Content-Type: application/text/plain" \ -d '0'
Note that
1234d
in the URL is the workflow instance ID. This can be any string you want.Expected result:
{ "instanceID": "<WORKFLOW_ID>" }
-
Check the workflow status via Workflow HTTP API:
curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234d
Expected result:
{ "instanceID": "<WORKFLOW_ID>", "workflowName": "MonitorWorkflow", "createdAt": "2023-06-19T13:24:23.004744700Z", "lastUpdatedAt": "2023-06-19T13:24:23.016307900Z", "runtimeStatus": "COMPLETED", "properties": { "dapr.workflow.custom_status": "", "dapr.workflow.input": "10", "dapr.workflow.output": "\"Hey 10\"" } }
The Timer sample is a workflow with a TimerWorkflowInput object that contains a name and a date. If the date from the input is larger that the current date, a timer is started and the workflow will only continue once the timer has completed.:
graph TB
A[Start]
B{input date > current date}
C([CreateTimer])
D[CreateGreetingActivity]
E[End]
A --> |"input: {date, name}"| B
B -->|"[Yes]"| C
C -->|"Wait"| A
B -->|"[No] input: {name}"| D
D -->|"output: {greeting} {name}"| E
-
Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI:
cd BasicWorkflowSamples dotnet build dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run
-
Start the
TimerWorkflow
via the Workflow HTTP API using cURL, or use the basicworkflows.http file if you're using VSCode with the REST client:curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/TimerWorkflow/start?instanceID=1234e \ -H "Content-Type: application/json" \ -d '{"DateTime": "2023-05-29T13:44:00+00:00","Name":"World"}'
Note that
1234e
in the URL is the workflow instance ID. This can be any string you want.Expected result:
{ "instanceID": "<WORKFLOW_ID>" }
-
Check the workflow status via Workflow HTTP API:
curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234e
Expected result:
{ "instanceID": "<WORKFLOW_ID>", "workflowName": "TimerWorkflow", "createdAt": "2023-06-19T13:25:59.745344700Z", "lastUpdatedAt": "2023-06-19T13:25:59.768925500Z", "runtimeStatus": "COMPLETED", "properties": { "dapr.workflow.custom_status": "", "dapr.workflow.input": "{\"DateTime\": \"2023-05-29T13:44:00+00:00\", \"Name\": \"World\"}", "dapr.workflow.output": "\"Guten Tag World at 2023-05-29 15:44:00\"" } }
The External interaction sample is a workflow that will wait with execution of the workflow until an external event has been received or the timeout has expired.
graph TB
A[Start]
B([WaitForExternalEventAsync])
C{IsApproved}
D[CreateGreetingActivity]
F[ExternalEvent]
E[End]
A --> B
F --> B
B --> C
C -->|"[Yes]"| D
C -->|"[No / Timeout]"| E
D --> E
-
Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI:
cd BasicWorkflowSamples dotnet build dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run
-
Start the
ExternalInteractionWorkflow
via the Workflow HTTP API using cURL, or use the basicworkflows.http file if you're using VSCode with the REST client:curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/ExternalInteractionWorkflow/start?instanceID=1234f \ -H "Content-Type: application/text/plain" \ -d '"World"'
Note that
1234f
in the URL is the workflow instance ID. This can be any string you want.Expected result:
{ "instanceID": "<WORKFLOW_ID>" }
-
Check the workflow status via Workflow HTTP API:
curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234f
If you check the status within the specified timeout, the
runtimeStatus
will beRUNNING
:{ "instanceID": "<WORKFLOW_ID>", "workflowName": "ExternalInteractionWorkflow", "createdAt": "2023-07-27T11:35:54.446941200Z", "lastUpdatedAt": "2023-07-27T11:35:55.694310900Z", "runtimeStatus": "RUNNING", "properties": { "dapr.workflow.custom_status": "", "dapr.workflow.input": "\"World\"" } }
If you wait until the timeout has expired, the
runtimeStatus
will beCOMPLETED
, with acustom_status
that the wait for external event is cancelled due to a timeout:{ "instanceID": "2283569d-5d56-4041-bd63-7df35fa3c879", "workflowName": "ExternalInteractionWorkflow", "createdAt": "2023-07-27T14:29:37.084711800Z", "lastUpdatedAt": "2023-07-27T14:29:57.011878800Z", "runtimeStatus": "COMPLETED", "properties": { "dapr.workflow.custom_status": "\"Wait for external event is cancelled due to timeout.\"", "dapr.workflow.input": "\"World\"", "dapr.workflow.output": "\"\"" } }
-
Now start the workflow again, raise an event within the timeout duration by calling the
raiseEvent
endpoint, and retrieve the status of the workflow. You can use cURL, or use the basicworkflows.http file if you're using VSCode with the REST client:curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/ExternalInteractionWorkflow/start?instanceID=1234f \ -H "Content-Type: application/text/plain" \ -d '"World"'
curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/1234f/raiseEvent/ \ -H "Content-Type: application/json" \ -d '{"IsApproved":true}'
curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234f
The
runtimeStatus
should now beCOMPLETED
:{ "instanceID": "<WORKFLOW_ID>", "workflowName": "ExternalInteractionWorkflow", "createdAt": "2023-07-27T11:50:16.526722900Z", "lastUpdatedAt": "2023-07-27T11:50:19.172489800Z", "runtimeStatus": "COMPLETED", "properties": { "dapr.workflow.custom_status": "", "dapr.workflow.input": "\"World\"", "dapr.workflow.output": "\"Hello World\"" } }
flowchart LR
A[CheckoutService]
B[PaymentService]
C[(Inventory DB)]
A --> B
A --> C
The CheckoutService app contains workflow that processes an order. The workflow takes an OrderItem
as input and returns a CheckoutResult
as output. The CheckoutWorkflow
workflow uses these activities:
NotifyActivity
: Notifies the customer of the progress of the order.CheckInventoryActivity
: Checks if the inventory is sufficient.ProcessPaymentActivity
: Processes the payment, by calling another Dapr service (PaymentService).UpdateInventoryActivity
: Updates the inventory after checking it again.RefundPaymentActivity
: Refunds the payment if theUpdateInventoryActivity
throws an exception.
graph TD
A[Start]
B[NotifyActivity]
C[CheckInventoryActivity]
X{Sufficient Inventory?}
D[ProcessPaymentActivity]
P{Payment successful?}
E[UpdateInventoryActivity]
F[RefundPaymentActivity]
BB[NotifyActivity]
BBB[NotifyActivity]
XX{Sufficient Inventory?}
Z[End]
A --> |OrderItem| B --> C
C --> X
X -->|"[Yes]"| D
X -->|"[No] CheckoutResult(Processed:false)"| Z
D --> P
P -->|"[Yes]"| E --> XX
P -->|"[No] CheckoutResult(Processed:false)"| Z
XX -->|"[Yes]"| BB --> |"CheckoutResult(Processed:true)"| Z
XX -->|"[No]"| BBB --> F --> |"CheckoutResult(Processed:false)"| Z
The CheckInventoryActivity
and UpdateInventoryActivity
classes use Dapr's state management building block to manage the inventory in a Redis state store.
The ProcessPaymentActivity
and RefundPaymentActivity
call out to the PaymentService.
Next to the workflow, this application has an InventoryController
with the following endpoints:
GET http://localhost:5064/inventory
: retrieves the inventoryDELETE http://localhost:5064/inventory
: clears the inventoryPOST http://localhost:5064/inventory/restock
: restocks the inventory
The InventoryController
also uses Dapr's state management building block.
The CheckoutService app relies on the PaymentService app to process the payment. The PaymentService app is a small ASP.NET app that exposes two endpoints:
/pay
: processes the payment/refund
: refunds the payment
The PaymentService app uses the Dapr Configuration API to read the isPaymentSuccess
configuration item from the configuration store (Redis). If the item key is not present, or if the item value is set to the string "true", the payment will be successful. If the item value is set to the string "false", the payment will fail. Use this setting to simulate a failed payment and check the workflow result.
Setting the configuration item is done via the redis-cli in the dapr_redis docker container:
docker exec dapr_redis redis-cli MSET isPaymentSuccess "true"
To configure the PaymentService to return a failed payment response use:
docker exec dapr_redis redis-cli MSET isPaymentSuccess "false"
Set the isPaymentSuccess
config item to "true" and start the PaymentService as follows:
-
Open a new terminal, change to the PaymentService directory and build the ASP.NET app:
cd PaymentService dotnet build
-
Run the app using the Dapr CLI:
dapr run --app-id payment --app-port 5063 --dapr-http-port 3501 --resources-path ./ResourcesLocal dotnet run
-
Open a new terminal, change to the CheckoutService directory and build the ASP.NET app:
cd CheckoutService dotnet build
-
Run the app using the Dapr CLI:
dapr run --app-id checkout --app-port 5064 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run
Ensure the --app-port is the same as the port specified in the launchSettings.json file.
-
Check the inventory using cURL, or use the checkout.http file if you're using VSCode with the REST client:
curl -X POST http://localhost:5064/inventory/restock
If the quantity of the items is not 0, clear the inventory by running:
curl -X POST http://localhost:5064/inventory/clear
-
Try ordering 100 paperclips while the inventory is not sufficient. Start the
CheckoutWorkflow
via the Workflow HTTP API:curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/CheckoutWorkflow/start?instanceID=1234f \ -H "Content-Type: application/json" \ -d '{"Name": "Paperclips", "Quantity": 100}'
Note that
1234f
in the URL is the workflow instance ID. This can be any string you want.Expected result:
{ "instanceID": "<WORKFLOW_ID>" }
Pay attention to the console output. A message will appear that indicates the inventory is insufficient.
-
Check the workflow status via Workflow HTTP API:
curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234f
Expected result:
{ "instanceID": "<WORKFLOW_ID>", "workflowName": "CheckoutWorkflow", "createdAt": "2023-06-19T13:37:30.261385700Z", "lastUpdatedAt": "2023-06-19T13:37:30.303315100Z", "runtimeStatus": "COMPLETED", "properties": { "dapr.workflow.custom_status": "\"Stopped order process due to insufficient inventory.\"", "dapr.workflow.input": "{\"Name\": \"Paperclips\", \"Quantity\": 125}", "dapr.workflow.output": "{\"Processed\":false}" } }
Depending on how quick the status is retrieved after starting the workflow, the
dapr.workflow.runtime_status
could still be"RUNNING"
. Repeat the GET status request until the status is"COMPLETED"
. -
Restock the inventory:
curl -X POST http://localhost:5064/inventory/restock
Expected result:
HTTP 200 OK
-
Try ordering paperclips again, now within the limits of the inventory. Start the
CheckoutWorkflow
via the Workflow HTTP API:curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/CheckoutWorkflow/start?instanceID=1234g \ -H "Content-Type: application/json" \ -d '{"Name": "Paperclips", "Quantity": 25}'
Note that
1234g
in the URL is the workflow instance ID. This can be any string you want.Expected result:
{ "instanceID": "<WORKFLOW_ID>" }
Pay attention to the console output. Messages will appear that indicate the inventory is sufficient and payment has been processed successfully.
-
Check the workflow status via Workflow HTTP API:
curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234g
Expected result:
{ "instanceID": "<WORKFLOW_ID>", "workflowName": "CheckoutWorkflow", "createdAt": "2023-06-19T13:35:42.126109Z", "lastUpdatedAt": "2023-06-19T13:35:53.644632200Z", "runtimeStatus": "COMPLETED", "properties": { "dapr.workflow.custom_status": "", "dapr.workflow.input": "{\"Name\": \"Paperclips\",\"Quantity\": 25}", "dapr.workflow.output": "{\"Processed\":true}" } }
-
Inspect the logs in ZipKin:
localhost:9411/zipkin
. Find the entry markedcheckout:create_orchestration||checkoutworkflow
and show the details. You'll now see a timeline of the workflow at the top, and the activities underneath.
Now try these different scenarios and check the workflow result.
Start the CheckoutWorkflow with:
- Shutting down the CheckoutService app once the CheckoutWorkflow has started. Restart the CheckoutService and watch the logs.
- A failing payment (set the
isPaymentSuccess
configuration item to "false"). - Shut down the PaymentService completely (check the logs for the retry attempts).
Any questions or comments about this sample? Join the Dapr discord and post a message the #workflow
channel.
Have you made something with Dapr? Post a message in the #show-and-tell
channel, we love to see your creations!