Three connected architecture lanes representing a fixed workflow, an adaptive AI agent, and a controlled hybrid system with validation and approval
Automation

AI Agents vs Workflow Automation: Choosing the Right Architecture

Choose between deterministic workflow automation, a bounded AI agent, and a hybrid architecture using explicit decision gates and one executable synthetic comparison.

Aug 31, 202622 min readMuhammad FarooqLast reviewed: Aug 31, 2026

Architecture selection should happen before framework selection. A fixed workflow, a bounded agent, and a hybrid system can all use models or tools, but they assign decision authority differently. This guide applies one principle throughout: use the least agentic architecture that can reliably satisfy the task. It compares the same eight synthetic internal-operations requests through three deterministic simulations so the differences are inspectable. The recorded decisions are educational data, not live model outputs, a provider benchmark, production traffic, client evidence, or proof that one architecture is universally superior.

The architecture decision comes before the framework

Start with task uncertainty, authority, and failure cost rather than an agent label.

An architecture is a distribution of responsibility. It decides which paths are fixed in code, which judgments may vary at runtime, where state lives, how tools are selected, who authorizes side effects, and how failures become observable. A framework can implement those choices, but it cannot make the product decision for you.

A useful first question is not whether a model can perform the task. Ask whether normal code can fully express the required policy. If inputs are constrained, rules are stable, steps are known, and failures need direct diagnosis, a deterministic workflow is usually the stronger baseline. Adding a model to one transformation does not automatically make the system an agent.

Agentic behavior becomes justified when the execution path cannot be completely predetermined: evidence is incomplete or ambiguous, decomposition varies by case, or runtime context determines which allowlisted capability is relevant. Even then, dynamic decision generation and authority to act should remain separate concerns.

Hybrid architecture is a first-class design, not a reluctant compromise. A deterministic shell can validate input and state, invoke a bounded model only at an uncertainty bottleneck, validate the proposal, apply policy and approval, execute a known operation, and record the transition. That pattern keeps useful interpretation without transferring the whole workflow to a model.

The first architecture gate
Observed task propertyDefault directionReason
Known path, stable rules, constrained contractsDeterministic workflowCode can express and test the policy directly
Variable decomposition and runtime tool choiceBounded agentThe useful decision cannot be enumerated completely in advance
Ambiguous interpretation but controlled executionHybridReasoning is useful at one boundary while authority remains deterministic

What counts as workflow automation

A workflow has an explicit path even when one step uses an LLM.

Deterministic workflow automation encodes sequence and branching in application logic. The system knows the permitted states, which rule selects each branch, which tool belongs to that branch, and which terminal outcomes exist. External APIs can still fail and distributed systems can still be nondeterministic, but the intended control flow is explicit.

A workflow can include a model-powered extraction, classification, or drafting step without becoming agentic. If code always calls the same model operation at the same stage, validates the same contract, and then follows fixed rules, the model is a component inside a workflow. The defining question is who chooses the path, not whether an LLM appears anywhere in the diagram.

Workflow automation is often preferable for simple CRUD, stable ETL, scheduled jobs, fixed API orchestration, deterministic validation and transformation, and high-risk actions where semantic reasoning adds little value. It supports reproducible tests, predictable call counts, straightforward retry policy, and failure ownership that maps cleanly to named steps.

  • Inputs and outputs have versioned contracts.
  • Branches are enumerated in code and reviewed like other business logic.
  • Tools are selected by the workflow rather than generated at runtime.
  • Retries, timeouts, idempotency, and state transitions are attached to known operations.
  • Unsupported or ambiguous cases terminate in an explicit review state.

What makes a system agentic

Agentic systems delegate bounded path selection, not unrestricted authority.

An agent receives a goal and context, proposes the next action, may select among allowlisted tools, observes results, and decides whether to continue, stop, or request review. The path can vary between otherwise similar cases. That runtime choice is what makes the architecture agentic.

The model proposal is still data. The application owns the available tool registry, exact argument schemas, step budget, evidence boundary, state transitions, policy checks, approvals, and execution. OpenAI's function-calling documentation likewise places function execution in application code and recommends using code for values or sequences the application already knows.

Dynamic choice has a real operational price. There are more decision events to validate, more possible trajectories to test, more chances for invalid tool selection, and potentially more model or tool calls. That does not make agents bad. It means the flexibility must solve a measured problem that fixed code cannot solve cleanly.

Common Mistakes

  • Calling any workflow with one model request an agent.
  • Treating a tool proposal as authorization to execute it.
  • Letting model text construct arbitrary commands, URLs, queries, or module names.
  • Using hidden reasoning text as the only audit record.
  • Adding an open-ended loop without a step limit or terminal review state.

The hybrid architecture

Keep the shell deterministic and spend reasoning only where uncertainty exists.

A practical hybrid looks like: deterministic input validation → known-rule attempt → bounded model decision only for unresolved ambiguity → exact schema validation → deterministic policy and approval → allowlisted execution → deterministic state transition. The model receives less authority and the system makes fewer variable decisions than a general agent loop.

This boundary also gives teams an incremental migration path. Start with a workflow, instrument review reasons, and find the repeated uncertainty bottleneck. Insert a model only there. If the model later makes the same predictable decision repeatedly, move that rule back into code. Architecture can evolve in both directions.

Microsoft's current Agent Framework guidance describes a spectrum between a model choosing every step and a fully deterministic pipeline, with workflows containing agent executors between those ends. LangGraph similarly distinguishes predetermined workflow paths from dynamic agent processes. Those are useful vendor descriptions, not universal definitions; the engineering boundary here is who owns path selection and execution authority.

Three-lane comparison of deterministic workflow automation, a bounded agent with dynamic allowlisted tool selection, and a hybrid architecture with deterministic preprocessing, bounded model judgment, policy and approval, known execution, and controlled outcomes
The same task can place path selection in code, in a bounded model loop, or at one controlled uncertainty boundary. In every lane, executable authority remains an application concern.

Start with the task, not the buzzword

Evaluate several dimensions instead of collapsing architecture into one score.

The decision matrix below is a gate-based engineering aid. It is not a benchmark and does not produce a universal score. Two dimensions can point in different directions: high ambiguity may justify model interpretation while high side-effect risk argues for deterministic authorization. That combination usually points toward hybrid rather than maximum autonomy.

Document each answer with examples from representative requests. If path variability is described as high, show cases that genuinely require different decompositions. If evidence uncertainty is low because every input maps to a stable database key, a free-form search agent may be unnecessary.

Treat human review as an outcome with a cost and purpose, not an architectural failure. Review is appropriate when evidence is missing, a proposal is outside policy, approval is absent, state has changed, or the evaluator cannot establish a safe result.

Architecture-selection dimensions and the pressure they create
DimensionWorkflow pressureAgentic pressureHybrid signal
Path variabilityLow; branches can be enumeratedHigh; decomposition varies at runtimeOnly one stage has variable paths
Input ambiguityLow or safely rejectedInterpretation is central to the outcomeInterpret, then return to fixed control
Rule stabilityStable and directly testableRules cannot enumerate every valid caseStable policy surrounds variable judgment
Tool-selection variabilityKnown tool per branchRelevant allowlisted tool depends on contextModel selects from a narrow subset
Side-effect riskHigh risk favors deterministic authorityDynamic proposals may still be usefulProposal is agentic; authorization is not
Auditability and reproducibilityExact replay is a core requirementTrajectory variance is acceptable and evaluatedDeterministic events frame model decisions
Latency and cost sensitivityPredictable calls are importantExtra decisions are justified by task valueReasoning is restricted to uncertain cases
State complexityKnown finite statesVariable loops and evidence accumulationExplicit state machine with bounded decision nodes

Worked synthetic example: internal operations request processing

One fixture exposes different strengths without pretending to be a model benchmark.

The example processes eight synthetic internal-operations requests. Routine inventory reports and policy lookups have stable mappings. Data deletion is high risk and lacks approval. Other requests contain ambiguous cleanup language, a mixed export-and-delete intent, missing evidence, or a recorded proposal for an unknown tool.

Every case has structured task properties plus one recorded decision used by the agentic and hybrid simulations. No model is called. Recorded decisions make validation, policy blocks, dynamic-decision counts, and review routing reproducible with Python 3.12 and the standard library.

The fixture is deliberately small and authored to illustrate architecture behavior. Its counts do not measure provider intelligence, model accuracy, production throughput, reliability, latency, cost, ROI, or client outcomes. It has no real credentials, external services, personal data, or executable side effect.

The same eight requests enter every architecture
Case groupArchitecture tensionSafe terminal options
Routine report and policy lookupStable mapping makes model choice unnecessaryComplete with a known read operation
Deletion and vague cleanupSemantic intent may vary but authority is high riskPolicy block or review; never execute in fixture
Access reviewEvidence supports a bounded planning decisionCreate a non-mutating plan
Mixed, unknown, or missing-evidence requestNo architecture should invent authorization or evidenceReview or reject invalid decision

Deterministic implementation

Known requests map to known tools; everything else becomes an explicit review state.

The deterministic runner validates evidence, requires low ambiguity, and looks up the request kind in a fixed route table. It completes the two routine read cases. The known deletion route reaches deterministic policy and is blocked because approval is false. Ambiguous, mixed, unknown, and missing-evidence requests go to review.

This behavior is predictable and easy to diagnose. It also leaves useful work unresolved when a medium-ambiguity access-review request could be converted into a safe plan. That is not proof that agents are better; it identifies one decision bottleneck that a team could investigate.

The deterministic path records no dynamic decisions and never selects a tool from model output. Its limitations are visible in review reasons rather than hidden behind a generic failure.

  • Strength: explicit branches, stable call counts, and direct failure ownership.
  • Strength: no model dependency for routine cases.
  • Trade-off: novel or ambiguous requests need new rules or human review.
  • Trade-off: expanding a rule tree can become costly if valid paths genuinely vary.

Agentic implementation

Recorded decisions choose among bounded options, then deterministic code validates every proposal.

The agentic runner consumes one recorded decision for every case. A real agent could obtain that proposal from a provider, but this fixture intentionally avoids a fake LLM comparison. The simulator validates the exact decision fields, kind, tool name, action, and cited evidence before considering execution.

The access-review case completes because the recorded decision selects the allowlisted planning tool with available evidence. The unknown shell tool and the proposal citing unavailable evidence are rejected as invalid decisions. High-risk deletion proposals are policy-blocked. The mixed request chooses review.

The agentic path resolves one case the fixed workflow cannot, but it also creates eight dynamic decision events and two invalid proposals to diagnose. This is the intended trade-off: flexibility adds a validation and evaluation surface. It is not a provider result because the decisions are hand-authored records.

Tips

  • Keep tool names and arguments in strict schemas with closed enums where practical.
  • Validate cited evidence against evidence actually retrieved for the run.
  • Enforce stop, retry, and step budgets outside the model.
  • Record decision metadata and validation outcomes, not hidden chain-of-thought.

Hybrid implementation

Routine and high-risk gates stay in code; only unresolved interpretation reaches the recorded decision boundary.

The hybrid runner first checks evidence, ambiguity, stable routes, and high-risk policy. Routine cases complete without a dynamic decision. The known deletion case is blocked before any model boundary. Only four ambiguous or variable cases consume recorded decisions.

The access-review plan completes after exact validation. The mixed request routes to review, the vague cleanup proposal is policy-blocked, and the unknown tool is rejected. The missing-evidence case never reaches a model decision because deterministic preprocessing already owns that failure.

For this fixture, hybrid retains the useful completion found by the agentic path while creating fewer dynamic decision events. That count belongs only to the authored cases. A different workload could make the hybrid boundary ineffective or create more model calls than expected; representative production evaluation is still required.

  • Deterministic shell: input, evidence, stable routes, risk, policy, approval, and state.
  • Bounded decision: only cases whose interpretation cannot be expressed by the known rules.
  • Deterministic return: schema validation, allowlist, authorization, execution result, and terminal state.
  • Review path: missing evidence, invalid proposals, blocked policy, mixed intent, or unresolved ambiguity.

Compare the same cases

Counts reveal architecture behavior; they do not rank real models or production systems.

The executable summary reports completions, review routes, rejected invalid decisions, dynamic decisions, policy blocks, unauthorized side effects, and recorded steps. It does not calculate a winner or percentage score. Review and rejection are different: review is a valid controlled outcome, while rejection means a decision failed the contract.

All three architectures prevent unauthorized side effects because the simulator never executes a mutation and policy sits outside recorded decisions. The workflow completes fewer cases. The agentic runner creates the most dynamic decisions and catches two invalid proposals. The hybrid runner keeps routine routes deterministic and uses recorded decisions for four cases.

A real team should add representative cases, operational traces, failure costs, and measured model/tool behavior before selecting an architecture. Small fixture counts support code inspection, not generalization.

Executed synthetic-fixture results only — not provider or industry benchmarks
ArchitectureWhat the fixture demonstratesWhat it does not prove
Deterministic workflowStable cases complete with no dynamic decisions; ambiguity routes to reviewThat workflows are always cheaper, faster, or more reliable
AgenticRecorded decisions can resolve a variable plan and can also fail validationAny provider's reasoning quality, accuracy, latency, or tool reliability
HybridKnown rules can reduce the fixture's dynamic-decision surfaceThat hybrid is universally optimal or production-ready

Run the deterministic architecture comparison

Two embedded artifacts reproduce every reported count without a network or dependency.

Save both artifacts under the documented examples directory and run the Python file from their parent directory. The script requires Python 3.12 or newer and only the standard library. It loads the synthetic fixture, runs all three simulations, asserts expected case outcomes and safety invariants, and writes deterministic UTF-8 stdout with LF separators and no trailing newline.

The agentic labels describe architecture mechanics around recorded decisions. They do not imply that a live LLM ran. The evaluator fails if a result count changes, an invalid proposal passes validation, a high-risk request bypasses policy, or any unauthorized side effect is recorded.

Eight synthetic internal-operations requests and recorded educational decisionsexamples/architecture_fixture.json
{
  "disclaimer": "SYNTHETIC EDUCATIONAL DATA — not client data or a model benchmark",
  "version": "1.0",
  "allowlisted_tools": [
    "catalog_lookup",
    "policy_lookup",
    "plan_change",
    "delete_record"
  ],
  "cases": [
    {
      "id": "ops-01",
      "description": "Prepare the current inventory summary.",
      "kind": "inventory_report",
      "ambiguity": "low",
      "risk": "low",
      "evidence_available": ["catalog-v1"],
      "requested_action": "read",
      "approval": false,
      "recorded_decision": {
        "kind": "call_tool",
        "tool": "catalog_lookup",
        "action": "read",
        "evidence": ["catalog-v1"],
        "reason": "The request maps to the inventory catalog."
      }
    },
    {
      "id": "ops-02",
      "description": "Return the retention policy for monthly reports.",
      "kind": "policy_question",
      "ambiguity": "low",
      "risk": "low",
      "evidence_available": ["retention-policy-v3"],
      "requested_action": "read",
      "approval": false,
      "recorded_decision": {
        "kind": "call_tool",
        "tool": "policy_lookup",
        "action": "read",
        "evidence": ["retention-policy-v3"],
        "reason": "A controlled policy lookup answers the request."
      }
    },
    {
      "id": "ops-03",
      "description": "Delete the expired snapshot referenced by the request.",
      "kind": "data_deletion",
      "ambiguity": "low",
      "risk": "high",
      "evidence_available": ["retention-policy-v3", "snapshot-17"],
      "requested_action": "delete",
      "approval": false,
      "recorded_decision": {
        "kind": "call_tool",
        "tool": "delete_record",
        "action": "delete",
        "evidence": ["retention-policy-v3", "snapshot-17"],
        "reason": "The snapshot is identified, but execution needs approval."
      }
    },
    {
      "id": "ops-04",
      "description": "Clean up old workspace data that is no longer useful.",
      "kind": "workspace_cleanup",
      "ambiguity": "high",
      "risk": "high",
      "evidence_available": ["retention-policy-v3"],
      "requested_action": "delete",
      "approval": false,
      "recorded_decision": {
        "kind": "call_tool",
        "tool": "delete_record",
        "action": "delete",
        "evidence": ["retention-policy-v3"],
        "reason": "The request suggests deletion but does not identify a safe target."
      }
    },
    {
      "id": "ops-05",
      "description": "Prepare an access-review plan for the analytics workspace.",
      "kind": "access_review",
      "ambiguity": "medium",
      "risk": "medium",
      "evidence_available": ["access-policy-v2", "workspace-analytics"],
      "requested_action": "create_plan",
      "approval": false,
      "recorded_decision": {
        "kind": "call_tool",
        "tool": "plan_change",
        "action": "create_plan",
        "evidence": ["access-policy-v2", "workspace-analytics"],
        "reason": "A non-mutating plan can be prepared from the available evidence."
      }
    },
    {
      "id": "ops-06",
      "description": "Export the archive and delete its source records.",
      "kind": "mixed_request",
      "ambiguity": "high",
      "risk": "high",
      "evidence_available": ["export-policy-v1", "retention-policy-v3"],
      "requested_action": "mixed",
      "approval": false,
      "recorded_decision": {
        "kind": "review",
        "tool": null,
        "action": null,
        "evidence": ["export-policy-v1", "retention-policy-v3"],
        "reason": "The request combines a reversible export with irreversible deletion."
      }
    },
    {
      "id": "ops-07",
      "description": "Run a custom cleanup command for the legacy workspace.",
      "kind": "novel_request",
      "ambiguity": "high",
      "risk": "medium",
      "evidence_available": ["legacy-workspace-note"],
      "requested_action": "custom",
      "approval": false,
      "recorded_decision": {
        "kind": "call_tool",
        "tool": "shell_exec",
        "action": "read",
        "evidence": ["legacy-workspace-note"],
        "reason": "The recorded proposal selects a tool outside the allowlist."
      }
    },
    {
      "id": "ops-08",
      "description": "Prepare an inventory summary from the missing catalog snapshot.",
      "kind": "inventory_report",
      "ambiguity": "low",
      "risk": "low",
      "evidence_available": [],
      "requested_action": "read",
      "approval": false,
      "recorded_decision": {
        "kind": "call_tool",
        "tool": "catalog_lookup",
        "action": "read",
        "evidence": ["catalog-v1"],
        "reason": "The proposal cites catalog evidence that is not available."
      }
    }
  ]
}
Standard-library simulator and assertions for all three architecture lanesexamples/compare_architectures.py
from __future__ import annotations

import json
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable


FIXTURE_PATH = Path(__file__).with_name("architecture_fixture.json")
DECISION_FIELDS = {"kind", "tool", "action", "evidence", "reason"}
ALLOWED_KINDS = {"call_tool", "review"}
ALLOWED_ACTIONS = {"read", "create_plan", "delete"}
FIXED_ROUTES = {
    "inventory_report": ("catalog_lookup", "read"),
    "policy_question": ("policy_lookup", "read"),
    "data_deletion": ("delete_record", "delete"),
}


@dataclass(frozen=True)
class Result:
    case_id: str
    status: str
    reason: str
    dynamic_decisions: int
    policy_blocks: int
    invalid_decisions: int
    unauthorized_side_effects: int
    steps: int


def load_fixture() -> dict[str, Any]:
    fixture = json.loads(FIXTURE_PATH.read_text(encoding="utf-8"))
    assert fixture["disclaimer"].startswith("SYNTHETIC EDUCATIONAL DATA")
    assert len(fixture["cases"]) == 8
    return fixture


def result(case: dict[str, Any], status: str, reason: str, *, dynamic: int = 0,
           policy: int = 0, invalid: int = 0, steps: int) -> Result:
    return Result(case["id"], status, reason, dynamic, policy, invalid, 0, steps)


def evidence_missing(case: dict[str, Any]) -> bool:
    return not case["evidence_available"]


def policy_blocked(case: dict[str, Any], action: str | None) -> bool:
    return (case["risk"] == "high" or action == "delete") and not case["approval"]


def validate_decision(case: dict[str, Any], decision: Any, allowlist: set[str]) -> str | None:
    if not isinstance(decision, dict) or set(decision) != DECISION_FIELDS:
        return "decision_schema"
    if decision["kind"] not in ALLOWED_KINDS:
        return "decision_kind"
    if not isinstance(decision["reason"], str) or not decision["reason"].strip():
        return "decision_reason"
    evidence = decision["evidence"]
    if not isinstance(evidence, list) or not all(isinstance(item, str) for item in evidence):
        return "decision_evidence_type"
    if not set(evidence).issubset(case["evidence_available"]):
        return "unavailable_evidence"
    if decision["kind"] == "review":
        if decision["tool"] is not None or decision["action"] is not None:
            return "review_shape"
        return None
    if decision["tool"] not in allowlist:
        return "unknown_tool"
    if decision["action"] not in ALLOWED_ACTIONS:
        return "unknown_action"
    return None


def run_workflow(case: dict[str, Any], _: set[str]) -> Result:
    if evidence_missing(case):
        return result(case, "review_required", "missing_evidence", steps=1)
    if case["ambiguity"] != "low":
        return result(case, "review_required", "unmapped_or_ambiguous", steps=2)
    route = FIXED_ROUTES.get(case["kind"])
    if route is None:
        return result(case, "review_required", "unmapped_or_ambiguous", steps=2)
    tool, action = route
    if policy_blocked(case, action):
        return result(case, "review_required", "approval_required", policy=1, steps=3)
    return result(case, "completed", f"fixed:{tool}", steps=3)


def run_agent(case: dict[str, Any], allowlist: set[str]) -> Result:
    decision = case["recorded_decision"]
    failure = validate_decision(case, decision, allowlist)
    if failure:
        return result(case, "rejected", failure, dynamic=1, invalid=1, steps=2)
    if decision["kind"] == "review":
        return result(case, "review_required", "recorded_review", dynamic=1, steps=3)
    if policy_blocked(case, decision["action"]):
        return result(case, "review_required", "approval_required", dynamic=1, policy=1, steps=3)
    return result(case, "completed", f"selected:{decision['tool']}", dynamic=1, steps=4)


def run_hybrid(case: dict[str, Any], allowlist: set[str]) -> Result:
    if evidence_missing(case):
        return result(case, "review_required", "missing_evidence", steps=2)
    route = FIXED_ROUTES.get(case["kind"])
    if case["ambiguity"] == "low" and route is not None:
        tool, action = route
        if policy_blocked(case, action):
            return result(case, "review_required", "approval_required", policy=1, steps=3)
        return result(case, "completed", f"fixed:{tool}", steps=3)
    decision = case["recorded_decision"]
    failure = validate_decision(case, decision, allowlist)
    if failure:
        return result(case, "rejected", failure, dynamic=1, invalid=1, steps=3)
    if decision["kind"] == "review":
        return result(case, "review_required", "recorded_review", dynamic=1, steps=4)
    if policy_blocked(case, decision["action"]):
        return result(case, "review_required", "approval_required", dynamic=1, policy=1, steps=4)
    return result(case, "completed", f"selected:{decision['tool']}", dynamic=1, steps=5)


def summarize(results: list[Result]) -> dict[str, int]:
    return {
        "completed": sum(item.status == "completed" for item in results),
        "review_required": sum(item.status == "review_required" for item in results),
        "rejected": sum(item.status == "rejected" for item in results),
        "dynamic_decisions": sum(item.dynamic_decisions for item in results),
        "policy_blocks": sum(item.policy_blocks for item in results),
        "invalid_decisions": sum(item.invalid_decisions for item in results),
        "unauthorized_side_effects": sum(item.unauthorized_side_effects for item in results),
        "steps": sum(item.steps for item in results),
    }


def execute(name: str, runner: Callable[[dict[str, Any], set[str]], Result],
            cases: list[dict[str, Any]], allowlist: set[str]) -> tuple[str, dict[str, int]]:
    results = [runner(case, allowlist) for case in cases]
    summary = summarize(results)
    assert len(results) == len(cases)
    assert summary["unauthorized_side_effects"] == 0
    fields = " ".join(f"{key}={value}" for key, value in summary.items())
    return f"{name} {fields}", summary


def main() -> None:
    fixture = load_fixture()
    cases = fixture["cases"]
    allowlist = set(fixture["allowlisted_tools"])
    workflow_line, workflow = execute("workflow", run_workflow, cases, allowlist)
    agent_line, agent = execute("agentic", run_agent, cases, allowlist)
    hybrid_line, hybrid = execute("hybrid", run_hybrid, cases, allowlist)
    assert workflow == {
        "completed": 2, "review_required": 6, "rejected": 0,
        "dynamic_decisions": 0, "policy_blocks": 1,
        "invalid_decisions": 0, "unauthorized_side_effects": 0, "steps": 18,
    }
    assert agent == {
        "completed": 3, "review_required": 3, "rejected": 2,
        "dynamic_decisions": 8, "policy_blocks": 2,
        "invalid_decisions": 2, "unauthorized_side_effects": 0, "steps": 25,
    }
    assert hybrid == {
        "completed": 3, "review_required": 4, "rejected": 1,
        "dynamic_decisions": 4, "policy_blocks": 2,
        "invalid_decisions": 1, "unauthorized_side_effects": 0, "steps": 27,
    }
    lines = [
        "SYNTHETIC EDUCATIONAL EXAMPLE — recorded decisions, not a model benchmark",
        f"cases={len(cases)}", workflow_line, agent_line, hybrid_line,
    ]
    sys.stdout.buffer.write("\n".join(lines).encode("utf-8"))


if __name__ == "__main__":
    main()

Expected output

SYNTHETIC EDUCATIONAL EXAMPLE — recorded decisions, not a model benchmark
cases=8
workflow completed=2 review_required=6 rejected=0 dynamic_decisions=0 policy_blocks=1 invalid_decisions=0 unauthorized_side_effects=0 steps=18
agentic completed=3 review_required=3 rejected=2 dynamic_decisions=8 policy_blocks=2 invalid_decisions=2 unauthorized_side_effects=0 steps=25
hybrid completed=3 review_required=4 rejected=1 dynamic_decisions=4 policy_blocks=2 invalid_decisions=1 unauthorized_side_effects=0 steps=27

Failure ownership

Change the layer that owns the failure rather than defaulting to prompt edits.

Failure taxonomy prevents architecture debates from becoming prompt debates. Invalid input belongs to the API or ingestion contract. A wrong fixed branch belongs to workflow logic. An unsupported tool proposal belongs to the decision boundary and allowlist. Missing or contradictory evidence belongs to context quality. A blocked deletion belongs to policy or approval, not model intelligence.

Schema validation owns missing, extra, and wrong-type fields. Tool integration owns timeouts, authentication, rate limits, and malformed responses. Persistence owns stale versions, duplicate delivery, and recovery conflicts. Evaluation design owns weak fixtures, labels, and graders. A model instruction cannot repair a broken database transaction or grant a user authority they do not possess.

This complements the prompt-engineering evaluation guide without duplicating it. Prompt changes are appropriate when evidence shows the model misunderstood a supported decision despite valid context, contract, tool behavior, and policy. They are not a universal repair mechanism.

Failure owner and first investigation
Failure classPrimary ownerFirst evidence to inspect
Invalid or incomplete inputInput validationRejected fields and contract version
Wrong fixed branchWorkflow logicRule match and test fixture
Invalid dynamic proposalModel/decision boundaryStructured decision and validator result
Unsupported conclusionContext/evidence qualityRetrieved evidence IDs and freshness
Tool call failsTool integrationNormalized arguments, timeout, and response class
Action forbiddenPolicy and authorizationPrincipal, resource, effect, approval, and policy version
Duplicate or stale executionPersistence/stateIdempotency key and expected state version
Misleading comparisonEvaluation designFixture scope, labels, grader, and disclaimer

Side effects and approval

A generated decision is not authorization to act.

The fixture's deletion tool is a named architectural boundary only. No record is deleted. Both agentic and hybrid paths can propose it, but deterministic policy checks risk and approval before any executor could run. Missing approval produces review_required and increments policy_blocks, while unauthorized_side_effects remains zero.

For a real effect, policy should evaluate authenticated identity, tenant, environment, resource, normalized arguments, risk tier, workflow version, and an approval bound to the exact operation. The executor should use an idempotency key and persist the outcome so retries do not repeat a mutation. AWS Well-Architected guidance similarly recommends idempotency tokens and state for mutating operations that may be delivered more than once.

Human review should show the original request, evidence, proposed operation, exact target, policy result, and expiry. Approving a conversation or vague intent is too broad. Denial, expiry, and state conflict should be durable events.

  • Proposal: what the decision layer recommends.
  • Authorization: whether this principal may perform this operation now.
  • Execution: a narrow, validated, idempotent integration call.
  • Audit: immutable identifiers, policy version, approval, attempt, and result.

Cost, latency, and operational trade-offs

Measure representative workloads instead of inventing prices or response times.

A deterministic workflow can avoid model calls for stable branches and usually has a more predictable maximum path. An agent loop may add decision turns, context growth, tool calls, retries, and review. A hybrid can restrict reasoning to uncertain cases, but preprocessing and multiple boundaries also add engineering complexity.

This Article reports no dollar values, token counts, or latency numbers because the fixture makes no network call. In production, capture model calls per case, input and output tokens, tool calls, elapsed time by step, retry count, review rate, queue time, infrastructure cost, and successful terminal outcomes. Compare architectures on the same representative cases and the same acceptance criteria.

Do not count lower cost as an improvement if task quality or policy compliance falls. Do not count more completions as an improvement if review was the correct safe outcome. Architecture selection is multi-dimensional: quality, safety, diagnosability, latency, cost, and operating burden all matter.

Observe decisions without storing hidden chain-of-thought

Record stable events, contracts, and outcomes that operators can act on.

A deterministic workflow needs step starts and completions, state transitions, retry attempts, external integration failures, idempotency keys, and terminal outcomes. An agent additionally needs structured decision kind, selected tool, validator result, evidence identifiers, step count, stopping reason, policy result, and review route.

A hybrid trace should make the boundary visible: which known rule ran, why the model decision node was entered, which structured proposal returned, which policy gate accepted or blocked it, and whether a side-effect executor ran. The fixture's reason strings are short authored explanations, not hidden chain-of-thought.

OpenTelemetry defines traces, metrics, and logs as complementary signals. A practical implementation can correlate them with run and decision identifiers while redacting sensitive input and tool output. Do not log secrets, raw personal data, approval tokens, or private model reasoning merely because a trace system can store them.

Minimum useful events by architecture
ArchitectureEventsOperational question answered
WorkflowStep, transition, retry, integration resultWhich known step failed and can it retry safely?
AgenticDecision, tool proposal, validation, policy, step limit, reviewWhich variable choice created the outcome?
HybridRule result, model-boundary entry, proposal, policy, executionWas reasoning necessary and did authority remain controlled?

When not to use an AI agent

Credible architecture work includes the decision to keep normal code.

Do not use an agent when the task is already a clear program. Simple CRUD, stable ETL, scheduled deterministic jobs, fixed API sequences, input normalization, schema validation, and ordinary state machines gain little from dynamic path selection. A model may add cost and variance without adding useful capability.

Avoid agentic execution for high-risk actions when reasoning adds little value. Password resets, permission grants, destructive deletion, financial transfers, deployment promotion, and legal-state changes should not become dynamic merely because a model can describe them. If a model helps interpret a request, return its proposal to deterministic identity, policy, approval, and execution layers.

Do not choose an agent to avoid writing requirements. Open-ended behavior does not repair an undefined policy. If success, available evidence, allowed tools, terminal outcomes, and review ownership cannot be stated, an agent will make the ambiguity harder to operate.

  • Normal code fully expresses the policy.
  • Every valid request follows the same known sequence.
  • The output must be exactly reproducible and directly explainable from rules.
  • Latency or call-count predictability is a hard requirement.
  • The only dynamic value is data, not the next action.
  • The action is high risk and semantic interpretation does not improve the decision.

How architectures evolve in both directions

Instrument uncertainty first, then move the boundary when evidence justifies it.

A low-risk migration starts deterministic. Record unsupported branches, review reasons, evidence gaps, and manual decisions. If one recurring ambiguity bottleneck resists stable rules, introduce a bounded structured model decision at that point. Keep input validation, policy, approval, idempotency, execution, and state transitions outside it.

Expand autonomy only after evaluations show that the new decision boundary improves the intended outcome on representative cases without unacceptable validation, review, cost, or policy regressions. Framework adoption should follow the state and control requirements, not precede them.

The reverse path matters just as much. If an agent repeatedly chooses the same tool from the same fields, encode that mapping in deterministic code and stop paying for a variable decision. If a tool is always called after another tool, combine or orchestrate them in code. Simplification is an architectural success, not a retreat.

Tips

  • Workflow → hybrid: isolate one measured uncertainty bottleneck.
  • Hybrid → agent: expand only the choices with evaluation evidence.
  • Agent → hybrid: move stable policy and repeated routes back into code.
  • Hybrid → workflow: remove the model when rules now cover the valid space.

Present architecture-selection work as portfolio evidence

Show the rejected alternatives and the limits of the evidence.

A credible portfolio case study includes the problem definition, architecture decision matrix, fixture, alternative designs, executable comparison, failure taxonomy, policy boundary, diagram, output, and limitations. The artifact should let a reviewer inspect why one stage is dynamic and why another remains deterministic.

Do not claim production deployment, client impact, scale, reliability, cost savings, latency improvement, or model superiority without corresponding evidence. Synthetic fixtures are valuable when labeled honestly: they prove that the author can define contracts, implement controls, execute an evaluator, and reason about trade-offs.

For the next implementation step, the production-agent guide shows how to build a bounded orchestrator with tools, state, retries, idempotency, observability, evaluation, and approval. The automation-engineer roadmap places those capabilities in a broader learning sequence.

Final architecture decision checklist

Make the least-agentic choice explicit and falsifiable.

Write the answers before selecting a framework. If the team cannot identify the variable decision, evidence boundary, permitted tools, policy owner, safe terminal states, and evaluation plan, the proposed agent is not yet an architecture.

Choose a deterministic workflow when rules fully express the valid space. Choose a bounded agent when variable decomposition or runtime tool selection is essential and can be evaluated. Choose hybrid when semantic judgment is useful but execution authority, policy, state, and side effects must remain tightly controlled.

  • Can the valid path be enumerated and tested in ordinary code?
  • Which exact decision cannot be predetermined, and why?
  • What evidence may the decision layer use, and how is provenance checked?
  • Are tool names, arguments, and outputs strictly validated?
  • Which actions are read-only, reversible, high risk, or prohibited?
  • Who authorizes effects, and is approval bound to one immutable operation?
  • What state, retry, timeout, idempotency, and recovery contracts exist?
  • What fixture measures completions, reviews, invalid decisions, blocks, and failures?
  • What latency, cost, call, retry, and review metrics will production capture?
  • What evidence would justify moving the boundary toward or away from an agent?

FAQ

Does using an LLM make a workflow an AI agent?

No. A deterministic workflow can call an LLM at a fixed stage and still keep sequence, branching, tools, and state transitions in code. Agentic behavior begins when the model dynamically selects the next action or tool within a bounded loop.

Is a hybrid architecture always the safest choice?

No. Hybrid adds boundaries and operating complexity. If normal code fully expresses the task, a deterministic workflow is simpler. Hybrid is useful when one or more interpretation decisions genuinely benefit from a model while policy and execution remain deterministic.

Do the embedded counts prove that hybrid systems outperform agents?

No. The eight cases and recorded decisions are synthetic educational data. The counts demonstrate how the authored simulator behaves; they do not measure a model provider, production workload, latency, cost, accuracy, or general architecture quality.

Where should human approval sit in an agent system?

Approval should sit outside the model's authority and bind to a specific normalized operation, target, policy version, and workflow state. A model may propose an action, but deterministic identity, authorization, and approval checks decide whether execution is permitted.

Can an agent architecture later become a deterministic workflow?

Yes. When traces show that the agent repeatedly makes the same decision from the same fields, move that decision into code. Architecture should become less agentic when observed behavior is stable enough to express as rules.

Sources

Primary and authoritative sources reviewed for this article.

Conclusion

Use the least agentic architecture that can reliably satisfy the task. Fixed paths belong in workflows. Truly variable decisions may justify a bounded agent. Ambiguous interpretation surrounded by controlled execution often belongs in a hybrid. The executable fixture makes those boundaries visible without pretending to benchmark a provider. When an agent is justified, continue with the production-oriented Python agent guide and preserve the same separation between decision, validation, authorization, execution, state, and evaluation.