Workflow Protocol - Activity Lifecycle
Activity Lifecycle
Activities are the basic units of work in a Dapr Workflow. Unlike orchestrations, activities are not replayed and do not need to be deterministic. They are executed exactly once per “schedule” (though retries may occur).
Execution Flow
- Scheduling: An orchestration requests an activity by sending a
ScheduleTaskaction to the Dapr engine. - Work Item Dispatch: The Dapr engine enqueues an activity task. When an activity worker (SDK) is available, the
engine sends an
ActivityWorkItemvia theGetWorkItemsstream. - Execution: The SDK receives the
ActivityWorkItem, which contains:name: The name of the activity to execute.input: The input data for the activity.instance_id: The ID of the workflow instance that scheduled the activity.task_id: A unique identifier for this specific activity execution.task_execution_id: A unique identifier for the specific attempt of this activity. This is useful for implementing idempotency in activity logic.completion_token: An opaque token used to correlate the response with this specific work item.
- Reporting: After the activity logic finishes, the SDK sends a
CompleteActivityTaskrequest back to Dapr.- Success: The SDK provides the serialized output in the
resultfield. - Failure: The SDK provides
failure_details(error message, type, stack trace).
- Success: The SDK provides the serialized output in the
Task Execution IDs
The task_execution_id (also known as the Task Execution Key) is a unique, runtime-generated string (typically a UUID)
that identifies a specific attempt to execute an activity task.
Why it matters to the SDK
While the Workflow SDK is generally stateless between work items, the task_execution_id provides critical context
for the Activity Worker:
- Distributed Idempotency: If an activity performs a side effect (e.g., charging a credit card), it
should use the
task_execution_idas an idempotency key. - Distinguishing Retries: Unlike the
task_id(which remains constant for a specific step in the workflow), thetask_execution_idchanges every time the engine retries the activity (e.g., due to a timeout or worker crash). - Zombie Detection: If an activity worker takes too long and the engine times it out and retries on another
worker, the original worker might eventually finish. By checking the
task_execution_idagainst a persistent store or external API, the worker can determine if it is a “zombie” whose results are no longer wanted.
Implementation Guidelines for SDKs:
- Expose to User: The SDK MUST expose the
task_execution_idto the activity implementation logic (e.g., via anActivityContext). - Do Not Cache: The SDK should not attempt to cache or reuse this ID across different work items.
- Opaque Usage: The SDK should treat the value as an opaque string. It is generated by the Dapr sidecar when the activity is dispatched and is not something the SDK needs to create or parse.
Completion Tokens
The completion_token is an opaque string generated by the Dapr runtime and delivered to the SDK as part of the
ActivityWorkItem.
Purpose and Intent
- Response Correlation: The sidecar uses the
completion_tokento reliably match anActivityResponse(fromCompleteActivityTask) to the original task it dispatched. - Stateless Tracking: It allows the sidecar to remain stateless or minimize state lookups when receiving a completion, as the token contains (or points to) the necessary context (instance ID, task ID, etc.).
- Zombie Prevention: If an activity times out and is retried, the new attempt will have a different
completion_token. If the original “zombie” worker eventually responds with the old token, the sidecar can easily identify and ignore the late response.
SDK Implementation Guidelines
- Capture: The SDK MUST capture the
completion_tokenfrom the incomingActivityWorkItem. - Propagation: The SDK MUST include the exact same
completion_tokenin theActivityResponsesent viaCompleteActivityTask. - Opaqueness: The SDK MUST treat the token as a black box. It should not attempt to parse, modify, or construct its own tokens.
- Storage: While the activity is executing, the SDK must keep this token in memory (e.g., in the
ActivityContext).
Task Activity IDs
In the Dapr runtime (specifically when using the Actors backend), activities are represented as actors. Each activity execution has a unique Task Activity ID (also known as the Activity Actor ID).
The ID follows a specific pattern:
{workflowInstanceID}::{taskID}::{generation}
- workflowInstanceID: The unique ID of the workflow instance that scheduled the activity.
- taskID: The sequence number of the task within the workflow execution (e.g., 0, 1, 2…).
- generation: A counter that increments if the workflow is restarted or “continued as new”.
This unique ID ensures that activity executions are isolated and can be tracked reliably across retries and restarts.
Retries
Dapr handles activity retries based on the policy defined in the orchestration (if the SDK supports defining retry
policies in the ScheduleTask action). If an activity fails and a retry policy is in place, the engine will re-enqueue
the activity task after the specified delay.
From the activity worker’s perspective, a retry is simply a new ActivityWorkItem with the same name and input, but
potentially a different task_id (or the same, depending on the backend implementation).
Idempotency
Because activities might be executed more than once (e.g., if the worker crashes after execution but before reporting completion), it is recommended that activity logic be idempotent where possible.
Comparison with Workflows
| Feature | Orchestration | Activity |
|---|---|---|
| Execution Style | Replay-based (Deterministic) | Direct execution |
| State | Managed via History Events | No internal workflow state |
| Side Effects | Forbidden (must use activities) | Allowed (IO, Database, etc.) |
| Lifetime | Can be long-running (days/months) | Usually short-lived |
| Connectivity | Connected via GetWorkItems | Connected via GetWorkItems |