Send Slack alerts for low Zendesk CSAT scores using a Claude Code skill
Install this skill
Download the skill archive and extract it into your .claude/skills/ directory.
csat-alert.skill.zipPrerequisites
This skill works with any agent that supports the Claude Code skills standard, including Claude Code, Claude Cowork, OpenAI Codex, and Google Antigravity.
- One of the agents listed above
- Zendesk account with CSAT surveys enabled and actively collecting responses (Professional plan or above)
- Slack bot with
chat:writepermission, added to the target channel
Why a Claude Code skill?
The other approaches in this guide are deterministic: they run the same logic every time, the same way. An Claude Code skill is different. You tell Claude what you want in plain language, and the skill gives it enough context to do it reliably.
That means you can say:
- "Check for bad CSAT ratings and post alerts to Slack"
- "How many bad ratings came in today? Summarize by agent."
- "Post a CSAT summary for this week to #cx-leadership instead"
The skill contains workflow guidelines, API reference materials, and a message template that the agent reads on demand. When you invoke the skill, Claude reads these files, writes a script on the fly, runs it, and reports results. If you ask for something different next time — a longer lookback window, filtering by ticket group, a trend summary instead of individual alerts — the agent adapts without you touching any code.
How it works
The skill directory has three parts:
SKILL.md— workflow guidelines telling the agent what steps to follow, which env vars to use, and what pitfalls to avoidreferences/— Zendesk API patterns (endpoints, request shapes, response formats) so the agent calls the right APIs with the right parameterstemplates/— a Slack Block Kit template so messages are consistently formatted across runs
When invoked, the agent reads SKILL.md, consults the reference and template files as needed, writes a Python script, executes it, and reports what it posted. The reference files act as guardrails — the agent knows exactly which endpoints to hit and what the responses look like, so it doesn't have to guess.
What is a Claude Code skill?
An Claude Code skill is a reusable command you add to your project that Claude Code can run on demand. Skills live in a .claude/skills/ directory and are defined by a SKILL.md file that tells the agent what the skill does, when to run it, and what tools it's allowed to use.
In this skill, the agent doesn't run a pre-written script. Instead, SKILL.md provides workflow guidelines and points to reference files — API documentation, message templates — that the agent reads to generate and execute code itself. This is the key difference from a traditional script: the agent can adapt its approach based on what you ask for while still using the right APIs and message formats.
Once installed, you can invoke a skill as a slash command (e.g., /zendesk-csat-alert), or the agent will use it automatically when you give it a task where the skill is relevant. Skills are portable — anyone who clones your repo gets the same commands.
Step 1: Create the skill directory
mkdir -p .claude/skills/zendesk-csat-alert/{templates,references}This creates the layout:
.claude/skills/zendesk-csat-alert/
├── SKILL.md # workflow guidelines + config
├── templates/
│ └── slack-alert.md # Block Kit template for Slack messages
└── references/
└── zendesk-csat-api.md # Zendesk satisfaction ratings API patternsStep 2: Write the SKILL.md
Create .claude/skills/zendesk-csat-alert/SKILL.md:
---
name: zendesk-csat-alert
description: Check for bad Zendesk CSAT ratings and post alerts to Slack with ticket details
disable-model-invocation: true
allowed-tools: Bash, Read
---
## Goal
Check for Zendesk satisfaction ratings with score "bad" in a given time window (default: last 1 hour) and post a formatted alert per rating to a Slack channel with full ticket context.
## Configuration
Read these environment variables:
- `ZENDESK_SUBDOMAIN` — Zendesk subdomain, e.g. "acme" for acme.zendesk.com (required)
- `ZENDESK_EMAIL` — Zendesk agent email for API auth (required)
- `ZENDESK_API_TOKEN` — Zendesk API token (required)
- `SLACK_BOT_TOKEN` — Slack bot token starting with xoxb- (required)
- `SLACK_CHANNEL_ID` — Slack channel ID starting with C (required)
Default lookback window: 1 hour. The user may request a different window.
## Workflow
1. Validate that all required env vars are set. If any are missing, print which ones and exit.
2. Fetch bad satisfaction ratings from Zendesk filtered to `score=bad`, sorted newest first. See `references/zendesk-csat-api.md` for the endpoint and response format.
3. Filter to ratings updated within the lookback window. If none, print "No new bad ratings found" and exit.
4. For each bad rating, fetch full ticket details (subject, requester, assignee) via the tickets endpoint. See `references/zendesk-csat-api.md` for the endpoint and response format.
5. Post a Block Kit message to Slack for each bad rating using the format in `templates/slack-alert.md`.
6. Print a summary of how many alerts were posted.
## Important notes
- Zendesk CSAT is binary: customers rate "Good" or "Bad." Any "Bad" rating warrants follow-up.
- Zendesk triggers can natively fire on satisfaction rating changes (Professional plan+), but this skill adds batch analysis, agent filtering, trend summaries, and Block Kit formatting beyond native capabilities.
- Authentication uses Basic Auth with `email/token:api_token` format. The `/token` suffix after the email is required.
- `SLACK_CHANNEL_ID` must be the channel ID (starts with `C`), not the channel name. The `chat.postMessage` API requires the ID.
- The Slack bot must be invited to the target channel or `chat.postMessage` will fail with `not_in_channel`.
- Use only Python standard library (`urllib.request`, `json`, `base64`) — no pip dependencies needed.Understanding the SKILL.md
Unlike a script-based skill, this SKILL.md doesn't contain a Run: command pointing to a script. Instead, it provides:
| Section | Purpose |
|---|---|
| Goal | Tells the agent what outcome to produce |
| Configuration | Which env vars to read and what defaults to use |
| Workflow | Numbered steps with pointers to reference files |
| Important notes | Non-obvious context that prevents common mistakes |
The allowed-tools: Bash, Read setting lets the agent both read reference files and execute code. The agent writes its own script based on the workflow steps and reference materials.
Step 3: Add reference files
references/zendesk-csat-api.md
Create .claude/skills/zendesk-csat-alert/references/zendesk-csat-api.md:
# Zendesk CSAT API Reference
## Authentication
All Zendesk API requests use Basic Auth. The username is `email/token:api_token` (note the `/token` suffix after the email address).
```
Authorization: Basic base64(email/token:api_token)
```
Base URL: `https://{subdomain}.zendesk.com/api/v2`
## List satisfaction ratings (filtered to bad)
Fetch recent bad CSAT ratings, sorted newest first.
**Request:**
```
GET https://{subdomain}.zendesk.com/api/v2/satisfaction_ratings?score=bad&sort_order=desc&per_page=20
Authorization: Basic <credentials>
```
**Parameters:**
- `score=bad` — filter to only unsatisfied ratings
- `sort_order=desc` — newest first
- `per_page=20` — number of results per page (max 100)
**Response shape:**
```json
{
"satisfaction_ratings": [
{
"id": 12345,
"ticket_id": 48291,
"score": "bad",
"comment": "Very disappointed with the response time",
"updated_at": "2026-03-05T14:30:00Z",
"created_at": "2026-03-05T14:28:00Z"
}
]
}
```
Key fields:
- `id` — unique rating ID
- `ticket_id` — the associated ticket
- `score` — always "bad" since we filter on it
- `comment` — optional customer comment explaining the rating (may be null)
- `updated_at` — use this for time-based filtering
## Get ticket details
Fetch full ticket information for context in the Slack alert.
**Request:**
```
GET https://{subdomain}.zendesk.com/api/v2/tickets/{ticket_id}.json
Authorization: Basic <credentials>
```
**Response shape:**
```json
{
"ticket": {
"id": 48291,
"subject": "Order delayed twice",
"requester_id": 67890,
"assignee_id": 11111,
"status": "solved",
"tags": ["billing", "escalation"],
"created_at": "2026-03-03T10:00:00Z"
}
}
```
Key fields:
- `subject` — ticket subject line for the alert
- `requester_id` — numeric ID of the customer (resolve via Users API if name is needed)
- `assignee_id` — numeric ID of the assigned agent (resolve via Users API if name is needed)
- `status` — current ticket status
## Get user details (optional)
Resolve a user ID to a name for richer alerts.
**Request:**
```
GET https://{subdomain}.zendesk.com/api/v2/users/{user_id}.json
Authorization: Basic <credentials>
```
**Response shape:**
```json
{
"user": {
"id": 67890,
"name": "Sarah Chen",
"email": "sarah@example.com"
}
}
```
Use this to resolve `requester_id` and `assignee_id` from the ticket response into display names.
## Rate limits
Zendesk allows roughly 700 API requests per minute on Professional plans. This workflow uses 1 request for ratings + 1 per bad rating for ticket details + optionally 1-2 for user resolution. Well under 1% of rate limits.templates/slack-alert.md
Create .claude/skills/zendesk-csat-alert/templates/slack-alert.md:
# Slack Alert Template
Use this Block Kit structure for each bad CSAT rating alert.
## Block Kit JSON
```json
{
"channel": "<SLACK_CHANNEL_ID>",
"text": "Bad CSAT rating on #<ticket_id>: <subject>",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Bad CSAT Rating Received",
"emoji": true
}
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": "*Ticket:* <ticket_url|#ticket_id>"},
{"type": "mrkdwn", "text": "*Subject:* <subject>"},
{"type": "mrkdwn", "text": "*Customer:* <requester_name>"},
{"type": "mrkdwn", "text": "*Assignee:* <assignee_name>"}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Customer feedback:* <comment>"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "View in Zendesk"},
"url": "<ticket_url>"
}
]
}
]
}
```
## Notes
- The top-level `text` field is required by the Slack API as a fallback for notifications and accessibility. Always include it.
- The ticket URL format: `https://{subdomain}.zendesk.com/agent/tickets/{ticket_id}`
- If the customer left no comment, display "(no comment)" instead of an empty string.
- Requester and assignee names require resolving user IDs via the Users API. If skipping resolution, show the ID instead.Step 4: Test the skill
Invoke the skill conversationally:
/zendesk-csat-alertClaude will read the SKILL.md, check the reference files, write a script, run it, and report the results. A typical run looks like:
Checking for bad CSAT ratings in the last 1 hour(s)...
Found 2 new bad rating(s)
Posted: #48291 — Order delayed twice (Bad — "Very disappointed with the response time")
Posted: #48195 — Slow response to outage report (Bad — no comment)
Done. Posted 2 alert(s) to Slack.What the Slack alert looks like
Bad CSAT Rating Received
Ticket: #48291 — Order delayed twice
Customer: Sarah Chen
Rating: Bad — "Very disappointed with the response time"
Agent: Mike P.
Because the agent generates code on the fly, you can also make ad hoc requests:
- "Check for bad CSAT ratings in the last 4 hours" — the agent adjusts the lookback window
- "How many bad ratings came in today? Break down by agent." — the agent adds grouping logic
- "Post a CSAT summary for this week to #cx-leadership" — the agent adapts the output format and channel
Submit a bad CSAT rating on a solved ticket in your Zendesk sandbox, wait a few seconds, then run the skill. If no bad ratings were submitted in the last hour, you'll see "No new bad ratings found" — which is correct, not an error.
Step 5: Schedule it (optional)
Option A: Cron + Claude CLI
# Run every 30 minutes during business hours
*/30 8-18 * * 1-5 cd /path/to/your/project && claude -p "Run /zendesk-csat-alert" --allowedTools 'Bash(*)' 'Read(*)'Option B: GitHub Actions + Claude
name: CSAT Alert
on:
schedule:
- cron: '*/30 * * * *' # Every 30 minutes
workflow_dispatch: {} # Manual trigger for testing
jobs:
alert:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: anthropics/claude-code-action@v1
with:
prompt: "Run /zendesk-csat-alert"
allowed_tools: "Bash(*),Read(*)"
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ZENDESK_SUBDOMAIN: ${{ secrets.ZENDESK_SUBDOMAIN }}
ZENDESK_EMAIL: ${{ secrets.ZENDESK_EMAIL }}
ZENDESK_API_TOKEN: ${{ secrets.ZENDESK_API_TOKEN }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}Option C: Cowork Scheduled Tasks
Claude Desktop's Cowork supports built-in scheduled tasks. Open a Cowork session, type /schedule, and configure the cadence — hourly, daily, weekly, or weekdays only. Each scheduled run has full access to your connected tools, plugins, and MCP servers.
Scheduled tasks only run while your computer is awake and Claude Desktop is open. If a run is missed, Cowork executes it automatically when the app reopens. For always-on scheduling, use GitHub Actions (Option B) instead. Available on all paid plans (Pro, Max, Team, Enterprise).
*/30 * * * * runs every 30 minutes UTC. GitHub Actions cron may also have up to 15 minutes of delay. For time-sensitive alerting, use cron on your own server or a dedicated scheduler instead.
Troubleshooting
When to use this approach
- You want conversational flexibility — ad hoc queries like "how many bad ratings came in today?" alongside scheduled checks
- You want on-demand summaries during team standups or QBRs, not just automated notifications
- You're already using Claude Code and want skills that integrate with your workflow
- You want to run tasks in the background via Claude Cowork while focusing on other work
- You prefer guided references over rigid scripts — the agent adapts while staying reliable
When to switch approaches
- You need near-real-time alerts (under 5 minutes) running 24/7 → use n8n with polling
- You want a visual workflow builder with no code at all → use n8n
- You need alerts running 24/7 with zero LLM cost → use the n8n approach (free when self-hosted)
Common questions
Why not just use a script?
A script runs the same way every time. The Claude Code skill adapts to what you ask — different lookback windows, filtered by agent or ticket group, summary format instead of individual alerts, a different channel. The reference files ensure it calls the right APIs even when improvising, so you get flexibility without sacrificing reliability.
Does this use Claude API credits?
Yes. Unlike a script-based approach, the agent reads skill files and generates code each time. Typical cost is $0.01-0.05 per invocation depending on how many ratings are returned and how much the agent needs to read. The Zendesk and Slack APIs themselves are free.
Can I run this on a schedule without a server?
Yes. GitHub Actions (Option B in Step 5) runs Claude on a cron schedule using GitHub's infrastructure. The free tier includes 2,000 minutes/month — more than enough for 30-minute polling intervals.
Can Zendesk trigger this automatically when a bad rating comes in?
Zendesk triggers can natively fire on Satisfaction: Changed to Bad (Professional plan+) for basic Slack notifications. This skill goes further — it polls the API for batch analysis, filters by agent or time window, calculates trends, and sends Block Kit formatted messages with full context. Use native triggers for simple alerts; use this skill for richer analysis.
Cost
- Claude API — $0.01-0.05 per invocation (the agent reads files and generates code)
- Zendesk API — included in Professional plan and above, no per-call cost
- Slack API — included in all plans, no per-call cost
- GitHub Actions (if scheduled) — free tier includes 2,000 minutes/month
Looking to scale your AI operations?
We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.