Demos applications that use the Dapr Workflow building block.
Read the blog posts:
-
- Ensure that you're using v1.15 (or higher) of the Dapr runtime and the CLI.
-
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
HelloWorldWorkflowvia 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/workflows/dapr/HelloWorldWorkflow/start?instanceID=1234a \ -H "Content-Type: application/text/plain" \ -d '"World"'
Note that
1234ain 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/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
ChainingWorkflowvia 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/workflows/dapr/ChainingWorkflow/start?instanceID=1234b \ -H "Content-Type: application/text/plain" \ -d '"World"'
Note that
1234bin 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/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
FanOutFanInWorkflowvia 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/workflows/dapr/FanOutFanInWorkflow/start?instanceID=1234c \ -H "Content-Type: application/json" \ -d '["Amsterdam", "Chicago", "New York"]'
Note that
1234cin 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/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
MonitorWorkflowvia 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/workflows/dapr/MonitorWorkflow/start?instanceID=1234d \ -H "Content-Type: application/text/plain" \ -d '0'
Note that
1234din 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/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
TimerWorkflowvia 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/workflows/dapr/TimerWorkflow/start?instanceID=1234e \ -H "Content-Type: application/json" \ -d '{"DateTime": "2023-05-29T13:44:00+00:00","Name":"World"}'
Note that
1234ein 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/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
ExternalInteractionWorkflowvia 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/workflows/dapr/ExternalInteractionWorkflow/start?instanceID=1234f \ -H "Content-Type: application/text/plain" \ -d '"World"'
Note that
1234fin 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/workflows/dapr/1234f
If you check the status within the specified timeout, the
runtimeStatuswill 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
runtimeStatuswill beCOMPLETED, with acustom_statusthat 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
raiseEventendpoint, 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/workflows/dapr/ExternalInteractionWorkflow/start?instanceID=1234f \ -H "Content-Type: application/text/plain" \ -d '"World"'
curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/1234f/raiseEvent/ \ -H "Content-Type: application/json" \ -d '{"IsApproved":true}'
curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/1234f
The
runtimeStatusshould 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 theUpdateInventoryActivitythrows 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" before continuing.
-
Open a terminal, change to the OrderProcess/PaymentService directory and build the ASP.NET app:
cd OrderProcess/PaymentService dotnet build -
Change to the OrderProcess/CheckoutService directory and build the ASP.NET app:
cd OrderProcess/CheckoutService dotnet build
-
Using the terminal, change to the OrderProcess directory and start the services using the Dapr CLI with multi-app run:
dapr run -f .This will start both the CheckoutService and PaymentService using the multi-app run configuration specified in the dapr.yaml 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
CheckoutWorkflowvia the Workflow HTTP API:curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/CheckoutWorkflow/start?instanceID=1234f \ -H "Content-Type: application/json" \ -d '{"Name": "Paperclips", "Quantity": 100}'
Note that
1234fin 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/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_statuscould 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
CheckoutWorkflowvia the Workflow HTTP API:curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/CheckoutWorkflow/start?instanceID=1234g \ -H "Content-Type: application/json" \ -d '{"Name": "Paperclips", "Quantity": 25}'
Note that
1234gin 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/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||checkoutworkflowand 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
isPaymentSuccessconfiguration 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!
