Objective
A support inbox never arrives sorted. A furious billing dispute from an enterprise account sits in the same queue as a thank-you note and a routine "where's my order?" — and a human has to read every one to decide how urgent it is and who should own it. That triage is the slowest, most error-prone step in support, and it happens before anyone has even started helping.
This recipe does the triage inside the database. Every message is scored
with SENTIMENT() — a SQL function that returns positive, negative, or
neutral using the engine's built-in model, no Hugging Face endpoint and no
Python service. Then a plain-SQL routing rule turns sentiment + customer tier +
a couple of keywords into a concrete assignment: team, owner, and priority.
The final step hands the hardest message to an in-database agent (AGENT_RUN)
that writes the routing rationale and a suggested first reply. One table, one
connection, one auth token.
Step 1: Create the inbox and the routing directory
recipe_support_inbox holds raw messages exactly as they land from email/chat.
recipe_support_teams is a tiny directory of who handles what, so routing
decisions point at a real owner.
CREATE TABLE IF NOT EXISTS recipe_support_inbox (
message_id INTEGER PRIMARY KEY,
received_at TIMESTAMP,
channel TEXT, -- email | chat | twitter
customer_name TEXT,
customer_tier TEXT, -- free | pro | enterprise
subject TEXT,
body TEXT
);
CREATE TABLE IF NOT EXISTS recipe_support_teams (
team TEXT PRIMARY KEY,
owner TEXT,
handles TEXT
);
Step 2: Seed a realistic inbox
Ten messages that look like a real Monday morning: praise, a churn-risk enterprise complaint, a double-charge, an outage, and routine questions.
INSERT INTO recipe_support_inbox (message_id, received_at, channel, customer_name, customer_tier, subject, body) VALUES
(1, '2026-08-03 08:02:00', 'email', 'Dana Whitfield', 'enterprise', 'Incredible onboarding', 'Your onboarding team was amazing this week - we were live in two days. Thank you!'),
(2, '2026-08-03 08:07:00', 'email', 'Marcus Reyes', 'enterprise', 'Charged twice, no refund', 'I have been charged twice this month and three emails to billing have gone unanswered. If this is not fixed today we are cancelling our contract.'),
(3, '2026-08-03 08:11:00', 'chat', 'Priya Nair', 'pro', 'Order status?', 'Hi, can you tell me the status of order #4471? It has been a few days.'),
(4, '2026-08-03 08:15:00', 'email', 'Tom Bexley', 'pro', 'URGENT: production is down', 'Our production integration is down and your API has been returning 500 errors for the last hour. This is business critical.'),
(5, '2026-08-03 08:19:00', 'twitter','Sofia Almeida', 'free', 'love it', 'just wanted to say the new dashboard is gorgeous, great work team!'),
(6, '2026-08-03 08:24:00', 'email', 'Henry Osei', 'enterprise', 'Considering alternatives', 'We are frustrated with the slow response times and are evaluating competitors. We need a call this week.'),
(7, '2026-08-03 08:30:00', 'chat', 'Aiko Tanaka', 'free', 'How do I reset my password?', 'Where is the password reset link? I cannot find it in settings.'),
(8, '2026-08-03 08:36:00', 'email', 'Gabriel Santos', 'pro', 'Refund not received', 'The refund you promised last week still has not arrived and I am starting to lose patience.'),
(9, '2026-08-03 08:41:00', 'email', 'Lena Vogt', 'enterprise', 'Renewal + expansion', 'We are thrilled with the platform and want to talk about expanding to two more teams next quarter.'),
(10,'2026-08-03 08:47:00', 'chat', 'Omar Haddad', 'free', 'Feature question', 'Does the export support CSV as well as JSON? Just checking before I upgrade.');
INSERT INTO recipe_support_teams (team, owner, handles) VALUES
('Escalations', 'Sarah Chen (Sr. CSM)', 'Angry or churn-risk enterprise accounts'),
('Billing', 'Ravi Kapoor', 'Charges, refunds, invoices'),
('On-call SRE', 'Ops pager rotation', 'Outages, 5xx errors, downtime'),
('Tier-1 Support', 'Support queue', 'General questions, how-tos'),
('Advocacy', 'Mia Torres (Community)', 'Happy customers, testimonials, expansion');
Step 3: Ground truth — read the inbox by eye
Before any AI, look at what you would route manually. This is the check you compare the machine against.
SELECT message_id, customer_tier, channel, subject
FROM recipe_support_inbox
ORDER BY message_id;
Expected: 10 rows. By eye, the obvious escalations are #2 (double charge + cancel threat, enterprise), #4 (production outage), and #6 (churn-risk enterprise). #1, #5, #9 are clearly happy. The rest are routine.
Step 4: Classify sentiment with one SQL function
SENTIMENT(text) returns a single word — positive, negative, or neutral.
No model to download, no endpoint to call.
SELECT message_id, customer_name, subject, SENTIMENT(body) AS sentiment
FROM recipe_support_inbox
ORDER BY message_id;
Expected: praise (#1, #5, #9) → positive; the billing disputes, the churn-risk note, and the outage (#2, #4, #6, #8) → negative; routine questions (#3, #10) → neutral. Borderline messages tip on tone — the terse password-reset ask (#7) comes back negative because the model reads its mild frustration. These labels are the model's judgment, not a lookup table, so treat them as signal, not gospel.
Persist the label so downstream routing and reporting don't re-run the model:
ALTER TABLE recipe_support_inbox ADD COLUMN sentiment TEXT;
UPDATE recipe_support_inbox SET sentiment = SENTIMENT(body);
Step 5: Triage — turn sentiment into a team, owner, and priority
This is the payoff: a deterministic routing rule that combines the sentiment label with the customer tier and a few keyword signals. It answers who gets this message and how fast.
SELECT
m.message_id,
m.customer_name,
m.customer_tier,
m.sentiment,
CASE
WHEN LOWER(m.body) LIKE '%down%' OR LOWER(m.body) LIKE '%500%' OR LOWER(m.body) LIKE '%outage%'
THEN 'On-call SRE'
WHEN LOWER(m.body) LIKE '%charge%' OR LOWER(m.body) LIKE '%refund%' OR LOWER(m.body) LIKE '%invoice%'
THEN 'Billing'
WHEN m.sentiment = 'negative' AND m.customer_tier = 'enterprise'
THEN 'Escalations'
WHEN m.sentiment = 'positive'
THEN 'Advocacy'
ELSE 'Tier-1 Support'
END AS assigned_team,
CASE
WHEN LOWER(m.body) LIKE '%down%' OR LOWER(m.body) LIKE '%500%' OR LOWER(m.body) LIKE '%outage%' THEN 'P1'
WHEN m.sentiment = 'negative' AND m.customer_tier = 'enterprise' THEN 'P1'
WHEN m.sentiment = 'negative' THEN 'P2'
WHEN m.sentiment = 'neutral' THEN 'P3'
ELSE 'P4'
END AS priority
FROM recipe_support_inbox m
ORDER BY priority, m.message_id;
Expected routing: #4 → On-call SRE / P1 (outage keywords), #2 → Billing / P1 (charge keyword routes it; negative + enterprise sets P1), #8 → Billing / P2 (refund keyword), #6 → Escalations / P1 (negative enterprise, no keyword), #1/#5/#9 → Advocacy / P4 (positive), #3/#10 → Tier-1 / P3 (neutral). Notice the design: the keyword rule deliberately sends billing pain to Billing even for enterprise accounts, while the priority still escalates on account tier — so an enterprise complaint gets the right owner and the right urgency.
Join to the directory to hand each message a named owner:
SELECT r.message_id, r.assigned_team, t.owner, r.priority, r.subject
FROM (
SELECT
m.message_id, m.subject, m.sentiment,
CASE
WHEN LOWER(m.body) LIKE '%down%' OR LOWER(m.body) LIKE '%500%' OR LOWER(m.body) LIKE '%outage%' THEN 'On-call SRE'
WHEN LOWER(m.body) LIKE '%charge%' OR LOWER(m.body) LIKE '%refund%' OR LOWER(m.body) LIKE '%invoice%' THEN 'Billing'
WHEN m.sentiment = 'negative' AND m.customer_tier = 'enterprise' THEN 'Escalations'
WHEN m.sentiment = 'positive' THEN 'Advocacy'
ELSE 'Tier-1 Support'
END AS assigned_team,
CASE
WHEN LOWER(m.body) LIKE '%down%' OR LOWER(m.body) LIKE '%500%' OR LOWER(m.body) LIKE '%outage%' THEN 'P1'
WHEN m.sentiment = 'negative' AND m.customer_tier = 'enterprise' THEN 'P1'
WHEN m.sentiment = 'negative' THEN 'P2'
WHEN m.sentiment = 'neutral' THEN 'P3'
ELSE 'P4'
END AS priority
FROM recipe_support_inbox m
) r
JOIN recipe_support_teams t ON t.team = r.assigned_team
ORDER BY r.priority, r.message_id;
Step 6: Level up — let an in-DB agent write the rationale and first reply
Deterministic rules route the obvious cases. For the highest-priority message, have the in-database agent read it, confirm the routing, and draft a response — so the assigned owner starts with a first draft, not a blank page.
SELECT AGENT_RUN(
'aidb-assistant',
'A support message from an enterprise customer reads: "I have been charged ' ||
'twice this month and three emails to billing have gone unanswered. If this ' ||
'is not fixed today we are cancelling our contract." Its sentiment is ' ||
'negative. In 3 short numbered lines output: (1) which team should own it and ' ||
'why, (2) the priority P1-P4, (3) a two-sentence first reply the owner can send.'
) AS triage_brief;
Expected: a short brief that routes the message to Billing, marks it P1,
and drafts an apologetic reply acknowledging the double charge and committing to
a same-day fix. (The message text is inlined here so the step is self-contained;
in a real app you'd pass the row's body in as a bind parameter — an
AGENT_RUN argument can't itself contain a (SELECT …) subquery.)
Cleanup (Optional)
DROP TABLE IF EXISTS recipe_support_inbox;
DROP TABLE IF EXISTS recipe_support_teams;
Use it from your app or agent
- REST/SDK: run Step 4 on
INSERT(or on a schedule) to stampsentimenton every new row, then Step 5 as a view your ticketing UI reads. Your app never calls a separate sentiment API — it's oneclient.sql(...). - Durable agent: wrap Step 6 in
CREATE AGENT ... ON INSERT INTO recipe_support_inboxso every new message is auto-triaged the moment it lands, with the rationale written to an audit table. - MCP: an LLM client (Claude Code, Cursor) can call the same SQL through the
querytool — "score and route the last 20 support messages" becomes one tool call. - Why in-DB: the message, its sentiment, the routing rule, the team directory, and the agent's reasoning all live in the same store under one tenant scope. No message text is shipped to a third-party NLP service, and the triage is auditable because it's just SQL.
Key Concepts Learned
SENTIMENT(text)is a first-class SQL function returningpositive/negative/neutral— classification without a separate ML stack.- Triage is a policy, and policy is just
CASElogic over the AI label plus business signals (tier, keywords) you already store. - Escalate from deterministic rules to
AGENT_RUNonly for the cases that need judgment — cheap where you can, smart where you must. - Everything — raw text, AI labels, routing, and rationale — stays in one engine, one query surface, one auth boundary.