# Routing support conversations with segments and session data

> A shared inbox where everything looks the same is a queue, not a support system. Segments route, session data answers the first three questions, and both belong in code.

Support works fine with twenty conversations a week: one person reads
everything. At two hundred it stops working. The billing questions sit behind
the "how do I export a CSV" questions, the enterprise trial that is about to
expire waits four hours behind a free user's feature request, and every
conversation starts with the same three questions: who are you, what plan are
you on, what were you doing?

The fix is not more people. It is telling the inbox what it is looking at.

## Two mechanisms, two jobs

**Session data** is context: key/value pairs shown beside the conversation. It
answers "who is this and what were they doing" before an operator asks.

**Segments** are routing: labels that drive who picks a conversation up, which
automations fire, and which saved views it appears in.

They are not interchangeable. Session data is read by a human; segments are read
by the system. Putting an account id in a segment gives you a thousand segments
and no routing; putting "billing" in session data gives you a note nobody can
filter on.

## Session data: three or four keys, chosen deliberately

```ts
import { setSessionData } from "@/lib/support/crisp";

setSessionData({
  plan: user.plan,                 // "free" | "pro" | "enterprise"
  account_id: user.accountId,      // so an operator can open your admin panel
  signed_up: user.createdAt.slice(0, 10),
  app_version: process.env.NEXT_PUBLIC_APP_VERSION ?? "dev",
});
```

Rules that come from watching real inboxes:

- **Only what an operator can act on.** If nobody in support will ever do
  anything differently because of a field, it is noise that will still be there
  in a year.
- **Never spread an object.** `setSessionData({ ...user })` ships every column
  the type gains later, including the ones added after this line was written.
- **Never a credential.** No tokens, session ids, signed URLs or reset links.
  The wrapper strips key names matching `token|secret|password|session|jwt` and
  values shaped like credentials, but relying on the filter is not a plan. If an
  operator needs something sensitive to help, they look it up in the admin panel
  behind their own login, where the access is logged.
- **Never free text a user typed.** A search query or a note can contain
  anything, including a third party's personal data.

Add the current page when the chat opens: it turns "it doesn't work" into a
question you can answer:

```ts
<SupportButton
  context={{ area: "billing", page: pathname, invoice_id: invoice.id }}
  segments={["billing"]}
>
  Ask billing support
</SupportButton>
```

## Segments: few, stable, lowercase

```ts
import { setSegments } from "@/lib/support/crisp";

const segments = [user.plan];                       // "pro"
if (isTrialing) segments.push("trial");
if (user.plan === "enterprise") segments.push("priority");

setSegments(segments);
```

A good segment set is small enough to memorise: `free`, `pro`, `enterprise`,
`trial`, `billing`, `onboarding`, `priority`. Each one should correspond to
something a human or an automation does differently.

Where each comes from:

- **Persistent segments**: plan, tier, lifecycle stage. Applied once when the
  widget mounts, from the server session.
- **Conversation segments**: the area the user was in when they opened the
  chat: `billing`, `import`, `api`. Applied by the button that opened it.

Two anti-patterns:

- **Ids in segments.** `user_1a2b3c` is not routing, it is a leak with a filter
  on it. Ids belong in session data.
- **A segment per feature.** Fifty segments route nothing, because nobody
  configures fifty rules. If you cannot list them from memory, there are too
  many.

## What routing looks like once the labels exist

Inside Crisp (or any equivalent inbox) the segments feed:

- **Saved views**: "enterprise + trial" as a view someone owns and watches.
- **Assignment rules**: `billing` goes to whoever is on billing this week.
- **Triggers**: a message to `enterprise` conversations that goes unanswered
  for fifteen minutes pings a Slack channel.
- **Reporting**: response time by segment is the number that tells you whether
  your priority customers are actually getting priority. Without segments you
  can only measure the average, which hides exactly the failures you care about.

The code's job is to apply correct labels every time. The configuration inside
the inbox is the support team's job, and it changes far more often than the
code, which is the reason to keep the segment vocabulary small and stable, and
to write the vocabulary down where both sides can see it.

## Keeping the vocabulary honest

Put the list in one place and derive from it:

```ts
// src/lib/support/segments.ts
export const SEGMENTS = ["free", "pro", "enterprise", "trial", "billing", "onboarding"] as const;
export type Segment = (typeof SEGMENTS)[number];

export function planSegments(user: { plan: Segment; trialEndsAt: Date | null }): Segment[] {
  const segments: Segment[] = [user.plan];
  if (user.trialEndsAt && user.trialEndsAt > new Date()) segments.push("trial");
  return segments;
}
```

Now a typo is a compile error, the set is greppable, and adding a segment is a
decision someone makes on purpose rather than a string that appears in a
component one afternoon.

## Sign-out and identity

Segments and session data are attached to the Crisp session, not to your app's.
Call `resetSession()` on sign-out or the next person on that browser inherits
the previous user's labels: an operator would see "enterprise, priority" on a
conversation from a stranger.

And remember that an unverified email makes every other label untrustworthy: if
you have not enabled HMAC verification, `plan: enterprise` is only as reliable
as the browser that claimed it.

## Measuring whether it worked

- **First response time by segment.** The point of routing is that `priority`
  and `free` diverge. If they are identical, the labels exist but nothing acts
  on them.
- **Questions per conversation before an answer.** Good session data pushes this
  down; it is the clearest signal that the context is the right context.
- **Segment distribution.** If 90% of conversations carry no segment, the
  triggers are not firing where you thought, usually because the widget mounts
  before the session resolves and the labels are applied to nothing.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
