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:
- Where does memory live?
- What identifier joins records across channels?
- 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_idaccount_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
And here’s the same idea as a practical workflow:
- Receive an event from Slack, Gmail, Outlook, Zendesk, or HubSpot
- Resolve the user to a stable
customer_idoraccount_id - Normalize the event into a support fact
- Write that fact into shared memory
- Load shared memory before the LLM drafts a response
- 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"
}
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()
);
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();
Read before generating a reply
select memory
from support_memory
where customer_id = $1;
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()
};
}
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 Memoryif you want something inspectable and boring - use
Redis Chat Memoryif Redis is already in your stack and latency matters - use
Chat Memory Managerwhen 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?
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)