
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.
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.
| Observed task property | Default direction | Reason |
|---|---|---|
| Known path, stable rules, constrained contracts | Deterministic workflow | Code can express and test the policy directly |
| Variable decomposition and runtime tool choice | Bounded agent | The useful decision cannot be enumerated completely in advance |
| Ambiguous interpretation but controlled execution | Hybrid | Reasoning 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.
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.
| Dimension | Workflow pressure | Agentic pressure | Hybrid signal |
|---|---|---|---|
| Path variability | Low; branches can be enumerated | High; decomposition varies at runtime | Only one stage has variable paths |
| Input ambiguity | Low or safely rejected | Interpretation is central to the outcome | Interpret, then return to fixed control |
| Rule stability | Stable and directly testable | Rules cannot enumerate every valid case | Stable policy surrounds variable judgment |
| Tool-selection variability | Known tool per branch | Relevant allowlisted tool depends on context | Model selects from a narrow subset |
| Side-effect risk | High risk favors deterministic authority | Dynamic proposals may still be useful | Proposal is agentic; authorization is not |
| Auditability and reproducibility | Exact replay is a core requirement | Trajectory variance is acceptable and evaluated | Deterministic events frame model decisions |
| Latency and cost sensitivity | Predictable calls are important | Extra decisions are justified by task value | Reasoning is restricted to uncertain cases |
| State complexity | Known finite states | Variable loops and evidence accumulation | Explicit 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.
| Case group | Architecture tension | Safe terminal options |
|---|---|---|
| Routine report and policy lookup | Stable mapping makes model choice unnecessary | Complete with a known read operation |
| Deletion and vague cleanup | Semantic intent may vary but authority is high risk | Policy block or review; never execute in fixture |
| Access review | Evidence supports a bounded planning decision | Create a non-mutating plan |
| Mixed, unknown, or missing-evidence request | No architecture should invent authorization or evidence | Review 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.
| Architecture | What the fixture demonstrates | What it does not prove |
|---|---|---|
| Deterministic workflow | Stable cases complete with no dynamic decisions; ambiguity routes to review | That workflows are always cheaper, faster, or more reliable |
| Agentic | Recorded decisions can resolve a variable plan and can also fail validation | Any provider's reasoning quality, accuracy, latency, or tool reliability |
| Hybrid | Known rules can reduce the fixture's dynamic-decision surface | That 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.
examples/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."
}
}
]
}examples/compare_architectures.pyfrom __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=27Failure 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 class | Primary owner | First evidence to inspect |
|---|---|---|
| Invalid or incomplete input | Input validation | Rejected fields and contract version |
| Wrong fixed branch | Workflow logic | Rule match and test fixture |
| Invalid dynamic proposal | Model/decision boundary | Structured decision and validator result |
| Unsupported conclusion | Context/evidence quality | Retrieved evidence IDs and freshness |
| Tool call fails | Tool integration | Normalized arguments, timeout, and response class |
| Action forbidden | Policy and authorization | Principal, resource, effect, approval, and policy version |
| Duplicate or stale execution | Persistence/state | Idempotency key and expected state version |
| Misleading comparison | Evaluation design | Fixture 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.
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.
- Architecture implementation: build a bounded Python agent after the architecture decision is justified.
- Evaluation practice: separate prompt failures from schema, context, tool, and policy failures.
- Career sequence: build workflow and integration foundations before adding model uncertainty.
- Python foundation: strengthen validation, testing, and failure handling around AI components.
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.
- Microsoft Agent Framework — Workflows
Reviewed for its first-party description of the spectrum between model-directed agents, workflows with agent executors, and deterministic pipelines.
- LangGraph documentation — Workflows and agents
Reviewed for the distinction between predetermined workflow paths and dynamic agent tool-selection loops.
- OpenAI API documentation — Function calling
Reviewed for application-owned function execution, strict schemas, narrow tools, and moving known values or fixed sequences into code.
- OpenAI API documentation — Safety in building agents
Reviewed for structured data boundaries, tool approvals, guardrails, and trace evaluation around agent workflows.
- AWS Well-Architected Framework — Make mutating operations idempotent
Reviewed for idempotency tokens and downstream responsibility when retries or duplicate delivery can repeat side effects.
- OpenTelemetry — Signals
Reviewed for the roles of traces, metrics, and logs in observing request paths and runtime behavior.
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.