AI Agents and Agentic Workflows
What an agent really is, how tool calling works, and how to keep one from doing something expensive or irreversible while nobody is watching.
Chatbot vs workflow vs agent
Three different things sold under one word. The difference is who decides the next step.
- Chatbot: you ask, it answers. No actions, no state beyond the conversation.
- Workflow: your code decides the steps; the model fills in the hard parts. Predictable, testable, and the right answer most of the time.
- Agent: the model decides which tools to call and in what order, looping until it thinks the goal is met.
- Agents are more capable and much less predictable. Only reach for one when the steps genuinely cannot be known in advance.
- Most “agents” shipped in products are workflows – and that is usually the better engineering decision.
“Summarise this defect and set its severity” is a workflow: two known steps. “Investigate why the nightly suite failed” is an agent: it must read logs, decide what to look at next, maybe re-run a test. Choose the simplest thing that does the job.
Agent architecture, goals and planning
Underneath, an agent is a loop with four moves – and a limit that stops it running forever.
- Goal in, then: decide an action, perform it, observe the real result, decide again.
- The model never acts directly. It asks your code to run a tool; your code decides whether to run it.
- Always cap the loop: maximum steps, maximum tokens, maximum wall-clock time.
- Without a stop condition an agent will happily loop until the budget is gone – this is the most common production failure.
- Log every decision, tool call and observation. Without a trace, an agent bug cannot be debugged.
A support agent loops between “look up the order” and “check the refund policy” because neither answers the question. With no step cap it burned through the monthly budget in a night. Max-steps is not an optimisation, it is a control.
Tool and function calling
The mechanism behind every agent: you describe your functions, the model picks one and supplies the arguments.
- The description is a prompt. A vague one makes the model pick the wrong tool – treat it as code, and review it.
- The schema constrains the arguments, but the model can still produce nonsense that fits the schema.
- Validate every argument in your own code. A tool call is untrusted input that happens to come from a model.
- Keep tools small and single-purpose. “do_everything(action, payload)” defeats the schema entirely.
- Mark which tools are read-only and which change state – that distinction drives the approval gate.
TOOLS = [{
"name": "get_order_status",
"description": "Look up the current status of one order by its id. Read-only.",
"input_schema": {
"type": "object",
"properties": {"order_id": {"type": "string", "pattern": "^ORD-[0-9]{6}$"}},
"required": ["order_id"],
},
}]
def run_tool(name: str, args: dict):
if name != "get_order_status":
raise ValueError(f"unknown tool {name}")
order_id = args["order_id"]
if not re.fullmatch(r"ORD-\d{6}", order_id): # validate again, always
raise ValueError("bad order id")
return db.fetch_status(order_id)Think of each tool as a public API endpoint where the caller is an enthusiastic intern who read the docs once. Every bit of validation you would write for a public endpoint, you write here – because that is exactly the threat model.
State, context and short- and long-term memory
What the agent carries between steps, and what it must look up instead.
- Short-term: the current loop’s messages and observations. Grows fast – trim or summarise.
- Long-term: facts stored outside the model in a database or vector store, injected when relevant.
- Scratchpad: intermediate results written to a file or table instead of being carried in the prompt. Cheaper and inspectable.
- Never hold anything authoritative only in context – prices, balances and statuses must be re-read from the system of record.
- Make state inspectable. “What did the agent believe at step 7?” should be answerable from a log.
An agent read a balance at step 2, did six more steps, then acted on the stale number. The fix is a rule, not a prompt: anything that can change is re-read immediately before it is used. That is a test case you can write today.
Connecting APIs, databases and browser tools
Where agents become genuinely useful – and genuinely dangerous.
- APIs: the safest surface. Give the agent a narrow service account, not an admin token.
- Databases: read-only connections wherever possible. Never let a model compose raw SQL against production.
- Browser automation: powerful for testing, slow and brittle. Prefer an API when one exists.
- Files: restrict to one working directory. Treat anything downloaded as untrusted.
- Least privilege, every time: whatever the agent can reach, assume it will eventually reach by accident.
Browser-driving agents are the obvious fit for test automation, and the honest answer in 2026 is that they are slower and less reliable than Playwright for anything you run every night. They shine for exploration – “try this journey and tell me what broke” – not for regression.
Human approval and controlled execution
The single most important control in agent design.
- Classify every tool: read-only, reversible write, irreversible.
- Irreversible actions – payments, deletions, emails to customers, production deploys – require a human approval step.
- Show the human the exact action and arguments, not a summary of intent.
- Default to dry run: produce the plan, let a person approve, then execute.
- Log who approved what. For anything financial or regulated this is not optional.
IRREVERSIBLE = {"send_email", "issue_refund", "delete_record", "deploy"}
def execute(name, args, approver=None):
if name in IRREVERSIBLE and approver is None:
return {"status": "awaiting_approval", "action": name, "args": args}
result = run_tool(name, args)
audit.write(action=name, args=args, approver=approver, result=result)
return resultAn agent given a mailbox tool emailed 400 customers during a test run. Nothing was hacked, nothing was wrong with the model – there was simply no gate. Ask for the list of irreversible tools in design review; if nobody has written one, that is your finding.
Agent frameworks: LangGraph and friends
What the frameworks give you, and what they cannot give you.
- LangGraph models the agent as a graph of nodes with explicit state – which makes the flow reviewable and resumable.
- LangChain provides the building blocks: loaders, splitters, vector store adapters, prompt templates.
- Provider SDKs now include their own agent loops and are often enough on their own.
- A framework does not give you correctness, cost control or safety. Those are still your design.
- Start with a plain loop you fully understand. Move to a framework when state and branching get hard.
Framework choice rarely causes production incidents. Missing step limits, missing validation and missing approval gates cause nearly all of them. Spend the review time there.
Multi-agent collaboration and task delegation
Several specialised agents instead of one – occasionally the right answer, often an expensive way to multiply failures.
- Supervisor pattern: one agent splits the work and hands pieces to specialists.
- Pipeline pattern: each agent does one stage and passes the output on – simpler and easier to test.
- Each extra agent adds cost, latency and a new place for the handoff to lose information.
- Errors compound: three steps at 90% each is 73% end to end.
- Test each agent on its own first. If one cannot be trusted alone, three cannot be trusted together.
A “generate, review, fix” trio for test cases sounds thorough. In practice the reviewer agreed with the generator almost every time – two calls, one opinion. Giving the reviewer the requirement instead of the generator’s reasoning fixed it. Independence has to be designed in.
Model Context Protocol (MCP): clients, servers, tools and resources
A standard way to expose tools and data to a model, so the same connector works across applications.
- An MCP server exposes capabilities; an MCP client (the AI application) consumes them.
- Servers offer tools (actions), resources (readable data) and prompts (reusable templates).
- Write the connector to your Jira, test management tool or database once, and any MCP-capable client can use it.
- The same safety rules apply at the server: validate arguments, scope permissions, mark destructive operations.
- For testers this is the practical route to “ask the assistant about our test results” without pasting exports into a chat window.
A useful first MCP server for a QA team exposes two read-only tools: “last night’s failures” and “history for this test”. Read-only means the blast radius is a wrong answer, not a wrong action – the right place to start.
Workflow automation with n8n
When the steps are known, a visual workflow tool beats an agent on cost, speed and debuggability.
- n8n connects triggers (webhook, schedule, form) to actions (HTTP, database, email, Slack) with an AI node where judgement is genuinely needed.
- The flow is visible, each execution is logged, and you can replay a failed run with the same input.
- Perfect for: triage an incoming defect, summarise a nightly report, route a form to the right team.
- Keep the AI node narrow – classify or summarise – and let ordinary nodes do the moving and the branching.
- The same rules still apply: validate what comes out of the AI node before anything acts on it.
Nightly suite finishes, webhook fires, an AI node groups 40 failures into 6 likely causes, the workflow posts the summary to Slack and opens one Jira per group. No agent, no loop, and you can replay yesterday’s run exactly.
Error handling, retries, observability and cost control
The engineering that decides whether an agent survives contact with production.
- Retries with exponential backoff for 429 and 5xx – and a maximum, so a retry storm cannot form.
- Idempotency keys on anything that creates or pays, so a retry cannot double-charge.
- Timeouts at every level: the call, the step, the whole run.
- Observability: log prompt, tool calls, tokens, latency and cost per run, with a trace id that ties them together.
- Budgets: a hard per-run token cap and a daily spend alert. Stop, do not degrade silently.
import time, random
def with_retry(fn, attempts=4):
for i in range(attempts):
try:
return fn()
except RateLimited:
if i == attempts - 1:
raise
time.sleep((2 ** i) + random.random()) # backoff plus jitterThe three questions that tell you whether an agent is production-ready: what is the maximum it can spend on one run, what stops it looping, and what happens if the same request arrives twice? If any answer is missing, it is not ready.
Project: an agent that reads a task, calls approved tools and reports back
- Define three tools: one read-only lookup, one reversible write, one irreversible action.
- Write the loop with a hard step limit and a token budget. Log every decision with a trace id.
- Put the irreversible tool behind the approval gate from this module and prove it cannot run without one.
- Break it on purpose: make a tool fail, make it return nonsense, return the same request twice. Record what the agent does each time.
- Write the run report: steps taken, tools called, tokens spent, and anything the agent got wrong.