DEV Community

Cover image for My agent got way better when I stopped feeding it garbage first
Lars Winstand
Lars Winstand

Posted on Originally published at standardcompute.com

My agent got way better when I stopped feeding it garbage first

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:

  1. Parse the thread
  2. Extract the newest human-written message
  3. Summarize prior context into 3-5 bullets
  4. 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)
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

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:

  • title
  • byline
  • excerpt
  • textContent
  • publishedTime

Example:

npm install @mozilla/readability jsdom
Enter fullscreen mode Exit fullscreen mode
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
  };
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Receive messy input
  2. Normalize into a stable schema
  3. Send cleaned input to the LLM
  4. 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
  }
}];
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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{...}"
  }'
Enter fullscreen mode Exit fullscreen mode

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)