Agentic Workflows

Design patterns for building autonomous AI agents that accomplish complex tasks

13 min readUpdated 11/9/2025
💡ELI5: What are Agentic Workflows?

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!

🛠️For Product Managers & Builders

When to Use Agentic Workflows

Ideal for:
  • • 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
Not suitable for:
  • • 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

ReAct
Reason then act in a loop
Plan-and-Execute
Create plan then execute steps
Routing
Direct to specialized agents
Orchestrator-Workers
Delegate to specialized agents

Key Considerations

Termination: Always set max steps and budgets to prevent infinite loops
Observability: Log all actions for debugging and monitoring
Guardrails: Require confirmations for sensitive operations
Testing: Agents are probabilistic - test many scenarios extensively
Deep Dive

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

  1. Start simple: Begin with a workflow, add agency gradually
  2. Clear tool descriptions: Agents are only as good as their tool docs
  3. Limit autonomy: Require approvals for sensitive actions
  4. Track everything: Log all actions for debugging
  5. Set budgets: Max steps, max cost, timeouts
  6. Test extensively: Agents are probabilistic—test many scenarios
  7. 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.

Related Resources

Continue Learning

Ready to Build Autonomous Agents?
Explore frameworks and patterns for building intelligent agents