In a recent research publication, Anthropic shared valuable insights about building effective Large Language Model (LLM) agents. This publication has been widely accepted as the definition of what "agentic" systems mean in practical terms. The key insight is that "agentic" isn't a binary concept but rather exists along a spectrum.
On one end of the spectrum, we have agentic workflows that are more predictable, with well-defined decision paths and more deterministic outcomes. On the other end, we have AI agents with greater autonomy to make and take decisions to accomplish goals without following strict predefined paths.
Anthropic makes an important architectural distinction between two types of agentic systems:
- Workflows: Systems where LLMs and tools are orchestrated through predefined code paths (more prescriptive)
- Agents: Systems where LLMs dynamically direct their own processes and tool usage (more autonomous)
The big takeaway is that while fully autonomous agents might seem appealing, workflows often provide better predictability and consistency for well-defined tasks. This aligns with enterprise requirements where reliability and maintainability are crucial.
The patterns we'll explore in this article start with workflow-based approaches, which offer more predictability and control, before moving toward more autonomous agent patterns. I've created a GitHub repository demonstrating how to implement these patterns using the Dapr Agents framework. Dapr Agents is a recently announced Dapr project, built in Python, that simplifies the creation of agentic systems easily, but with the full power and reliability of Dapr underneath. This project aims to showcase implementing Anthropic's patterns using a common travel planning scenario to demonstrate how Dapr Agents reduces complexity and boilerplate code.
Pattern 1: Augmented LLM
The Augmented LLM pattern is the foundational building block for any kind of agentic system. It enhances a language model with external capabilities like memory and tools.

Use Cases
The Augmented LLM pattern shines in scenarios like:
- Personal assistants that remember user preferences
- Customer support agents that access product information
- Research tools that retrieve and analyze information
- Domain-specific applications that need special tools
Implementation with Dapr Agents
Dapr Agents makes this pattern straightforward. Here's how:
@tool
def search_flights(destination: str) -> List[FlightOption]:
"""Search for flights to the specified destination."""
# Mock flight data (would be an external API call in a real app)
return [
FlightOption(airline="SkyHighAir", price=450.00),
FlightOption(airline="GlobalWings", price=375.50)
]
# Create agent with memory and tools
travel_planner = Agent(
name="TravelBuddy",
role="Travel Planner Assistant",
instructions=["Remember destinations and help find flights"],
tools=[search_flights]
)
As expected by any agentic framework nowadays, Dapr Agents automatically handle:
- Agent configuration - Simple configuration with role and instructions guides the LLM behavior
- Memory persistence - The agent maintains conversation history without additional code
- Tool integration - The @tool decorator handles input validation, type conversion, and output formatting
This implementation ensures minimal boilerplate and extensibility.
Pattern 2: Stateful LLM
The Stateful LLM pattern extends the Augmented LLM by adding durability to agent interactions. While the Augmented LLM pattern provides a good foundation, enterprise applications often need persistence and reliability that go beyond simple in-memory capabilities. The Stateful LLM pattern addresses this gap, making it ideal for production environments where reliability is critical.
This pattern isn't one of Anthropic's original patterns, but rather demonstrates how easily the Augmented LLM pattern can be transformed into a durable agent with Dapr. What makes this approach powerful is that it doesn't just persist message history – it dynamically creates durable activities for each interaction and stores them reliably in Dapr's state stores.

Use Cases
This pattern is ideal for:
- Long-running tasks that may take minutes or days to complete
- Distributed systems running across multiple services
- Customer support handling complex multi-session tickets
- Business processes with LLM intelligence at each step
Implementation with Dapr Agents
Dapr Agents delivers impressive capabilities in this area through its AssistantAgent class:
travel_planner = AssistantAgent(
name="TravelBuddy",
role="Travel Planner",
goal="Help users find flights and remember preferences",
instructions=[
"Find flights to destinations",
"Remember user preferences",
"Provide clear flight info"
],
tools=[search_flights],
message_bus_name="messagepubsub",
state_store_name="workflowstatestore",
state_key="workflow_state",
agents_registry_store_name="workflowstatestore",
agents_registry_key="agents_registry",
)
The implementation follows Dapr's sidecar architecture model, where all the infrastructure concerns – persistence, messaging, reliability – are handled by the Dapr runtime running alongside your application. This clean separation of concerns means your agent code can focus entirely on the business and agentic logic without being cluttered with infrastructure code.
By simply swapping the Agent class with AssistantAgent and running with a Dapr sidecar, your application inherits enterprise-grade durability and reliability features without any changes to your business logic. This demonstrates one of Dapr's core strengths – the ability to progressively enhance applications with production-ready capabilities through configuration rather than code changes.
The advantages of using Dapr for stateful LLMs include:
- Persistent Memory - Agent state is stored in Dapr's state store, surviving process crashes and system restarts
- Workflow Orchestration - All agent interactions managed through Dapr's workflow system with durability and recoverability
- Service Exposure - REST endpoints for workflow management come out of the box
This approach delivers production reliability with minimal code complexity.
Pattern 3: Prompt Chaining
As your application requirements grow more complex, you'll often need to break problems down into smaller steps. The Prompt Chaining pattern addresses this by decomposing a complex task into a sequence of steps, where each LLM call processes the output of the previous one. This pattern allows for better control of the overall process, validation between steps, and specialization of each step.

Use Cases
This pattern works well for scenarios like:
- Content generation (creating outlines first, then expanding, then reviewing...)
- Multi-stage analysis (performing complex analysis into sequential steps)
- Quality assurance workflows (adding validation between processing steps)
Implementation with Dapr Agents
Dapr's workflow orchestration implements this pattern elegantly, allowing developers to use familiar programming constructs and patterns to define complex multi-step processes:
@workflow(name='travel_planning_workflow')
def travel_planning_workflow(ctx: DaprWorkflowContext, user_input: str):
# Step 1: Extract destination using a simple prompt (no agent)
destination_text = yield ctx.call_activity(extract_destination, input=user_input)
# Gate: Check if destination is valid
if "paris" not in destination_text.lower():
return "Unable to create itinerary: Destination not recognized or supported."
# Step 2: Generate outline with planning agent (has tools)
travel_outline = yield ctx.call_activity(create_travel_outline, input=destination_text)
# Step 3: Expand into detailed plan with itinerary agent (no tools)
detailed_itinerary = yield ctx.call_activity(expand_itinerary, input=travel_outline)
return detailed_itinerary
The implementation showcases three different approaches to task definitions:
- Basic prompt-based task (no agent)
- Agent-based task without tools
- Agent-based task with tools
Dapr Agents' workflow orchestration provides several advantages for this pattern:
- Workflow as Code - Tasks are defined in developer-friendly ways to write business logic.
- Workflow Persistence - Long-running chained tasks survive process restarts
- Hybrid Execution - Easily mix prompts, agent calls, and tool-equipped agents
Pattern 4: Routing
As your application grows to handle diverse types of requests, you'll need more specialized handling for different inputs. The Routing pattern addresses this by classifying an input and directing it to a specialized follow-up task, allowing for the separation of concerns. In this way, you can create specialized experts for different types of queries, rather than trying to handle everything with a single one-size-fits-all approach.

Use Cases
The Routing pattern is best in situations like:
- Resource optimization (sending simple queries to smaller models)
- Multi-lingual support (routing queries to language-specific handlers)
- Customer support (directing different query types to specialized handlers)
- Content creation (routing writing tasks to topic specialists)
- Hybrid LLM systems (using different models for different tasks)
Implementation with Dapr Agents
Dapr Agents provides an elegant code-based approach:
@workflow(name="travel_assistant_workflow")
def travel_assistant_workflow(ctx: DaprWorkflowContext, input_params: dict):
# Route to the appropriate specialized handler
if query_type == QueryType.ATTRACTIONS:
response = yield ctx.call_activity(
handle_attractions_query,
input={"query": user_query}
)
elif query_type == QueryType.ACCOMMODATIONS:
response = yield ctx.call_activity(
handle_accommodations_query,
input={"query": user_query}
)
elif query_type == QueryType.TRANSPORTATION:
response = yield ctx.call_activity(
handle_transportation_query,
input={"query": user_query}
)
else:
response = "I'm not sure how to help with that specific travel question."
return response
The advantages of Dapr's approach include:
- Familiar Control Flow - Uses standard programming if-else constructs for routing
- Extensibility - The control flow can be extended for future requirements easily
- LLM-Powered Classification - Uses an LLM to categorize queries
This pattern highlights how Dapr makes complex decision logic easier to implement and maintain with code-based workflows.
Pattern 5: Parallelization
Sometimes solving a problem requires analyzing multiple dimensions simultaneously. The Parallelization pattern enables this by allowing LLMs to work on different aspects of a task concurrently, with outputs aggregated programmatically. This pattern improves efficiency for complex tasks with independent subtasks that can be processed in parallel.

Use Cases
This pattern is effective for scenarios like:
- Complex research (processing different aspects of a topic in parallel)
- Multi-faceted planning (creating various elements of a plan concurrently)
- Product analysis (analyzing different aspects of a product in parallel)
- Content creation (generating multiple sections of a document simultaneously)
Implementation with Dapr Agents
Dapr's workflow DSL handles parallel flow creation elegantly:
@workflow(name="travel_planning_workflow")
def travel_planning_workflow(ctx: DaprWorkflowContext, input_params: dict):
# Process three aspects of the travel plan in parallel
parallel_tasks = [
ctx.call_activity(research_attractions, input={"destination": destination, "preferences": preferences, "days": days}),
ctx.call_activity(recommend_accommodations, input={"destination": destination, "preferences": preferences, "days": days}),
ctx.call_activity(suggest_transportation, input={"destination": destination, "preferences": preferences, "days": days})
]
# Wait for all parallel tasks to complete
results = yield wfapp.when_all(parallel_tasks)
return final_plan
The benefits of using Dapr for parallelization include:
- Simplified Concurrency - Handles the complex orchestration of parallel tasks
- Automatic Synchronization - Waits for all parallel tasks to complete
- Workflow Durability - The entire parallel process is durable and recoverable
Pattern 6: Orchestrator-Workers
For highly complex tasks where the number and nature of subtasks can't be known in advance, the Orchestrator-Workers pattern offers a powerful solution. This pattern features a central orchestrator LLM that dynamically breaks down tasks, delegates them to worker LLMs, and synthesizes their results. Unlike the previous patterns where the workflow is predefined, here the orchestrator determines the workflow dynamically based on the specific input.

Use Cases
This pattern is well-suited for:
- Software development tasks spanning multiple files
- Research gathering information from multiple sources
- Business analysis evaluating different facets of a complex problem
- Content creation combining specialized content from various domains
Implementation with Dapr Agents
Dapr's workflow system provides a clean implementation of this pattern too:
@workflow(name="orchestrator_travel_planner")
def orchestrator_travel_planner(ctx: DaprWorkflowContext, input_params: dict):
# Step 1: Orchestrator analyzes request and determines required tasks
plan_result = yield ctx.call_activity(
create_travel_plan,
input={"request": travel_request}
)
tasks = plan_result.get("tasks", [])
# Step 2: Execute each task with a worker LLM
worker_results = []
for task in tasks:
task_result = yield ctx.call_activity(
execute_travel_task,
input={"task": task}
)
worker_results.append({
"task_id": task["task_id"],
"result": task_result
})
# Step 3: Synthesize the results into a cohesive travel plan
final_plan = yield ctx.call_activity(
synthesize_travel_plan,
input={
"request": travel_request,
"results": worker_results
}
)
return final_plan
The advantages of Dapr for the Orchestrator-Workers pattern include:
- Dynamic Planning - The orchestrator can dynamically create subtasks based on input
- Worker Isolation - Each worker focuses on solving one specific aspect of the problem
- Simplified Synthesis - The final synthesis step combines results into a coherent output
Pattern 7: Evaluator-Optimizer
Quality is often achieved through iteration and refinement. The Evaluator-Optimizer pattern implements a dual-LLM process where one model generates responses while another provides evaluation and feedback in an iterative loop. This pattern is particularly valuable when output quality is critical and benefits from multiple refinement cycles.

Use Cases
This pattern works well for:
- Content creation requiring adherence to specific style guidelines
- Translation needing nuanced understanding and expression
- Code generation meeting specific requirements and handling edge cases
- Complex search requiring multiple rounds of information gathering and refinement
Implementation with Dapr Agents
Implemented with Dapr, this pattern looks as the following:
@workflow(name="evaluator_optimizer_travel_planner")
def evaluator_optimizer_travel_planner(ctx: DaprWorkflowContext, input_params: dict):
# Generate initial travel plan
current_plan = yield ctx.call_activity(
generate_travel_plan,
input={"request": travel_request, "feedback": None}
)
# Evaluation loop
iteration = 1
meets_criteria = False
while iteration <= max_iterations and not meets_criteria:
# Evaluate the current plan
evaluation = yield ctx.call_activity(
evaluate_travel_plan,
input={"request": travel_request, "plan": current_plan}
)
score = evaluation.get("score", 0)
feedback = evaluation.get("feedback", [])
meets_criteria = evaluation.get("meets_criteria", False)
# Stop if we meet criteria or reached max iterations
if meets_criteria or iteration >= max_iterations:
break
# Optimize the plan based on feedback
current_plan = yield ctx.call_activity(
generate_travel_plan,
input={"request": travel_request, "feedback": feedback}
)
iteration += 1
return {
"final_plan": current_plan,
"iterations": iteration,
"final_score": score
}
The benefits of using Dapr for this pattern include:
- Iterative Improvement Loop - Manages the feedback cycle between generation and evaluation
- Quality Criteria - Enables clear definition of what constitutes acceptable output
- Maximum Iteration Control - Prevents infinite loops by enforcing iteration limits
This pattern can be extended to support longer or indefinite iterations using Dapr Workflow's continue-as-new API, which enables eternal workflows without infinite loops.
Pattern 8: Autonomous Agent
Moving to the far end of the agentic spectrum, the Autonomous Agent pattern represents a fundamental shift from the workflow-based approaches we've explored so far. Instead of a system with predefined steps where LLMs are simply participants in a choreographed process, we now have an autonomous agent that can plan its own steps and execute them based on its understanding of the goal. The agent decides which tools to use, when to use them, and how to interpret their results—all without requiring a human-designed workflow to guide it. This autonomy is powerful for open-ended tasks where the exact sequence of steps can't be known in advance, but it also requires careful design to ensure the agent operates within appropriate boundaries.

Use Cases
This pattern excels in scenarios like:
- Research assistants finding and synthesizing information
- Customer support resolving complex issues through gathering information
- Software development debugging and fixing issues
- Personal assistants handling scheduling and information lookup
Implementation with Dapr Agents
Dapr's ReAct agent provides a powerful implementation of this pattern:
# Create the ReAct agent with both tools
travel_agent = ReActAgent(
name="TravelHelper",
role="Travel Assistant",
instructions=["Help users plan trips by providing weather and activities"],
tools=[search_weather, find_activities]
)
The advantages of using Dapr for autonomous agents include:
- ReAct Framework - Implements the Reasoning-Action-Observation loop for structured problem-solving
- Self-Directed Process - The agent decides which tools to use and when to use them
Conclusion
The journey from simple agentic workflows to fully autonomous agents represents a spectrum of approaches for integrating LLMs into your applications. As we've seen through these eight patterns, different use cases call for different levels of agency and control. The key is selecting the right pattern for your specific needs:
- Start with simpler patterns like Augmented LLM and Prompt Chaining for well-defined tasks where predictability is crucial
- Progress to more dynamic patterns like Parallelization and Orchestrator-Workers as your needs grow more complex
- Consider fully autonomous agents only for open-ended tasks where the benefits of flexibility outweigh the need for strict control
Dapr Agents provides a powerful, production-ready framework for implementing all these patterns, offering a unique combination of developer-friendly APIs and enterprise-grade capabilities. By leveraging Dapr's mature ecosystem, you get the benefits of portability, reliability, and observability without the complexity typically associated with production-grade agent systems.
Try It Yourself
As we've seen throughout these examples, Dapr Agents is a framework for implementing both workflow-based and autonomous agent-based systems. The key advantages include:
- Code-based Orchestration - Clean and maintainable way to implement complex interactions
- Workflow Durability - Persistent, recoverable workflows that survive process crashes
- Tool Integration - Simple tool definition and integration
- Abstracted State Management - Built-in support for state persistence
- True OSS and Vendor Neutrality - Avoids vendor lock-in; supports multiple LLMs and cloud/on-prem deployment
- Production-Ready Observability - Native support for metrics (Prometheus) and tracing (OpenTelemetry)
- Built-in Multi-Agent Coordination - Agents can communicate via Dapr's event-driven messaging. We will review multi-agent patterns in the next blog post
The best way to learn is by doing:
- Clone this repository and run the patterns yourself.
- Clone dapr-agents and run the quickstarts.
I'd love to hear about what you build with Dapr-agents. Join Dapr Discord and share what you have built on the Dapr Agents channel!