How I design an agentic AI workflow that survives real users
Most agent demos work once. The hard part is the second week, when real users show up with inputs you never imagined.
The demo worked. It always works. I typed a clean question into the agent, it called two tools, wrote a tidy answer, and everyone on the call nodded. Then we shipped it, and on day two a real user pasted in a support ticket with three questions, a screenshot reference, and a refund demand, all crammed into one message. The agent looped four times, called the refund tool twice, and apologized to itself. That gap between the demo and day two is the whole subject of agentic AI workflow design, and it is where I spend most of my time when I embed with a team.
I build agents for a living. I drop into a company for a few weeks, ship something into production, and then stay long enough to watch it meet actual traffic. The staying is the point. Anyone can wire an LLM to a few tools and get a good screenshot. Keeping it useful once thousands of messy humans touch it is a different craft, and almost none of that craft is about the model.
Agentic AI workflow design starts with the failure, not the happy path
When I open a blank file for a new agent, I do not start with the flow that works. I start by writing down every way it can go wrong. The tool times out. The user asks two things at once. The model invents an order id that does not exist. The API returns a 200 with an empty body, which is worse than an error because nothing signals failure. I list maybe fifteen of these before I write a single line of prompt.
Why bother? Because an agent is a loop, and loops amplify mistakes. A single bad classification in a normal app shows one wrong screen and stops. A single bad classification inside an agent loop can trigger a tool call, which feeds context back into the model, which then justifies the next wrong call. Errors compound. So the design question is never how do I make it smart. It is how do I stop it from spiraling when it is confidently wrong.
Give the agent fewer tools than you think it needs
The instinct is to hand the model everything. Ten tools, twenty, a whole toolbox, and let it figure out the plan. I have tried that. It fails in a boring way. The more tools you expose, the more the model second guesses which one to use, and the longer your context grows with tool schemas it will never call on this turn. You pay for those tokens and get worse decisions in return.
I keep it tight. Most production agents I have shipped run on three to six tools. If a task seems to need more, that is usually a signal it should be two agents, or one agent plus a plain function that runs deterministically without asking the model at all. The model should decide the ambiguous parts. It should not be deciding things a switch statement can decide faster, cheaper, and without a chance of hallucinating.
State is the whole game
Here is the thing nobody tells you when you start. The prompt matters far less than the state. An agent is a small program that happens to have a language model in the middle, and like any program its behavior is defined by what it remembers between steps. If your state is just a growing pile of chat messages, you have already lost. The model drowns in its own history and starts answering questions from six turns ago.
So I keep an explicit state object and pass a narrow, curated view of it into each step. Retries live in state. The current goal lives in state. Which tools have already run, and what they returned, lives in state. The model sees a short summary, not the raw transcript. Here is roughly what a single guarded step looks like.
def run_step(state):
# Hard stop before the loop can run away
if state["retries"] >= 2:
return escalate(state, reason="tool failed twice")
result = call_tool(state["next_action"])
if result.error:
state["retries"] += 1
return state # try again on the next loop pass
state["retries"] = 0
state["history"].append(result.summary())
return advance(state)
That retries check is not clever. But it is the difference between an agent that escalates cleanly after two failures and one that burns through your OpenAI bill retrying a dead endpoint two hundred times at three in the morning. I have gotten that bill. You only get it once.
Write the system prompt like a runbook, not a personality
Most system prompts I inherit read like a character sketch. You are a friendly, helpful assistant who loves solving problems. That line does nothing. The model is already friendly. What it does not know is your rules, your limits, and what it is forbidden from doing. So I write the prompt like an operations runbook handed to a new hire on their first morning.
You are a support agent for Acme. You can read orders and issue refunds up to $50.
Rules:
1. Never invent an order id. If you cannot find the order, say so and stop.
2. Refunds above $50 require a human. Call escalate_to_human with a reason.
3. One tool call at a time. Read the result before choosing the next step.
4. If a tool returns an error twice, stop and escalate. Do not keep looping.
5. Always confirm the order id back to the user before issuing any refund.
Notice there is no personality in there. Just boundaries and stop conditions. The single most valuable line in any agent prompt is the one that tells it when to give up and hand off to a person. Models are trained to be helpful, which means they will keep trying long past the point where a sensible employee would raise their hand. You have to give them explicit permission to quit.
Put a human where being wrong is expensive
Not every step deserves the same trust. Reading an order is cheap to get wrong. Issuing a refund is not. Sending an email to a customer is not. So I draw a line through the workflow. Below it, the agent acts on its own. Above it, a human approves before anything leaves the building. This is old wisdom from payments and ops teams, and it maps cleanly onto agents.
An agent that can act without limits is not a product. It is a liability with good grammar.
The trick is making that handoff feel fast. If a human approval step takes a day, people route around it and your safety rail becomes theater. So I try to make approvals a single click inside a tool the operator already lives in, usually Slack or Zendesk, with the agent's proposed action and its reasoning sitting right there in the message. Approve, edit, or reject, and the loop continues.
Observability, or you are flying blind
You cannot improve what you cannot see, and agents are hard to see into because the interesting part is a chain of decisions, not a single request. From the first day I log every step. Not for some future audit. For debugging tonight, while the incident is still warm and the user is still annoyed.
- The full prompt actually sent to the model, after templating, not the template
- Every tool call with its arguments and the raw response it returned
- Token counts per step, so a runaway loop shows up as an obvious cost spike
- The stop reason, so I know whether it finished, escalated, or hit a limit
When something breaks in production, and it will, I want to replay the exact sequence that led there. Postgres handles this fine. You do not need a specialized platform on day one. A table of steps with a run id and a timestamp will answer ninety percent of your questions, and pgvector is already sitting right there when you need to search across past runs by similarity.
What I actually ship first
My first version into production is almost embarrassing. One narrow task. Three tools. A hard cap on loop count. A human approving anything that touches money or reaches a customer. Aggressive logging on every step. No memory across sessions yet, no clever planning, no multi agent choreography. It is boring on purpose, because boring survives contact with real users and clever usually does not.
Then I watch. The real workflow reveals itself in the logs within a week, and it is never the flow I sketched in the kickoff meeting. Users ask for things in an order I did not predict. They combine two steps into one sentence. They abandon halfway and come back an hour later expecting the agent to remember. That is the real specification, and the only way to get it is to ship the boring version and read what happens next.
I still get surprised. Last month an agent I thought was airtight got asked a question in three languages inside a single sentence and handled it better than I would have. That is the strange joy of this work. You design the guardrails carefully, you assume the worst, and then real people show you both how creative they are at breaking things and, once in a while, how much further the thing can go than you built it for. Design for the failure. Stay for the surprise.
Questions
What is the difference between an agentic workflow and a normal LLM call?
A normal call is one request and one response. An agentic workflow is a loop where the model chooses tools, reads their results, and decides the next step across several turns. The loop is what makes it powerful and also what makes it dangerous, because a single wrong decision can feed itself back in and compound. Most of the design effort goes into controlling that loop, not into the model.
How many tools should an AI agent have access to?
Fewer than you think. Most production agents I ship run on three to six tools. More tools means the model second guesses which one to use, your context fills with schemas it will not call, and decisions get worse. If a task seems to need more, split it into two agents or move the deterministic parts into plain functions that never touch the model.
Where should a human review an agent's actions?
Wherever being wrong is expensive. Reading data is cheap to get wrong, so let the agent do it alone. Anything that moves money, sends a message to a customer, or is hard to reverse should wait for a human approval. Keep that approval to a single click inside a tool the operator already uses, like Slack or Zendesk, or people will route around it.
What should I log to debug an agent in production?
Log every step: the full prompt after templating, each tool call with its arguments and raw response, token counts per step, and the stop reason. Store it with a run id and a timestamp so you can replay the exact sequence that led to a failure. Postgres is enough on day one. You do not need a specialized platform to answer most of your questions.