paulserban.eu

Writing Edition

Paul Serban

Writing, snippets & book notes

AI Workflows vs. AI Agents vs. Agentic AI: A Developer's Guide to Building Intelligent Systems

Cutting through the hype to understand when determinism, autonomy, or a blend of both is the right architecture for your AI-powered system

Introduction

The vocabulary around AI-powered systems has expanded faster than the engineering practices around it. Terms like "AI agent," "agentic AI," and "AI workflow" appear interchangeably in documentation, product pages, and conference talks - yet they describe architecturally distinct systems with meaningfully different trade-offs. Using the wrong mental model leads to real engineering problems: systems that are either too rigid to handle variance or too autonomous to trust in production.

This article is for software engineers and technical leads who need to make concrete architectural decisions: when to use a fixed pipeline, when to introduce an agent loop, and when to build something in between. We will examine each paradigm in terms of control flow, failure modes, observability, and cost - then work through practical implementation patterns in TypeScript and Python that reflect how these systems behave at the boundaries, not just in the happy path.

The goal is not to declare a winner. These patterns exist on a spectrum, and the best production systems often compose them deliberately. Understanding the distinctions is a prerequisite for doing that composition well.

The Problem: Architecture Choices That Look Easy But Aren't

When engineers first encounter modern LLM APIs, the natural impulse is to treat the model as a smart function: give it input, get output, move on. This works for isolated tasks. It breaks down as soon as you need the model to do more than one thing in sequence, make decisions based on intermediate state, or act on external systems.

At that point, you face a genuine architectural fork. You can structure the LLM calls into a deterministic pipeline - deciding in advance what steps happen, in what order, under what conditions. Or you can give the model some degree of control over its own next action, letting it reason about what to do based on intermediate results. The first path leads to AI workflows. The second leads to agents. Neither is inherently superior; they optimize for different things.

What makes this genuinely hard is that the trade-offs are not always obvious up front. A workflow feels safe until you encounter an edge case the pipeline didn't anticipate. An agent feels flexible until it starts making unexpected decisions with real side effects. Teams that don't reason about this explicitly tend to either over-engineer with agents when workflows would have worked, or under-engineer with rigid pipelines that break in production.

AI Workflows: Deterministic Pipelines with LLM Steps

An AI workflow is a structured sequence of operations where the control flow - the order of steps, the branching logic, the error handling - is defined by the engineer, not the model. LLM calls are components in the pipeline, not orchestrators of it. The model produces output; the surrounding code decides what to do with that output.

This is the pattern you reach for when the problem is well-understood, the variance in inputs is manageable, and you need predictable, auditable behavior. A document summarization pipeline, a content moderation system, a code review assistant that always runs the same analysis steps - these are workflow problems. The LLM adds intelligence at specific nodes; the workflow provides structure around it.

A common implementation pattern in TypeScript might look like this:

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

interface WorkflowContext {
  rawDocument: string;
  extractedEntities?: string[];
  summary?: string;
  classification?: string;
}

async function extractEntities(ctx: WorkflowContext): Promise<WorkflowContext> {
  const response = await client.messages.create({
    model: "claude-opus-4-5",
    max_tokens: 512,
    messages: [
      {
        role: "user",
        content: `Extract named entities (people, organizations, locations) from this text as a JSON array of strings. Return only the JSON array, no other text.\n\n${ctx.rawDocument}`,
      },
    ],
  });

  const text =
    response.content[0].type === "text" ? response.content[0].text : "[]";
  return { ...ctx, extractedEntities: JSON.parse(text) };
}

async function summarize(ctx: WorkflowContext): Promise<WorkflowContext> {
  const entityContext =
    ctx.extractedEntities?.length
      ? `Key entities: ${ctx.extractedEntities.join(", ")}.`
      : "";

  const response = await client.messages.create({
    model: "claude-opus-4-5",
    max_tokens: 256,
    messages: [
      {
        role: "user",
        content: `Summarize the following document in 2-3 sentences. ${entityContext}\n\n${ctx.rawDocument}`,
      },
    ],
  });

  const summary =
    response.content[0].type === "text" ? response.content[0].text : "";
  return { ...ctx, summary };
}

async function classify(ctx: WorkflowContext): Promise<WorkflowContext> {
  const response = await client.messages.create({
    model: "claude-opus-4-5",
    max_tokens: 64,
    messages: [
      {
        role: "user",
        content: `Classify this document as one of: legal, financial, technical, general.\nSummary: ${ctx.summary}\nReturn only the single classification word.`,
      },
    ],
  });

  const classification =
    response.content[0].type === "text"
      ? response.content[0].text.trim().toLowerCase()
      : "general";
  return { ...ctx, classification };
}

async function runDocumentWorkflow(document: string): Promise<WorkflowContext> {
  let ctx: WorkflowContext = { rawDocument: document };
  ctx = await extractEntities(ctx);
  ctx = await summarize(ctx);
  ctx = await classify(ctx);
  return ctx;
}

This pattern has several important properties. Every step is a pure function over context - easy to test, easy to replace. The orchestration logic lives entirely outside the model. If classify starts misbehaving, you know exactly where to look. You can add logging, caching, retries, and circuit breakers at each step without touching the model prompts.

The key limitation is that the branching logic is static. If a document is so short that entity extraction is meaningless, the step still runs. If the classification needs additional context from a database, you need to add that as a step explicitly. The pipeline does what you designed it to do - no more, no less. That constraint is both its strength and its ceiling.

AI Agents: Model-Driven Control Flow

An AI agent inverts the control structure. Instead of the engineer specifying the sequence of operations, the model decides what action to take next based on its current state, available tools, and goal. The agent loop typically looks like: observe current state -> reason about what to do -> take action -> observe result -> repeat until done.

This is not a new concept - it has roots in classical AI planning and reinforcement learning - but LLMs have made it practical to implement with natural language as the reasoning medium. The model doesn't follow a script; it interprets a goal and generates a plan dynamically. Tools (functions the model can invoke) provide the mechanism for acting on external systems.

The canonical implementation uses the tool-use capability built into modern LLM APIs. The model receives a system prompt describing its goal, a list of available tools with their schemas, and a conversation history. It responds either with a tool call (I need to do X) or a final answer (I'm done). The surrounding agent loop executes tool calls, feeds results back into the conversation, and continues until the model signals completion.

Here is a minimal but realistic agent loop in Python using Anthropic's API:

import anthropic
import json
from typing import Any

client = anthropic.Anthropic()

# Tool implementations
def search_documentation(query: str) -> str:
    # Placeholder: would call a real search index
    return f"[Search results for '{query}': Found 3 relevant sections on error handling patterns]"

def read_file(path: str) -> str:
    try:
        with open(path, "r") as f:
            return f.read()
    except FileNotFoundError:
        return f"Error: File '{path}' not found."

def write_file(path: str, content: str) -> str:
    with open(path, "w") as f:
        f.write(content)
    return f"Successfully wrote {len(content)} characters to {path}"

TOOLS = [
    {
        "name": "search_documentation",
        "description": "Search internal documentation for relevant information",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "The search query"}
            },
            "required": ["query"],
        },
    },
    {
        "name": "read_file",
        "description": "Read a file from the local filesystem",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "Path to the file"}
            },
            "required": ["path"],
        },
    },
    {
        "name": "write_file",
        "description": "Write content to a file",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string"},
                "content": {"type": "string"},
            },
            "required": ["path", "content"],
        },
    },
]

TOOL_REGISTRY: dict[str, Any] = {
    "search_documentation": search_documentation,
    "read_file": read_file,
    "write_file": write_file,
}

def run_agent(goal: str, max_iterations: int = 10) -> str:
    messages = [{"role": "user", "content": goal}]

    for iteration in range(max_iterations):
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=2048,
            tools=TOOLS,
            system="You are a coding assistant with access to the local filesystem and documentation. Work step by step to accomplish the user's goal.",
            messages=messages,
        )

        # Append assistant response to history
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason == "end_turn":
            # Model has finished - extract final text
            for block in response.content:
                if hasattr(block, "text"):
                    return block.text
            return "Task completed."

        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    fn = TOOL_REGISTRY.get(block.name)
                    if fn:
                        result = fn(**block.input)
                    else:
                        result = f"Error: unknown tool '{block.name}'"
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result,
                    })

            messages.append({"role": "user", "content": tool_results})

    return "Max iterations reached without completion."

The critical difference from a workflow is that nothing in this code determines whether the agent should search documentation before reading a file, or vice versa. The model reasons about that sequence dynamically based on the goal. Add more tools and the agent can compose increasingly complex plans - but it also has more ways to go wrong.

Agents are powerful precisely because they can handle problems where the path from start to finish is not fully specifiable in advance. A developer assistant that needs to understand a codebase before suggesting changes, a research agent that follows citation chains, a support agent that adapts its troubleshooting steps based on system state - these are agent problems. The task structure is too variable to hardcode.

Agentic AI: Composing Agents and Workflows at Scale

"Agentic AI" is a broader term describing systems where AI models operate with enough autonomy to pursue goals across multiple steps, potentially orchestrating other models or tools, with limited human intervention per step. It encompasses agents as described above, but also multi-agent architectures, human-in-the-loop systems, and hybrid compositions of workflows and agents.

In practice, production agentic systems rarely use a single flat agent loop. More common patterns include orchestrator-subagent architectures, where a high-level model decomposes a goal and delegates to specialized agents or workflows; parallelized workflows with agent decision nodes; and systems with approval checkpoints that pause for human review before consequential actions.

Consider a software engineering assistant that handles a bug report end-to-end. At the top level, an orchestrator agent receives the report and decides which specialized capability to invoke: a code search agent to locate relevant files, a workflow pipeline to run tests and collect output, another agent to draft a fix, a workflow to validate the fix against a test suite. The orchestrator doesn't know which steps will be needed before it starts - that's why it's an agent - but each sub-component may be a deterministic workflow because its own sub-task is well-defined.

// Simplified orchestrator pattern
interface SubAgentResult {
  success: boolean;
  output: string;
  agentName: string;
}

type SubAgent = (input: string) => Promise<SubAgentResult>;

async function orchestratorAgent(
  bugReport: string,
  agents: Record<string, SubAgent>
): Promise<string> {
  const client = new Anthropic();

  // First pass: have the orchestrator decompose the problem
  const planResponse = await client.messages.create({
    model: "claude-opus-4-5",
    max_tokens: 512,
    messages: [
      {
        role: "user",
        content: `Given this bug report, identify which agents to invoke in order and with what input. 
Available agents: ${Object.keys(agents).join(", ")}.
Return JSON: { "steps": [{ "agent": string, "input": string }] }
Bug report: ${bugReport}`,
      },
    ],
  });

  const planText =
    planResponse.content[0].type === "text"
      ? planResponse.content[0].text
      : "{}";
  const plan = JSON.parse(planText.replace(/```json|```/g, "").trim());

  // Execute each delegated step
  const results: SubAgentResult[] = [];
  for (const step of plan.steps) {
    const agent = agents[step.agent];
    if (agent) {
      const result = await agent(step.input);
      results.push(result);
    }
  }

  // Synthesize final response
  const synthesisResponse = await client.messages.create({
    model: "claude-opus-4-5",
    max_tokens: 1024,
    messages: [
      {
        role: "user",
        content: `Synthesize a developer-facing response to the bug report based on these sub-agent results:\n${JSON.stringify(results, null, 2)}\n\nBug report: ${bugReport}`,
      },
    ],
  });

  return synthesisResponse.content[0].type === "text"
    ? synthesisResponse.content[0].text
    : "Unable to synthesize response.";
}

This pattern - orchestrator model planning over sub-agents and workflows - is what most production "agentic" systems look like beneath the marketing. The key engineering challenges shift from "how do I make the agent smarter" to "how do I decompose the problem such that each sub-component has a well-bounded responsibility and failure mode."

Agentic AI also introduces genuine questions about trust and authorization that don't apply to simpler patterns. If an agent can write to a database, send emails, or execute code, the question of which agent is permitted to do what - and under what conditions - becomes a first-class engineering concern, not an afterthought.

Trade-offs and Failure Modes

The central trade-off between workflows and agents is control versus flexibility. Workflows give you deterministic behavior, straightforward observability, and predictable cost per execution. Agents give you dynamic problem-solving at the cost of non-determinism, harder observability, and variable (often higher) cost.

Workflows fail in predictable ways: edge cases the engineer didn't anticipate, brittle string parsing between steps, prompt regressions when a model version changes. These failures are usually diagnosable because the control flow is visible. Agents fail in less predictable ways: the model makes a plausible-seeming but incorrect plan, a tool is called with wrong arguments in a way that cascades, or the agent loops without converging. These failures can be difficult to reproduce because the exact sequence of reasoning is not deterministic.

A particularly important failure mode for agents is the "confident wrong plan" problem. Because the model reasons in natural language and has been trained to be helpful and decisive, it can produce plans that look coherent but contain logical errors or incorrect assumptions about the environment. Without guardrails - budget limits on iterations, validation of tool outputs, human checkpoints for destructive actions - these plans execute to completion before anyone notices the mistake.

Cost is a non-trivial concern that teams routinely underestimate. An agent that runs 10 model calls per task costs roughly 10x more than a workflow that accomplishes the same goal with a single call. In high-volume systems, this difference is the difference between a viable product and one that's economically unsustainable. The rule of thumb: prefer workflows for tasks where the structure is known; use agents only where genuine flexibility in control flow is required.

Observability deserves its own mention. With workflows, you can log the input and output of every step, trace a request end-to-end, and replay it. With agents, capturing the full trajectory - the conversation history, every tool call, every intermediate result - requires deliberate instrumentation. Frameworks like LangSmith, Weights & Biases, and Braintrust address this, but it has to be designed in from the beginning, not bolted on after incidents.

Best Practices

Start with the simplest architecture that solves the problem. If a prompt chain with deterministic branching handles your use case, build that. The engineering cost, operational complexity, and failure surface of agent systems are meaningfully higher than workflows. Reserve agents for tasks where the path to completion genuinely cannot be specified in advance.

Define clear tool contracts and treat them as APIs. Every tool an agent can invoke should have a precise schema, deterministic behavior, and clear error semantics. A tool that sometimes returns structured data and sometimes returns an error message as a normal string will produce unpredictable agent behavior. Design tools as you would design service endpoints: explicit inputs, explicit outputs, no side-channel behavior.

Implement hard guardrails at the agent loop level, independent of the model. Maximum iteration counts, budget limits on tool calls, blocklists for destructive actions without confirmation - these should be enforced in code, not just requested in the system prompt. A system prompt saying "always ask before deleting files" is much weaker than code that intercepts delete_file calls and requires a human confirmation signal.

For agentic systems with real-world consequences, implement human-in-the-loop checkpoints at decision nodes with irreversible effects. This is not just a safety measure - it is an engineering pattern for managing uncertainty. When the agent's confidence in a plan is low (something you can infer from its reasoning output), route to a human rather than proceeding. This degrades gracefully instead of failing silently.

Instrument everything. Log the full conversation history for every agent run. Track which tools were called, with what arguments, and what they returned. Measure per-run token usage and latency. Store enough context to replay a run and understand why the agent made the decisions it did. Production debugging of agent systems without this instrumentation is nearly impossible.

When composing workflows and agents, be explicit about where the boundary is. Document which components are deterministic and which are model-driven. This is especially important in multi-team codebases where the person debugging a failure may not have written the component that's misbehaving. A component that looks like a function but makes LLM calls internally is a hidden source of non-determinism that should be clearly marked.

Analogies and Mental Models

One useful mental model is the contrast between a manufacturing assembly line and a skilled contractor. An AI workflow is the assembly line: every station does a defined operation, the product moves along a predetermined path, and quality control happens at specific checkpoints. It is efficient, predictable, and scales well - as long as the product fits the line.

An AI agent is the skilled contractor: given a goal and a set of tools, they figure out the approach, adapt to what they find, and make judgment calls along the way. This handles novel situations the assembly line can't, but you need to trust the contractor's judgment, and their work is harder to audit step by step.

Agentic AI is more like a general contractor managing subcontractors: some subcontractors run their own assembly lines (workflows), some are other skilled contractors (sub-agents), and the general contractor decides who does what. The system's intelligence is distributed across the hierarchy. The challenge is coordination, trust boundaries, and failure isolation - the same challenges in any complex organizational structure, just implemented in software.

The 80/20 Insight

Most of the value in LLM-based systems comes from a small set of architectural decisions, not from prompt optimization or model selection. The 20% of decisions that produce 80% of the outcomes: 1. Choosing workflow vs. agent correctly is the most consequential single decision. Getting this wrong - using an agent where a workflow would work, or a workflow where an agent is required - produces systems that are either fragile or unnecessarily complex. 2. Scoping tools narrowly determines the blast radius when things go wrong. An agent with 20 tools including several that write to production databases is a different risk profile than one with 5 read-only tools. Start narrow; expand deliberately. 3. Building observability in from day one determines whether you can operate the system in production. The teams that succeed with agentic systems treat logging and tracing as non-negotiable requirements, not nice-to-haves. 4. Treating prompts as code - versioning them, testing them against a suite of cases, reviewing changes - prevents the quiet regressions that are the most common failure mode in LLM-based production systems. 5. Designing for graceful degradation by building human escalation paths for low-confidence situations. Systems that fail gracefully to a human review queue are operationally viable; systems that fail silently into bad outputs erode trust quickly.

Key Takeaways

Five things you can apply immediately:

  1. Audit your current LLM code for misclassification. If you have an agent loop doing the same steps every time, it's a workflow wearing an agent costume. Simplify it. If you have a rigid pipeline that breaks on unusual inputs, consider whether a decision node powered by a model would handle the variance better.
  2. Define tool schemas as rigorously as you would define a REST API. Types, required fields, error return values - all of it explicit. The model's behavior is only as good as its tools' contracts.
  3. Add iteration and token budget limits to every agent loop today. If you have agent code in production without these, you have an unbounded cost vector and a missing safety net.
  4. Instrument a single agent run end-to-end and capture the full trace to a log store. If you can't replay a run and understand why the agent made each decision, you cannot debug production incidents reliably.
  5. Document the control flow boundary in every system that mixes workflows and agents. A comment or README section that maps out which components are deterministic and which are model-driven costs almost nothing and saves hours of debugging later.

Conclusion

The distinction between AI workflows, AI agents, and agentic AI is not semantic - it is architectural. Workflows give you control and predictability at the cost of flexibility. Agents give you flexibility at the cost of predictability and operational complexity. Agentic systems compose both, which introduces all the challenges of distributed system design alongside the non-determinism of LLM reasoning.

Neither pattern is inherently better. The best systems are built by engineers who understand the trade-offs clearly enough to choose the right pattern for each component, and to compose them in ways that isolate failures and preserve observability. That understanding starts with precise definitions - which this article has tried to provide - but it deepens with production experience.

As LLMs become more capable and tool ecosystems mature, the pressure toward more agentic architectures will increase. The teams that navigate this well will be those that resist the gravitational pull of complexity for its own sake, that invest in observability and guardrails as first-class engineering concerns, and that stay focused on what the system needs to accomplish rather than what the most sophisticated pattern would look like.

Build the simplest thing that works. Instrument it thoroughly. Expand autonomy deliberately, with appropriate guardrails, and only where the problem demands it.

References