Workflow Protocol - Activity Lifecycle

Low-level description of the Workflow building block internals.

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

  1. Scheduling: An orchestration requests an activity by sending a ScheduleTask action to the Dapr engine.
  2. Work Item Dispatch: The Dapr engine enqueues an activity task. When an activity worker (SDK) is available, the engine sends an ActivityWorkItem via the GetWorkItems stream.
  3. 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.
  4. Reporting: After the activity logic finishes, the SDK sends a CompleteActivityTask request back to Dapr.
    • Success: The SDK provides the serialized output in the result field.
    • Failure: The SDK provides failure_details (error message, type, stack trace).

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:

  1. Distributed Idempotency: If an activity performs a side effect (e.g., charging a credit card), it should use the task_execution_id as an idempotency key.
  2. Distinguishing Retries: Unlike the task_id (which remains constant for a specific step in the workflow), the task_execution_id changes every time the engine retries the activity (e.g., due to a timeout or worker crash).
  3. 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_id against 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_id to the activity implementation logic (e.g., via an ActivityContext).
  • 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

  1. Response Correlation: The sidecar uses the completion_token to reliably match an ActivityResponse (from CompleteActivityTask) to the original task it dispatched.
  2. 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.).
  3. 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_token from the incoming ActivityWorkItem.
  • Propagation: The SDK MUST include the exact same completion_token in the ActivityResponse sent via CompleteActivityTask.
  • 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

FeatureOrchestrationActivity
Execution StyleReplay-based (Deterministic)Direct execution
StateManaged via History EventsNo internal workflow state
Side EffectsForbidden (must use activities)Allowed (IO, Database, etc.)
LifetimeCan be long-running (days/months)Usually short-lived
ConnectivityConnected via GetWorkItemsConnected via GetWorkItems