-
Notifications
You must be signed in to change notification settings - Fork 79
Description
Background
The Dapr Python SDK provides two primary ways to create workflows:
- Legacy Client Approach (now deprecated):
client = DaprClient()
client.invoke_workflow(...)- Workflow Extension (Preferred):
wfr = WorkflowRuntime()
@wfr.workflow(name="task_chain_workflow")
def my_workflow(ctx: DaprWorkflowContext):
result = yield ctx.call_activity(my_activity)
return result
@wfr.activity(name="activity_2")
def my_activity(ctx):
return "activity output"dapr-agents introduces a third abstraction layer with custom decorators:
@workflow(name="task_chain_workflow")
def my_workflow(ctx: DaprWorkflowContext):
result = yield ctx.call_activity(get_character)
return result
@task(description="Pick a random character from The Lord of the Rings")
def get_character() -> str:
passBased on user feedback and my analysis, this third approach creates more complexity than benefits it offers.
Limitations and Complexity Issues
1. Limited Runtime Control and Configuration
Impact: Lost Access to Core Dapr Features
The dapr-agents approach abstracts away the WorkflowRuntime and DaprWorkflowClient, making core Dapr capabilities inaccessible:
# dapr-agents abstracts this away:
# wfr = WorkflowRuntime() # Hidden in WorkflowApp
# wf_client = DaprWorkflowClient() # Hidden in WorkflowAppSome Limitations:
- Single Runtime Pattern: One
WorkflowAppmanages everything vs multiple isolated runtimes with native SDK - Lost Runtime Control: No access to
wfrinstance for advanced configuration, such as: custom retry policies per activity, fine-grained timeout control - Hidden Client Initialization: Automatic
DaprWorkflowClientcreation prevents custom setup, manual workflow scheduling with custom instance IDs - Automatic Runtime Lifecycle: Can't control start/shutdown timing for integration scenarios
2. Cannot Safely Combine @task and @activity
Real-World Problem: Most workflows combine LLM and non-LLM tasks:
# This FAILS - incompatible registration approaches:
wfr = WorkflowRuntime()
# Native SDK activities
@wfr.activity(name="database_lookup")
def lookup_user_data(ctx):
# Direct database call - no LLM needed
return database.get_user(user_id)
# dapr-agents tasks
@task(description="Generate personalized content")
def generate_content(user_data: str) -> str:
pass # LLM call via @task3. Hidden Dependencies and Magic Behavior
Automatic OpenAI Dependency Creation:
# In WorkflowTask.model_post_init():
if self.description and not self.llm:
try:
self.llm = OpenAIChatClient() # Hidden dependency!
except Exception as e:
logger.warning(f"Could not create default OpenAI client: {e}")Problems:
- ✅ Implicit dependency when using
description=""parameter - ✅ Hidden import:
from dapr_agents.llm.openai import OpenAIChatClient - ✅ Silent failure if OpenAI environment variables missing
Proposal
Deprecate and Remove:
@workflow()decorator@task()decoratorWorkflowAppworkflow lifecycle management
Offer Alternative Approach
Provide Helper Methods and Documentation:
Instead of adding new workflow and task decorators in dapr-agents, we should provide lightweight helper methods and utilities inside the existing LLM clients and Agent APIs. These helpers would make it simple to call LLMs directly inside a Dapr activity without introducing hidden dependencies or runtime abstractions.
For example, dapr-agents could expose a convenience method such as llm.run_prompt() or llm.ask() that works seamlessly when invoked inside an activity. This allows developers to write workflows with the native WorkflowRuntime API, register activities normally, and still take advantage of dapr-agents LLM integrations when needed.
This strategy keeps runtime control in the hands of the developer, avoids mixing registration models (@task vs @activity), and ensures dependencies remain explicit. It also reduces complexity for debugging and makes documentation easier: we can show clear patterns where non-LLM activities and LLM-powered activities coexist naturally.
Benefits
- Full Dapr Control: Access to all native
WorkflowRuntimecapabilities - Safe Task Combination: Mix LLM and non-LLM activities seamlessly
- Explicit Dependencies: Clear LLM client imports and setup, easier to learn, and maintain.
- Simpler Debugging: Direct stack traces without abstraction layers
Future Exploration
After deprecation, explore if a minimal activity annotation variation could provide LLM convenience without the limitations:
@wfr.activity(name="content_generation")
@llm_enabled(description="Generate marketing content")
def generate_content(ctx, topic: str) -> str:
pass # Optional: Pure LLM calls where beneficialThis would need to:
- ✅ Preserve runtime control
- ✅ Work alongside native
@wfr.activity - ✅ Avoid hidden dependencies
Implementation approach
Phase 1: Deprecation and Helper Methods
-
Mark
@workflowand@taskas deprecated -
Add
WorkflowHelpersmodule for common LLM patterns -
Update documentation and quickstarts
-
Show native Dapr integration patterns
Phase 3: Removal
- Remove deprecated decorators
- Clean up
WorkflowAppworkflow management - Focus on LLM client and agent utilities only
Conclusion
While the @workflow and @task abstractions demonstrate the powerful patterns possible with dapr-agents (as shown in the building effective Dapr Agents article), they introduce complexity that outweighs their benefits.
The proposed approach preserves all the capabilities demonstrated in those patterns while maintaining compatibility with native Dapr workflows and providing users full control over their workflow runtime and configuration.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status