I hit one of those agent bugs that wastes half a day because nothing is fully broken.
An n8n workflow looked sharp for the first couple steps, then slowly turned into an exhausted intern. It summarized the wrong part of an email thread. It missed the actual ask in a CRM note. It answered based on stale context from a webhook payload.
My first instinct was the usual one:
- maybe GPT-5 needed a better prompt
- maybe Claude Opus needed more examples
- maybe I should route some tasks to Llama or Qwen instead
Wrong diagnosis.
The real issue was simpler: I was handing good models terrible input.
Raw Gmail chains. Full scraped HTML. Webhook payloads with 80 irrelevant keys. CRM exports where the one useful sentence was buried between timestamps, internal notes, and a copied Zoom link.
Once I cleaned the input before the LLM saw it, the agent got more reliable fast.
That has become one of my default opinions for agent workflows:
Preprocessing beats prompt tweaking more often than people want to admit.
The model is usually not confused. Your input is.
A lot of people say long-context models can handle messy input.
Sure. They can accept it.
That does not mean they use it well.
The "Lost in the Middle" result shows something most people running agents have already felt in production: if the important detail is buried in the middle of a long context, performance drops.
That maps directly to automation work.
If your support agent gets a 20-message thread and the real customer ask is wedged between a legal disclaimer and six quoted replies, don’t act surprised when it answers the wrong thing.
If your sales workflow sends raw HubSpot notes plus every custom field RevOps has ever created, don’t be surprised when the output turns into generic sludge.
This is not just an LLM problem.
It’s an input-shaping problem.
Better inputs also help prompt caching
This was the part that changed how I think about preprocessing.
Cleaning inputs is not only about quality. It also affects cost.
OpenAI’s prompt caching depends on the rendered prefix matching across requests. That includes developer instructions, tool definitions, and conversation history. If the front of your prompt changes every run because you keep stuffing in unstable junk, cache hits drop.
And cache hits matter.
A lot.
The practical takeaway is simple:
- stable prefixes are cheaper
- normalized inputs are more reusable
- noisy raw payloads sabotage both reliability and caching
If you are running agents all day in n8n, Make, Zapier, OpenClaw, or your own worker system, this compounds quickly.
This is also why flat-rate inference setups become attractive once you start scaling automations. If your workflows are constantly reprocessing noisy context, per-token billing punishes you twice: once for the extra junk, and again for the retries and drift. Standard Compute exists for exactly that kind of workload: OpenAI-compatible API, flat monthly pricing, and no need to babysit token spend while agents run.
The 4 highest-leverage cleanup steps
If you only fix a few things, fix these.
1) Email threads: stop sending the whole archaeological dig
Most support and sales agents do not need the entire thread every turn.
They usually need:
- the latest customer message
- a short summary of prior context
- a few preserved facts like order ID, refund date, SKU, or previous commitment
That’s it.
Full email threads are context poison:
- signatures
- disclaimers
- mobile footers
- repeated headers
- nested quoted replies
A much better pattern is:
- Parse the thread
- Extract the newest human-written message
- Summarize prior context into 3-5 bullets
- Preserve critical entities separately
| Input style | What happens |
|---|---|
| Full email thread | Long, repetitive, unstable across turns, easy to bury the real ask |
| Latest message plus summary | Shorter, more actionable, more stable for agents |
Here’s a rough Node example for the shape you want:
function shapeEmailThread(rawThread) {
return {
latest_customer_message: extractLatestCustomerMessage(rawThread),
thread_summary: summarizePriorThread(rawThread),
critical_facts: {
order_id: extractOrderId(rawThread),
promised_refund_date: extractRefundDate(rawThread),
sku: extractSku(rawThread)
}
};
}
You do not need perfect parsing to get a big improvement.
You just need to stop dumping the whole dig site into the prompt.
2) Scraped pages: raw HTML is a crime against context windows
If you are passing full scraped HTML into GPT-5 or Claude and hoping for clean extraction, you are making the job harder than it needs to be.
For articles and content pages, Mozilla Readability is the easiest default.
It gives you a much better object than a raw DOM dump:
titlebylineexcerpttextContentpublishedTime
Example:
npm install @mozilla/readability jsdom
const { Readability } = require('@mozilla/readability');
const { JSDOM } = require('jsdom');
function extractArticle(html) {
const dom = new JSDOM(html);
const article = new Readability(dom.window.document).parse();
return {
title: article?.title,
byline: article?.byline,
excerpt: article?.excerpt,
text: article?.textContent,
publishedTime: article?.publishedTime
};
}
Compare that with sending:
- nav bars
- cookie banners
- footers
- related stories
- tracking junk
- 60 unrelated links
| Option | Result |
|---|---|
| Raw scraped HTML | High noise, huge token footprint, main content gets buried |
| Readability output | Lower noise, smaller input, main content surfaces first |
If your agent summarizes pages, extracts claims, creates notes, or classifies content, this is not a nice-to-have.
It should be the default.
3) Webhooks: flatten the blob
Webhook payloads are where agent quality goes to die.
Stripe, Shopify, HubSpot, Salesforce, Discord, and internal apps all love deeply nested JSON with fields your LLM does not need.
If your model has to do schema archaeology before it can do the real task, you already lost time and tokens.
Instead of forwarding the raw payload, normalize it.
For example:
{
"event_type": "order.refund_requested",
"customer_name": "Ava Chen",
"customer_email": "ava@example.com",
"account_id": "acct_123",
"order_id": "ord_456",
"sku_list": ["SKU-1", "SKU-2"],
"status_before": "fulfilled",
"status_after": "refund_requested",
"user_message": "Package arrived damaged",
"internal_notes_summary": "VIP customer, approved expedited handling",
"timestamp": "2026-09-25T10:30:00Z"
}
That stable shape is much easier for a model to reason over.
| Input style | What changes |
|---|---|
| Raw webhook payload | Inconsistent fields, noisy nesting, harder reasoning |
| Normalized event schema | Stable field names, better reliability, easier reuse |
And yes, this also helps caching because the prompt prefix becomes more repeatable.
4) CRM notes: extract facts, not formatting
CRM notes are usually a mess of:
- timestamps
- copied email fragments
- internal comments
- meeting links
- stale follow-ups
The model rarely needs all of that.
It usually needs a compact representation like:
{
"account_name": "Northwind Labs",
"current_stage": "proposal_sent",
"latest_customer_ask": "Need pricing for 50 seats",
"risks": ["procurement delay", "security review pending"],
"next_action": "send revised quote",
"owner": "Sam",
"deadline": "2026-09-30"
}
That is much more useful than 2,000 tokens of CRM archaeology.
n8n already gives you enough to do this
One reason I keep telling people not to buy a “smarter” model first: the boring fixes are often already available in the workflow tool they use today.
In n8n, you already have enough primitives to clean most inputs before they hit the LLM:
- HTML node for extraction and cleanup
- Code node for JavaScript normalization, deduping, and pruning
- Set node for shaping stable payloads
- Merge / IF nodes for fallback logic
A sane architecture in n8n usually looks like this:
- Receive messy input
- Normalize into a stable schema
- Send cleaned input to the LLM
- Keep raw payload available only if needed
That same pattern works in Make, Zapier, OpenClaw, or a custom queue worker.
Example Code node idea in n8n:
const payload = $json;
return [{
json: {
event_type: payload.type,
customer_email: payload.customer?.email,
customer_name: payload.customer?.name,
order_id: payload.order?.id,
latest_message: payload.ticket?.latest_message,
internal_notes_summary: summarizeNotes(payload.ticket?.notes || []),
timestamp: payload.created_at
}
}];
That one step can make downstream prompts dramatically cleaner.
Don’t over-clean: keep a dual path
There is a real counterargument here.
Sometimes aggressive cleanup deletes the one detail that mattered:
- the timestamp proving an SLA breach
- the SKU that distinguishes two similar products
- the exact wording of a previous promise
And some tasks genuinely need raw structure:
- debugging malformed HTML
- validating a webhook schema
- auditing exact CRM wording
- checking whether a parser broke the payload
So the pattern I trust is this:
- cleaned input for reasoning
- raw payload available on demand
That gives GPT-5, Claude Opus, Llama, or whatever model you use the readable version first, while preserving source data for inspection when needed.
What a cache-friendly prompt actually looks like
It looks boring.
That’s good.
You want the top of the prompt to be stable:
- same system or developer instructions
- same tool definitions
- same field order
- same labels
- same schema
Then you slot task-specific content into predictable sections.
Example structure:
SYSTEM:
You classify inbound support events.
Always return JSON.
TOOLS:
- create_ticket
- escalate_refund
INPUT_SCHEMA:
- event_type
- customer_name
- customer_email
- order_id
- latest_message
- thread_summary
- critical_facts
TASK_INPUT:
{normalized payload here}
And an OpenAI-compatible request might look like:
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5",
"input": "SYSTEM:\nYou classify inbound support events...\n\nTASK_INPUT:\n{...}"
}'
If you are using Standard Compute, the same OpenAI-compatible request shape works there too, which is useful if you want to keep your existing SDKs and stop thinking about per-token billing while these automations run at volume.
My rule now: fix the garbage before blaming the model
People want the answer to be:
- switch from GPT-5 to Claude
- add more few-shot examples
- buy a bigger context window
- add one magic prompt line
Sometimes that helps.
A lot of the time, it doesn’t.
The easiest win in agent ops is much less glamorous:
clean the mess before the model sees it.
Strip email boilerplate.
Collapse thread history.
Run scraped pages through Mozilla Readability.
Prune webhook fields.
Flatten CRM notes into a schema the model can reason about.
Do that first.
A surprising number of “model quality” problems disappear immediately.
And if the agent still struggles after that, great.
Now you are finally debugging the model instead of debugging your garbage input.
Practical checklist
If you want the short version, here’s the checklist I’d apply this week:
- remove signatures, disclaimers, and quoted replies from email threads
- pass latest message + summary instead of whole thread
- run article pages through Mozilla Readability before LLM extraction
- normalize webhook payloads into stable flat schemas
- extract CRM facts into explicit fields
- keep raw payloads available for fallback or audit
- keep prompt prefixes stable so caching can work
- only compare models after input quality is under control
That order matters.
Input cleanup first.
Prompt tuning second.
Model swapping third.
That’s the stack rank that has saved me the most time.
Top comments (0)