Agentic Workflows
Design patterns for building autonomous AI agents that accomplish complex tasks
Imagine you ask your friend to plan a birthday party. Instead of you telling them every single step, they figure it out themselves - they make a guest list, pick a venue, order food, and send invitations. That's an agent!
Agentic workflows give AI the ability to plan, make decisions, use tools, and adapt as it works toward a goal - just like your friend planning that party. Instead of following a script, the AI figures out what to do next based on what's happening.
Example: You: "Research our competitors" → Agent: 1) Searches for competitors 2) Visits their websites 3) Extracts pricing 4) Compiles comparison 5) Generates report. All on its own!
When to Use Agentic Workflows
- • Complex, multi-step tasks requiring planning
- • Tasks where the path isn't predetermined
- • Workflows needing tool use and decision-making
- • Research, analysis, and data gathering
- • Automating knowledge work that requires judgment
- • Simple, single-step tasks (use direct prompting)
- • Deterministic workflows (use regular code)
- • Tasks requiring 100% reliability without errors
- • Real-time critical operations (agents add latency)
Core Agent Patterns
Key Considerations
Agentic Workflows
Agentic workflows are design patterns for building AI systems that can accomplish complex, multi-step tasks autonomously. Unlike simple question-answering, agents can plan, use tools, handle errors, and adapt their approach based on results.
What are Agentic Workflows?
An agentic workflow gives an AI model agency—the ability to take actions and make decisions to accomplish a goal. Instead of executing a fixed script, agents:
- Plan their approach
- Execute actions using tools
- Observe the results
- Adapt their strategy
Think of it like delegating a project to a colleague: you give them a goal and trust them to figure out the steps.
Agentic vs Non-Agentic
Non-agentic (simple workflow):
User: "Summarize this document"
AI: [Generates summary]
Done.
Agentic (complex workflow):
User: "Research our competitors' pricing and create a comparison"
Agent:
1. Plans: "I need to search each competitor's website, extract pricing, create a table"
2. Searches competitor A's site
3. Extracts pricing data
4. Searches competitor B's site
5. If data missing: tries alternative approach
6. Compiles into comparison table
7. Reviews for completeness
Done.
Core Agentic Patterns
1. ReAct (Reason + Act)
The foundational pattern: interleave reasoning with actions.
async function reactAgent(goal) {
let context = `Goal: \${goal}\n`
let done = false
while (!done) {
// Thought: Reason about what to do next
const thought = await llm.generate(`
\${context}
Thought: What should I do next to achieve the goal?
`)
// Action: Decide on a tool to use
const action = await llm.generate(`
\${thought}
Action: Which tool should I call?
Action Input: What parameters?
`)
// Execute the action
const observation = await executeTool(action)
// Update context
context += `\${thought}\n\${action}\nObservation: \${observation}\n`
// Check if goal is achieved
done = await llm.generate(`\${context}\nIs the goal achieved?`) === "yes"
}
return generateFinalAnswer(context)
}
When to use: Multi-step tasks requiring tools and reasoning
2. Plan-and-Execute
Create a plan upfront, then execute steps sequentially.
async function planAndExecute(goal) {
// Step 1: Create a plan
const plan = await llm.generate(`
Goal: \${goal}
Create a step-by-step plan to achieve this goal.
Format: numbered list of actions.
`)
// Step 2: Execute each step
const results = []
for (const step of parsePlan(plan)) {
const result = await executeStep(step)
results.push(result)
// Re-plan if step fails
if (result.failed) {
plan = await replan(goal, results)
}
}
// Step 3: Synthesize final answer
return await synthesize(goal, results)
}
When to use: Complex tasks where upfront planning improves efficiency
3. Routing
Direct queries to specialized sub-agents.
async function routingAgent(userQuery) {
// Classify the query
const category = await llm.generate(`
Query: \${userQuery}
Which category does this belong to?
- technical_support
- billing
- sales
- general
`)
// Route to specialist
const specialists = {
technical_support: technicalAgent,
billing: billingAgent,
sales: salesAgent,
general: generalAgent
}
return await specialists[category](userQuery)
}
When to use: Distinct domains requiring specialized knowledge
4. Parallelization
Execute independent tasks simultaneously.
async function parallelAgent(tasks) {
// Identify independent tasks
const dependencies = await analyzeDependencies(tasks)
// Execute parallel batches
const results = {}
for (const batch of dependencies.batches) {
const batchResults = await Promise.all(
batch.map(task => executeTask(task, results))
)
Object.assign(results, batchResults)
}
return results
}
When to use: Tasks with sub-tasks that can run in parallel
5. Orchestrator-Workers
One orchestrator delegates to multiple worker agents.
async function orchestratorPattern(goal) {
const orchestrator = new OrchestratorAgent()
// Orchestrator breaks down the goal
const subgoals = await orchestrator.decompose(goal)
// Delegate to workers
const workers = {
researcher: new ResearchAgent(),
writer: new WriterAgent(),
reviewer: new ReviewAgent()
}
const results = {}
for (const subgoal of subgoals) {
const worker = workers[subgoal.type]
results[subgoal.id] = await worker.execute(subgoal)
}
// Orchestrator synthesizes
return await orchestrator.synthesize(results)
}
When to use: Complex projects with distinct specialized roles
6. Iterative Refinement
Generate, evaluate, and refine outputs.
async function iterativeRefinement(task, maxIterations = 3) {
let output = await llm.generate(task)
for (let i = 0; i < maxIterations; i++) {
// Evaluate the output
const critique = await llm.generate(`
Task: \${task}
Output: \${output}
Critique this output. What could be improved?
`)
// If good enough, stop
if (critique.includes("No improvements needed")) {
break
}
// Refine
output = await llm.generate(`
Task: \${task}
Previous output: \${output}
Critique: \${critique}
Generate an improved version addressing the critique.
`)
}
return output
}
When to use: Quality-critical outputs (writing, code, analysis)
Building a Production Agent
Step 1: Define Capabilities
What can your agent do?
const tools = [
{
name: "search_docs",
description: "Search internal documentation",
execute: searchDocs
},
{
name: "query_database",
description: "Query customer database",
execute: queryDB
},
{
name: "send_email",
description: "Send email to customer",
execute: sendEmail
}
]
Step 2: Implement Core Loop
class Agent {
constructor(tools, maxSteps = 10) {
this.tools = tools
this.maxSteps = maxSteps
this.history = []
}
async run(goal) {
let step = 0
while (step < this.maxSteps) {
// Decide what to do
const action = await this.decide(goal, this.history)
// Goal achieved?
if (action.type === "finish") {
return action.answer
}
// Execute action
const result = await this.execute(action)
// Record history
this.history.push({ action, result })
step++
}
throw new Error("Max steps reached without completion")
}
async decide(goal, history) {
const prompt = `
Goal: \${goal}
Available tools: \${this.tools.map(t => t.description).join(', ')}
History:
\${formatHistory(history)}
What should I do next? Use JSON format:
{
"type": "use_tool" | "finish",
"tool": "tool_name",
"input": "tool_input",
"reasoning": "why this action"
}
`
return await llm.generate(prompt)
}
async execute(action) {
if (action.type === "finish") {
return action.answer
}
const tool = this.tools.find(t => t.name === action.tool)
return await tool.execute(action.input)
}
}
Step 3: Add Error Handling
async function execute(action) {
try {
return await tool.execute(action.input)
} catch (error) {
// Return error to agent so it can adapt
return {
error: true,
message: error.message,
suggestion: "Try a different approach"
}
}
}
Step 4: Implement Guardrails
async function decide(goal, history) {
const action = await llm.generate(decisionPrompt)
// Validate before executing
if (action.tool === "send_email") {
// Require human confirmation
if (!await requestApproval(action)) {
return { type: "finish", answer: "Action cancelled by user" }
}
}
return action
}
Key Design Considerations
When to Stop
Agents need clear termination conditions:
function shouldContinue(history, goal) {
// Max steps
if (history.length >= MAX_STEPS) return false
// Goal achieved
if (goalAchieved(history, goal)) return false
// Stuck in a loop
if (detectLoop(history)) return false
// Errors exceeded threshold
if (countErrors(history) > MAX_ERRORS) return false
return true
}
Memory Management
Long-running agents need memory management:
function manageMemory(history, maxTokens) {
// Keep recent history
const recent = history.slice(-10)
// Summarize older history
const summary = summarize(history.slice(0, -10))
return {
summary,
recentHistory: recent
}
}
Cost Control
Monitor and limit costs:
class CostControlledAgent {
constructor(maxCost = 1.00) {
this.maxCost = maxCost
this.currentCost = 0
}
async run(goal) {
while (this.currentCost < this.maxCost) {
const action = await this.decide()
const cost = estimateCost(action)
if (this.currentCost + cost > this.maxCost) {
return "Budget exceeded, partial result: ..."
}
const result = await this.execute(action)
this.currentCost += cost
if (isComplete(result)) return result
}
}
}
Observability
Track what agents are doing:
class ObservableAgent {
async execute(action) {
console.log({
timestamp: Date.now(),
action: action.type,
tool: action.tool,
reasoning: action.reasoning
})
const result = await tool.execute(action.input)
console.log({
timestamp: Date.now(),
result: summarize(result)
})
return result
}
}
Testing Agents
Unit Tests for Tools
describe("search_docs tool", () => {
it("returns relevant documents", async () => {
const results = await searchDocs("password reset")
expect(results).toContainMatch(/password/i)
})
})
Integration Tests for Workflows
describe("customer support agent", () => {
it("resolves password reset request", async () => {
const result = await agent.run("user forgot password")
expect(result.toolsUsed).toContain("search_docs")
expect(result.toolsUsed).toContain("send_email")
expect(result.answer).toMatch(/reset link sent/i)
})
})
End-to-End Evals
const agentEvals = [
{
goal: "Find pricing for enterprise plan",
expectedTools: ["search_docs"],
successCriteria: (result) => result.includes("$999/month")
},
{
goal: "Cancel user's subscription",
expectedTools: ["query_database", "cancel_subscription"],
requiredApprovals: ["user_confirmation"]
}
]
Common Pitfalls
Infinite loops: Agent repeats the same actions indefinitely. Add loop detection.
Scope creep: Agent goes off-task. Use focused prompts and validate each action.
Tool misuse: Agent calls wrong tools. Improve tool descriptions and add examples.
Overconfidence: Agent doesn't ask for help when stuck. Implement uncertainty detection.
Ignoring errors: Agent doesn't adapt when actions fail. Return errors to the agent context.
Best Practices
- Start simple: Begin with a workflow, add agency gradually
- Clear tool descriptions: Agents are only as good as their tool docs
- Limit autonomy: Require approvals for sensitive actions
- Track everything: Log all actions for debugging
- Set budgets: Max steps, max cost, timeouts
- Test extensively: Agents are probabilistic—test many scenarios
- Provide feedback: Return rich error messages so agents can adapt
Agentic workflows are the future of AI applications. They move beyond simple prompting to systems that can actually get things done. Start with simple patterns like ReAct, validate thoroughly, and gradually increase autonomy as you build confidence in your system's reliability.