Building a CSAT insights pipeline with Hasura, Claude, and Mailgun
How I turned a firehose of raw support survey responses into a weekly digest of themes the team actually reads.
Every week we collected hundreds of CSAT survey responses and did almost nothing with them. The scores went into a dashboard; the comments — where all the signal lives — went nowhere. This is how I closed that loop with a small, boring pipeline.
The shape of the problem
Raw survey rows aren’t insight. A pile of “the booking flow was confusing” comments only becomes useful once it’s clustered, counted, and put in front of the right person on a cadence they’ll actually keep.
So the pipeline has three jobs:
- Pull the week’s responses out of the database.
- Summarise them into recurring themes.
- Deliver the digest somewhere it gets read.
Pulling the data
The responses live behind Hasura, so extraction is just a GraphQL query with a date filter:
query WeeklyResponses($since: timestamptz!) {
csat_responses(where: { created_at: { _gte: $since } }) {
score
comment
channel
}
}
No ORM, no extra service — Hasura already exposes exactly the slice I need.
Summarising with Claude
The comments go to the Claude API with a prompt that asks for themes, not prose. The key move is forcing structured output so the next step doesn’t have to parse English:
const res = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{
role: "user",
content: `Cluster these support comments into themes.
Return ONLY JSON: { theme: string, count: number, example: string }[].
${JSON.stringify(comments)}`,
}],
});
const themes = JSON.parse(res.content[0].text);
Ask a model for JSON and it will usually oblige — but validate anyway. One malformed response shouldn’t take down the whole job.
Delivering it
The themes get rendered into a simple HTML email and sent through Mailgun every Monday morning. That’s the entire trick: the insight has to arrive before the week’s decisions get made, or it’s just archaeology.
What I’d change
The clustering is stateless — it forgets last week existed. The next version keeps a running set of themes so we can see a complaint trending, not just present. That’s the difference between a report and an early-warning system.