I build AI automations for mid-sized companies for a living. Invoice routing, support triage, document extraction, lead qualification, that kind of thing. Unsexy stuff that saves real hours.
In the last twelve months, almost every technical call I've had started with the same question: "Should we use LangGraph or CrewAI for this?"
My answer is usually no. And I want to explain why, with actual code, because "agents are overhyped" hot takes without code are worthless.
What clients ask for vs. what they need
A typical request sounds like this: "We get 200 supplier emails a day. Someone reads each one, figures out if it's an invoice, a complaint, or a delivery note, and forwards it to the right department. Can an AI agent do this?"
Notice what's happening here. The word "agent" has done so much marketing work that people now use it for anything involving an LLM. But look at the actual task:
- Receive email
- Classify it
- Extract a few fields
- Route it
- Log everything
There is no planning. No dynamic tool selection. No multi-step reasoning where step 4 depends on what the model discovered in step 2. It's a pipeline. A boring, deterministic pipeline with exactly one non-deterministic component in the middle.
Giving this task to an autonomous agent loop is like hiring a consultant to staple documents. It will get done, eventually, and you'll pay for every token of "thinking" along the way.
The pattern I ship instead
Here's the architecture I've deployed at maybe a dozen companies now. n8n as the orchestrator (self-hosted, because German clients and data residency), a single LLM call for the classification, Postgres for state, and strict validation between the LLM and anything that touches production.
The core of it is one API call with a forced JSON structure:
javascript
// n8n Code node, or plain Node.js, doesn't matter
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": process.env.ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system: `You classify supplier emails for a logistics company.
Respond with JSON only, no markdown, matching exactly:
{
"category": "invoice" | "complaint" | "delivery_note" | "other",
"confidence": number between 0 and 1,
"supplier_name": string or null,
"reference_number": string or null,
"summary": string, max 200 chars
}`,
messages: [
{ role: "user", content: emailBody }
]
})
});
Nothing clever. The interesting part is what happens after the call, because this is where most tutorials stop and most production incidents start.
Validate like the model is a hostile junior dev
The LLM output goes through a schema check before it touches anything:
javascript
import { z } from "zod";
const EmailClassification = z.object({
category: z.enum(["invoice", "complaint", "delivery_note", "other"]),
confidence: z.number().min(0).max(1),
supplier_name: z.string().nullable(),
reference_number: z.string().nullable(),
summary: z.string().max(200)
});
function parseClassification(raw) {
// Models still occasionally wrap JSON in code fences,
// even when you tell them not to. Strip defensively.
const cleaned = raw.replace(/```
{% endraw %}
json|
{% raw %}
```/g, "").trim();
const parsed = EmailClassification.safeParse(JSON.parse(cleaned));
if (!parsed.success) {
throw new ValidationError(parsed.error);
}
return parsed.data;
}
And then the rule that has saved me more than any prompt engineering trick: a confidence threshold with a human fallback.
javascript
if (result.confidence < 0.85) {
await routeToHumanQueue(email, result);
return;
}
await routeToDepartment(email, result.category);
In one deployment, about 7 percent of emails land in the human queue. The client was initially disappointed by that number. Three months later they told me it's their favorite feature, because the 93 percent that flow through automatically have been correct essentially every time, and the weird edge cases (a complaint written inside a forwarded invoice, in Turkish) get human eyes instead of silent misrouting.
An autonomous agent would have handled that Turkish invoice-complaint hybrid too. Confidently. Wrongly.
The two boring things that matter more than your framework
Idempotency. Email webhooks fire twice. n8n retries on timeouts. Your workflow will process the same message multiple times unless you make it impossible:
sql
INSERT INTO processed_emails (message_id, classification, processed_at)
VALUES ($1, $2, now())
ON CONFLICT (message_id) DO NOTHING;
Check the row count. Zero means you already handled it, exit early. I've seen a duplicate webhook forward the same invoice to accounting twice, and accounting paid it twice. That bug cost more than the entire automation project.
Tracing. I log every LLM call to Langfuse: input, output, latency, token cost, and the final routing decision. Not for compliance theater. Because when the client calls and says "the system misrouted something on Tuesday," I need to see exactly what the model saw and said, in about 30 seconds. Without traces, every complaint becomes archaeology.
Neither of these problems is solved by an agent framework. Both will hurt you regardless of which one you pick.
So when do I actually reach for agents?
To be fair: they exist for a reason. I use agentic loops when the task genuinely needs runtime decisions about what to do next. Research tasks with unknown depth. Debugging workflows where the next step depends on what the last command returned. Coding assistants, obviously.
My rule of thumb after two years of client work: if you can draw the workflow as a flowchart before writing any code, build the flowchart. Use the LLM as a smart function inside it. If you can't draw the flowchart because the path depends on what the model finds along the way, that's agent territory.
Most business processes fall in the first bucket. Companies have spent decades standardizing them, that's the whole point of a process. The current funding wave (agent startups pulled in something like 1.8 billion dollars in July alone) is betting heavily on the second bucket. Fine. But don't let the investment thesis of venture capital decide your architecture.
What this looks like in production numbers
One concrete deployment, running since spring: supplier email triage for a company with around 140 employees.
- ~200 emails/day, ~4,100/month processed
- 93 percent fully automated, 7 percent to human review
- LLM cost: under 40 euros a month
- Infrastructure: one n8n instance and a Postgres database that were already there
- Time to production: 6 days, including two days of the client - arguing about what counts as a "complaint"
Those two days of arguing were the real project, by the way. The code was the easy part. It always is.
If you're building something similar and got stuck somewhere between the webhook and the validation layer, drop a comment. I read them all, and edge cases from other people's inboxes are my favorite genre of horror story.
Top comments (0)