DEV Community

Cover image for I stopped letting GPT-5 babysit my inbox and the whole workflow got cheaper and better
Lars Winstand
Lars Winstand

Posted on • Originally published at standardcompute.com

I stopped letting GPT-5 babysit my inbox and the whole workflow got cheaper and better

I used to think email was a terrible place for AI.

Too messy. Too human. Too full of forwarded chains from 2017 and HTML generated by software nobody at the company can name.

Then I spent some time reading inbox automation threads, especially a good one on r/openclaw about email flows, and the pattern finally clicked:

Email is a great surface for AI if you stop making the model act like your mail server.

That sounds obvious. But a lot of inbox automations still do this:

  • new message arrives
  • ask GPT-5 if it is support
  • ask Claude if it is sales
  • ask another model if it is spammy
  • ask again which alias it belongs to
  • ask again whether to reply now or later

That is not intelligence.

That is expensive amnesia.

The better pattern is simple:

  • code owns state, retries, scheduling, sync, and verification
  • the LLM only handles decisions that actually require judgment

That split made my inbox workflows cheaper, easier to debug, and way less fragile.

The rule I keep coming back to

A comment from an OpenClaw workflow discussion said it better than most docs do:

If your workflow stops working when you hit your LLM usage limit, the LLM is probably doing too much.

That was about coding agents, but it applies perfectly to inbox automation.

If your email pipeline depends on a model to remember mailbox state, dedupe events, handle retries, or re-check routing rules every run, you built the wrong system.

Models are good at judgment.

They are bad at being custodians.

Email feels chaotic, but the transport is already structured

Humans experience email as chaos.

Machines do not.

Every message already arrives with useful structure:

  • From
  • To
  • Reply-To
  • Subject
  • thread identifiers
  • message IDs
  • headers
  • timestamps
  • raw MIME
  • attachment boundaries
  • alias addresses

That matters because a lot of routing decisions should never hit an LLM in the first place.

If invoices always go to ap@company.com, GPT-5 should not be rediscovering that rule every morning.

If support mail always lands on a specific alias, code should route it deterministically.

If a thread was already processed, your worker should know that from a database, not from a prompt.

What the model should do

Use GPT-5, Claude Opus 4.6, Grok, Qwen, or Llama for the parts that actually need reasoning:

  • classify ambiguous messages
  • summarize long threads
  • extract intent from ugly forwarded chains
  • draft replies for human review
  • decide whether an attachment looks like a contract, invoice, or support artifact

What code should do

Everything repetitive:

  • sync mailbox changes
  • persist sync tokens
  • enforce sender and alias rules
  • schedule follow-ups
  • retry failures
  • suppress duplicate processing
  • verify whether a thread was already handled
  • log decisions for auditability

That architecture is less flashy than "AI inbox agent."

It is also the architecture that still works next month.

Gmail’s quota numbers basically tell you how to build this

This is the part that changed how I think about inbox pipelines.

Google publishes quota costs for Gmail API methods.

  • history.list = 2 quota units
  • messages.list = 5 quota units
  • messages.get = 20 quota units
  • threads.get = 40 quota units
  • messages.send = 100 quota units

Those numbers are not trivia.

They are design hints.

Google is telling you to do this:

  1. watch for changes
  2. fetch only what changed
  3. apply deterministic filters
  4. call an LLM only for edge cases

Not this:

  1. poll the inbox every minute
  2. fetch everything unread
  3. dump whole threads into Claude
  4. repeat forever

If your workflow wakes up every minute and asks a frontier model to inspect all unread mail, you did not build automation.

You built a recurring bill.

A sane Gmail pipeline

For Gmail, the pattern is straightforward:

  1. subscribe to inbox changes with users.watch
  2. receive events via Cloud Pub/Sub
  3. use history.list to get changed message IDs
  4. fetch only the messages you actually need
  5. run deterministic rules first
  6. escalate ambiguous messages to GPT-5 or Claude

Start the mailbox watch

POST https://www.googleapis.com/gmail/v1/users/me/watch
Content-Type: application/json

{
  "topicName": "projects/myproject/topics/mytopic",
  "labelIds": ["INBOX"],
  "labelFilterBehavior": "INCLUDE"
}
Enter fullscreen mode Exit fullscreen mode

Minimal Node example for change processing

import { google } from "googleapis";

const gmail = google.gmail({ version: "v1", auth });

async function processMailboxChange(startHistoryId: string) {
  const history = await gmail.users.history.list({
    userId: "me",
    startHistoryId,
    historyTypes: ["messageAdded"]
  });

  const messageIds = new Set<string>();

  for (const item of history.data.history ?? []) {
    for (const added of item.messagesAdded ?? []) {
      if (added.message?.id) messageIds.add(added.message.id);
    }
  }

  for (const id of messageIds) {
    const msg = await gmail.users.messages.get({
      userId: "me",
      id,
      format: "metadata",
      metadataHeaders: ["From", "To", "Subject", "Reply-To"]
    });

    const headers = Object.fromEntries(
      (msg.data.payload?.headers ?? []).map(h => [h.name!, h.value!])
    );

    const subject = headers.Subject || "";
    const to = headers.To || "";
    const from = headers.From || "";

    if (to.includes("ap@company.com")) {
      await routeToAccountsPayable(id);
      continue;
    }

    if (from.endsWith("@trustedvendor.com") && subject.includes("Invoice")) {
      await routeToAccountsPayable(id);
      continue;
    }

    await sendToLLMForClassification({ id, subject, from, headers });
  }
}
Enter fullscreen mode Exit fullscreen mode

The important part is not the code style.

The important part is the order of operations:

  • cheap mailbox sync first
  • deterministic routing second
  • model call last

Outlook and Microsoft 365 do the same thing with different names

Microsoft Graph has the same architecture, just with delta queries instead of Gmail history.

GET https://graph.microsoft.com/v1.0/me/mailFolders/{id}/messages/delta
Enter fullscreen mode Exit fullscreen mode

You keep:

  • @odata.nextLink while paging
  • @odata.deltaLink for the next sync cycle

That token is your memory.

Your worker should own it.

Not the model.

Example shape in Node

async function syncOutlookFolder(deltaUrl?: string) {
  const url = deltaUrl ?? "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages/delta";
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${token}` }
  });

  const data = await res.json();

  for (const msg of data.value ?? []) {
    if (msg.toRecipients?.some((r: any) => r.emailAddress?.address === "support@company.com")) {
      await routeToSupport(msg);
      continue;
    }

    await classifyIfNeeded(msg);
  }

  if (data["@odata.nextLink"]) {
    return syncOutlookFolder(data["@odata.nextLink"]);
  }

  if (data["@odata.deltaLink"]) {
    await saveDeltaLink(data["@odata.deltaLink"]);
  }
}
Enter fullscreen mode Exit fullscreen mode

Same pattern.

Different API.

Cloudflare Email Workers make the boundary really obvious

This is my favorite example because the separation is so clean.

import PostalMime from "postal-mime";

export default {
  async email(message, env, ctx): Promise<void> {
    const subject = message.headers.get("subject") || "";
    const from = message.from || "";
    const to = message.to || "";

    if (to === "ap@example.com") {
      await message.forward("finance@example.com");
      return;
    }

    if (subject.includes("Invoice") && from.endsWith("@vendor.com")) {
      await message.forward("finance@example.com");
      return;
    }

    const parsed = await PostalMime.parse(message.raw);

    const llmResult = await classifyEmail({
      subject,
      from,
      to,
      text: parsed.text,
      html: parsed.html
    });

    if (llmResult.label === "support") {
      await message.forward("support@example.com");
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

That first if statement is doing more useful work than a lot of "autonomous agent" demos.

Tooling: what fits what

Option What it’s best at
Gmail API Push notifications with users.watch, incremental sync with history.list, and quota-aware filtering before LLM calls
Microsoft Graph Mail API Folder-level delta sync, durable mailbox state with @odata.nextLink and @odata.deltaLink, and selective classification
Cloudflare Email Workers Running custom logic directly on inbound mail with headers, raw MIME, and actions like forward(), reply(), and setReject()
OpenClaw Self-hosted orchestration with cron, webhooks, tools, memory, and multi-agent routing around the actual model calls
n8n / Make / Zapier Fast no-code or low-code orchestration for teams that want rules, retries, and integrations before adding LLM steps

None of these are "AI-native" in the marketing sense.

That is exactly why they are useful.

The ugly parts are real

To be fair, email is not clean in practice.

You still have to deal with:

  • MIME weirdness
  • HTML-only bodies
  • giant forwarded chains
  • inline images
  • attachments that contain the real payload
  • receipts, contracts, and legal threads that break naive parsers

This is why pure rules are not enough.

But it is also why pure LLM pipelines are a mistake.

The right pattern is a hard boundary with soft fallbacks:

  • parse headers and MIME deterministically
  • route obvious cases with rules
  • persist state outside the model
  • escalate ambiguous content to GPT-5, Claude, Grok, Qwen, or Llama
  • keep human review for high-risk actions like sending final replies

That hybrid setup is less sexy than "fully autonomous inbox agent."

It is also how adults build production automations.

Why this matters even more when you pay per token

This is where the economics get annoying.

If your workflow sends the same kinds of routing decisions to a model over and over, you are paying for the model to rediscover your own business logic repeatedly.

That is a bad architecture problem disguised as an AI problem.

For teams running agents all day in n8n, Make, Zapier, OpenClaw, or custom Node workers, per-token billing makes this worse fast.

You end up watching usage dashboards instead of shipping.

That is exactly why I like the model of deterministic orchestration first, LLM calls second, and why flat-rate API access is so appealing for automation-heavy workloads.

With Standard Compute, you can keep the OpenAI-compatible API shape your workflows already expect, but stop treating every classification, retry, and long thread as a billing event. It is a drop-in replacement for existing SDKs and HTTP clients, with dynamic routing across GPT-5.4, Claude Opus 4.6, and Grok 4.20 behind a predictable monthly price.

That matters a lot when your automations run 24/7 and the whole point is to stop babysitting both the inbox and the token meter.

My practical rule now

If I am building inbox automation from scratch, this is the stack I trust:

  • Gmail API or Microsoft Graph for mailbox sync
  • Cloudflare Email Workers or a Node worker for deterministic handling
  • PostalMime for parsing
  • OpenClaw, n8n, Make, or Zapier for orchestration if needed
  • GPT-5 or Claude only when the email asks a real question

That is the whole shift.

Stop asking the model to be the loop.

Make the model answer the question.

Everything got cheaper after that.

Everything also got better.

Top comments (0)