DEV Community

Cover image for My support bot kept forgetting the last ticket until I stopped treating Slack and email like separate worlds
Lars Winstand
Lars Winstand

Posted on Originally published at standardcompute.com

My support bot kept forgetting the last ticket until I stopped treating Slack and email like separate worlds

I found this bug the annoying way.

A customer reported an issue in Slack on Monday. We fixed it.

Then on Wednesday they emailed support, and the bot answered like it had never seen them before.

Wrong plan tier. No awareness of the previous issue. It even suggested the same failed workaround from two days earlier.

My first instinct was the usual one: blame the model.

Maybe GPT-5 needed a better prompt. Maybe Claude was compressing context too aggressively. Maybe the retrieval step was weak. Maybe the router picked the wrong model.

Nope.

The bot didn’t have an intelligence problem.

It had a memory address problem.

The actual bug: memory was keyed to channels, not customers

This is the part that bites a lot of support automations.

If you build an AI support workflow in n8n, Make, Zapier, OpenClaw, or your own stack, it’s easy to assume “memory” means the agent has a durable understanding of the customer.

Usually it doesn’t.

In n8n’s AI memory docs, there’s a pretty clear split:

  • Simple Memory = chat history for the current session
  • external memory backends = shared, durable memory across workflows

That distinction matters a lot.

If your Slack flow stores memory under a Slack thread ID, and your email flow stores memory under an email conversation ID, and your CRM writes updates under a ticket ID, then your bot will look smart inside each channel and completely lose the plot when the customer switches channels.

That’s not model failure.

That’s architecture.

Why support bots look great in demos and weird in production

Because demos happen in one clean conversation.

Production support is messy:

  • customer starts in Slack
  • follows up over email
  • support rep adds notes in Zendesk
  • account data lives in HubSpot
  • someone reopens the issue a week later
  • customer replies from a different email alias

Now your “memory” is spread across IDs that don’t know each other exist.

That’s why I’ve gotten pretty opinionated about this:

Most support bots do not need a smarter model first. They need shared memory keyed by a stable customer identifier.

If you can’t answer these three questions quickly, your bot is going to embarrass you:

  1. Where does memory live?
  2. What identifier joins records across channels?
  3. What events actually get written into memory?

The n8n detail that made the problem obvious

n8n’s docs are unusually honest here.

They separate session memory from external memory backends, and they list multiple external options directly in the docs:

  • Motorhead
  • Redis Chat Memory
  • Postgres Chat Memory
  • Xata
  • Zep

There’s also a separate Chat Memory Manager node for more explicit control.

That node exists for exactly the kind of workflow where basic “chat memory” stops being enough:

  • memory can’t be attached directly where you need it
  • you need to reduce or inspect memory size
  • you want to inject synthetic context before generation

That last one matters more than people think.

Once you stop treating memory like a magical model feature and start treating it like workflow state, the design gets much clearer.

The question changes from:

Which model should answer this?

To:

What should the model know before it answers?

That’s a much better question.

The fix that actually worked

We stopped keying memory to channels.

Instead of storing memory under:

  • Slack thread ID
  • email conversation ID
  • Zendesk ticket ID

We stored it under a stable identifier like:

  • customer_id
  • account_id

Then every meaningful event from Slack, email, and CRM wrote to the same durable memory record.

Not every message. That’s overkill.

Only the stuff that matters:

  • issue opened
  • issue resolved
  • workaround tried
  • workaround failed
  • refund discussed
  • plan tier confirmed
  • known bug linked
  • escalation state changed

A support bot doesn’t need a diary.

It needs a case file.

The pattern I’d recommend first

Here’s the simple version:

Slack event / Email reply / CRM update
  -> normalize event
  -> resolve stable customer_id
  -> write durable memory to Postgres or Redis
  -> optionally summarize
  -> load shared memory before generating reply
Enter fullscreen mode Exit fullscreen mode

And here’s the same idea as a practical workflow:

  1. Receive an event from Slack, Gmail, Outlook, Zendesk, or HubSpot
  2. Resolve the user to a stable customer_id or account_id
  3. Normalize the event into a support fact
  4. Write that fact into shared memory
  5. Load shared memory before the LLM drafts a response
  6. Inject CRM summary if needed

Example memory record

This is roughly the shape I want, whether it lives in Postgres, Redis, or another backend:

{
  "customer_id": "cust_48291",
  "account_tier": "pro",
  "open_issue": {
    "id": "ticket_1932",
    "summary": "Slack OAuth sync fails after token refresh",
    "status": "investigating"
  },
  "last_resolved_issue": {
    "id": "ticket_1888",
    "summary": "Webhook retries duplicated events",
    "resolved_at": "2026-09-24T14:11:00Z"
  },
  "known_failed_workarounds": [
    "Reconnect Slack app",
    "Regenerate workspace token"
  ],
  "sentiment": "frustrated",
  "last_updated_at": "2026-09-27T10:22:00Z"
}
Enter fullscreen mode Exit fullscreen mode

That is far more useful than dumping raw transcripts into memory forever.

What to store vs what not to store

This is where teams often swing from “the bot forgets everything” to “the bot stores way too much.”

Both are bad.

Good memory

  • current open issue
  • last resolved issue
  • account tier
  • blocked integrations
  • promised follow-up dates
  • workarounds already attempted
  • escalation state

Bad memory

  • every greeting
  • every raw log blob
  • full email threads forever
  • internal notes that should never be surfaced back to the customer

Selective memory beats giant memory.

A lot of support workflows work better with compact facts plus fresh CRM reads than with huge transcript history.

A concrete Postgres version

If I were building this in a boring, inspectable way, I’d start with Postgres.

Schema

create table support_memory (
  customer_id text primary key,
  memory jsonb not null default '{}'::jsonb,
  updated_at timestamptz not null default now()
);
Enter fullscreen mode Exit fullscreen mode

Upsert on every normalized event

insert into support_memory (customer_id, memory, updated_at)
values (
  $1,
  jsonb_build_object(
    'last_event', $2::jsonb,
    'account_tier', $3,
    'open_issue', $4::jsonb,
    'known_failed_workarounds', $5::jsonb
  ),
  now()
)
on conflict (customer_id)
do update set
  memory = support_memory.memory || excluded.memory,
  updated_at = now();
Enter fullscreen mode Exit fullscreen mode

Read before generating a reply

select memory
from support_memory
where customer_id = $1;
Enter fullscreen mode Exit fullscreen mode

Example normalization layer

This is the part people skip, and then they wonder why memory gets messy.

Your Slack event, email event, and CRM event should all become the same internal shape.

type SupportFact = {
  customer_id: string;
  source: 'slack' | 'email' | 'zendesk' | 'hubspot';
  type:
    | 'issue_opened'
    | 'issue_resolved'
    | 'workaround_tried'
    | 'workaround_failed'
    | 'plan_confirmed'
    | 'escalated';
  summary: string;
  timestamp: string;
};

function normalizeSlackEvent(event: any): SupportFact {
  return {
    customer_id: event.customerId,
    source: 'slack',
    type: 'workaround_failed',
    summary: 'Customer retried OAuth reconnect and it still failed',
    timestamp: new Date().toISOString()
  };
}
Enter fullscreen mode Exit fullscreen mode

That normalized layer is where the real quality comes from.

If you’re using n8n, here’s the practical mapping

Option What it’s good at
n8n Simple Memory Session-scoped chat history. Fine for one conversation. Bad fit for cross-channel support by itself.
Redis Chat Memory / Postgres Chat Memory / Xata / Zep / Motorhead Durable external memory shared across workflows and channels. Much better fit for Slack + email + CRM support.
n8n Chat Memory Manager Explicit memory control, context injection, memory-size management, and custom memory handling.

My opinionated version:

  • use Postgres Chat Memory if you want something inspectable and boring
  • use Redis Chat Memory if Redis is already in your stack and latency matters
  • use Chat Memory Manager when you need tighter control over what gets injected into the prompt

If you’re running support automation at any real volume, I would not trust session memory alone.

Can a stronger model hide bad memory design?

Temporarily, yes.

GPT-5, Claude, or Grok can sometimes infer missing context from a frustrated follow-up message. Better models are better at that game.

But that only works when the missing context is still recoverable from the latest message.

It fails when:

  • the important detail only exists in HubSpot or Zendesk
  • the customer changed channels
  • the issue happened last month
  • the previous workaround is buried in another system

No model can recall context it never received.

This is why model bake-offs often distract teams from the real bug.

The easiest way to test whether your bot has this problem

Run this checklist:

# sanity check questions
# 1. does Slack memory use the same key as email memory?
# 2. can a CRM update be loaded into the reply context?
# 3. can you inspect the current memory record for one customer?
# 4. are failed workarounds stored explicitly?
# 5. does a reopened issue update the same record?
Enter fullscreen mode Exit fullscreen mode

If the answer to any of those is “not really,” your bot probably doesn’t have a model problem.

It has a state management problem.

Where Standard Compute fits

Once you fix memory architecture, then model routing actually starts to matter.

That’s the order I’d do it in.

First:

  • unify memory
  • normalize events
  • key everything by customer_id
  • load shared context before generation

Then optimize inference cost and throughput.

That’s where something like Standard Compute makes sense for agent-heavy support workflows. It gives you an OpenAI-compatible API with flat monthly pricing, so you can run support agents across Slack, email, CRM automations, and background jobs without watching token spend every five minutes.

If you’re building in n8n, Make, Zapier, OpenClaw, or custom agent infrastructure, predictable cost matters a lot once these workflows are running all day.

Especially when you start adding:

  • summarization steps
  • memory writes
  • memory reads
  • retries
  • multi-model routing
  • background classification jobs

Per-token billing gets annoying fast.

The unsexy lesson

I wanted the fix to be smarter prompts or better model routing.

It wasn’t.

The fix was this:

One customer, one memory record, many channels.

Once Slack events, email replies, and CRM updates all wrote to the same durable store under the same stable key, the bot stopped “forgetting” the last ticket.

Not because it got smarter.

Because it finally had a place to remember.

If your support bot keeps acting like it has short-term memory loss, don’t start with a model bake-off.

Start by tracing:

  • where memory lives
  • what writes to it
  • what key joins records across systems
  • what context gets loaded before generation

That fix is boring.

It’s also the one that works.

Top comments (0)