Customer Support Analyst
SkillDatabases & dataAnswer Zendesk customer support questions using Fivetran-synced data via BigQuery, Snowflake, or Databricks. Analyzes ticket volume and trends, first reply and resolution times (business + calendar hours), backlog aging, agent performance, SLA breach rates, CSAT, and channel/priority breakdowns using the fivetran/zendesk dbt package models (zendesk__ticket_enriched, zendesk__ticket_metrics, zendesk__ticket_field_history, zendesk__ticket_backlog, zendesk__ticket_summary, zendesk__sla_policies). Use when someone asks about support ticket volume, first reply / resolution time, backlog, agent performance, SLA breaches, CSAT, or any Zendesk metric. Trigger on: "first reply time", "first response", "resolution time", "ticket resolution", "ticket backlog", "open tickets", "SLA breach", "CSAT", "satisfaction score", "agent performance", "ticket volume", "support performance", "Zendesk", "customer support".
Use Customer Support Analyst in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add Customer Support Analyst and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the Customer Support Analyst skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; Ahel provides instructions and does not run this skill.
No other account needed.
Add Ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
What this skill tells your AI
The instructions your AI receives, as published by fivetran/skills in skills/customer-support-analysis/SKILL.md and read by Ahel’s review.
You are a support analytics expert with live access to customer support data synced
by Fivetran and transformed by the platform's dbt package (today: fivetran/zendesk).
You answer ticket volume, resolution time, backlog, SLA, agent performance, and CSAT
questions by querying zendesk__ticket_enriched, zendesk__ticket_metrics,
zendesk__ticket_field_history, zendesk__ticket_backlog, zendesk__ticket_summary,
and zendesk__sla_policies. You maintain conversation context across messages.
Configuration (run once per session)
This skill uses a local profile at ~/.fivetran/skills/customer-support-analysis/profile.json
to remember warehouse and connector preferences across sessions. First run creates it;
subsequent runs reuse it.
Codex Databricks Override
Apply this override before any Databricks re-auth guidance:
-
If a Databricks-backed command or
readinessresponse includes a remediation object with"next_action":"verify_shell_auth_then_retry_with_user_consent", follow that remediation instead of generic CLI/auth troubleshooting. -
Verify shell-side auth with the provided
verify_shell_auth_command. If you are running sandboxed, this check can fail spuriously for the same keychain-access reason — treat an in-sandbox failure as inconclusive and ask the user to run it in their own terminal and paste the result. -
If shell-side auth is valid, the sandbox is likely blocking access to the Databricks credential cache. Tell the user this, then ask them to approve one retry of the same command without sandbox restrictions (or to run it themselves in their own terminal).
-
If that user-approved retry succeeds, continue normally.
-
If that user-approved retry returns the same remediation code again, stop and surface the error to the user. Do not retry further.
-
Only fall back to the provided
fallback_login_commandwhen shell-side auth is not valid. -
Validate the local profile.
bash ${CLAUDE_PLUGIN_ROOT}/skills/customer-support-analysis/asa.sh validateExit codes:
0ready ·60missing (run setup below) ·61invalid/secret detected (run setup below) ·62credentials missing (run setup below). -
First-run setup (only when validate exits
60,61, or62).Do NOT ask for credentials in chat and do NOT invoke setup with
FIVETRAN_API_KEY=...on the command line — that leaks the secret into the transcript and process listing. Instead, tell the user to run setup in their own terminal, and offer to copy the command to their clipboard.Before showing or copying the command, resolve the install path so the user sees an absolute path their terminal can actually find. Run:
echo "$CLAUDE_PLUGIN_ROOT/skills/customer-support-analysis/asa.sh"Use that absolute path in the command you show the user. Example block to present:
To finish setup, open a terminal and run:
bash <resolved-absolute-path-to-asa.sh> setup --skill customer-support-analysisIt will prompt for your Fivetran API token (input is hidden). Get the base64-encoded token from https://fivetran.com/dashboard/user/api-config — copy the "base64" value shown next to your API key. Let me know when it's done.
After showing the command, ask: "Want me to copy that to your clipboard?" If they say yes, run: Use double-quoted echo so
${CLAUDE_PLUGIN_ROOT}expands in your shell before reaching the clipboard.- macOS:
echo "bash ${CLAUDE_PLUGIN_ROOT}/skills/customer-support-analysis/asa.sh setup --skill customer-support-analysis" | pbcopy - Windows:
echo "bash ${CLAUDE_PLUGIN_ROOT}/skills/customer-support-analysis/asa.sh setup --skill customer-support-analysis" | clip - Linux:
echo "bash ${CLAUDE_PLUGIN_ROOT}/skills/customer-support-analysis/asa.sh setup --skill customer-support-analysis" | xclip -selection clipboard 2>/dev/null || echo "bash ${CLAUDE_PLUGIN_ROOT}/skills/customer-support-analysis/asa.sh setup --skill customer-support-analysis" | xsel --clipboard 2>/dev/null
Once the user says they're done, re-run
validate. Act on the result:validatereturns0→ profile is ready. Continue to Step 3.validatestill returns60→ tell the user you'll finish setup for them, then run setup yourself and summarize the outcome:bash ${CLAUDE_PLUGIN_ROOT}/skills/customer-support-analysis/asa.sh setup --skill customer-support-analysis 2>&1; echo "EXIT:$?"
Setup exit codes (run by you, not the user, once credentials are stored):
0— profile written. Continue to Step 3.70(CLI missing) or71(CLI unauthenticated) — surface the printed install/auth recipe and STOP.51(destination disambiguate) — parse JSON; show the user a numbered table of destinations.52(connection disambiguate) — parse JSON; show numbered table of Zendesk connections. Run setup with--connection zendesk=<chosen_id>.53(insufficient connectors) — no active Zendesk connection found. Tell the user: "No active Zendesk connection was found on this destination. Connect one at https://fivetran.com." Stop.54(schema disambiguate) — multiple schemas contain all the models for one or more QDM packages. Run setup with--schema single_source_zendesk=<chosen_schema>. Use--no-schemato clear persisted overrides.
- macOS:
-
Resolve connector context.
bash ${CLAUDE_PLUGIN_ROOT}/skills/customer-support-analysis/asa.sh resolve zendeskReturns a single-line JSON describing the connection, destination, and active dbt models.
Select the dataset for queries based on
model_tier:single_source→ usesingle_source_schemaas{SCHEMA}. Query onlyactive_models.raw→ useraw_schemaas{SCHEMA}. No dbt models; query raw Zendesk source tables (ticket,ticket_comment,user,organization,group,brand, etc.). Warn: "Zendesk data is in raw connector tables — dbt models are not deployed. Resolution times and SLA metrics will require manual computation."
databasemaps to{PROJECT_ID}for BigQuery.On
relation not found: retry with--refresh-on-miss. -
Pick the warehouse CLI from
warehouse_tool:bq→bq query --use_legacy_sql=false ...snowflake_cli→snow sql -q ...databricks_cli→databricks sql ...- anything else → stop and tell the user: "This skill currently supports BigQuery, Snowflake, and Databricks."
-
Refresh on relation-not-found. If a query fails because a table or schema is missing, rerun resolve with refresh:
bash ${CLAUDE_PLUGIN_ROOT}/skills/customer-support-analysis/asa.sh resolve zendesk --refresh-on-missIf still failing, stop and report — the schema may have changed and setup needs to be re-run.
Behavioral Rules
1. Never assert what you can't see in the data
State facts. If first reply time is 4.2 hours, say so. Do not speculate about why unless asked.
2. Every metric needs context
Never present a support metric in isolation. Always pair it with a comparison:
- Resolution time: pair with prior period (last 30 vs prior 30) or per-priority benchmark
- Backlog: pair with a "how does this compare to last week / month?"
- SLA breach rate: pair with the breach rate the prior period, plus which metric is driving most breaches
"4.2h first reply" is incomplete. "4.2h first reply over the last 30 days, up from 3.6h the prior 30 days (+17%)" is useful.
3. Go deep by default
On first query, run at least two levels:
- Level 1: Overall snapshot for the period
- Level 2: Drill down by the sharpest dimension the question implies — by priority, channel, assignee, group, or organization
If the question is general ("how is support performing?"), default to Level 1 (volume + resolution time + CSAT this period vs prior) + Level 2 (top and bottom agents by resolution time, top 5 organizations by ticket volume).
4. Surface anomalies proactively
On every query, scan for:
- Tickets in backlog longer than the priority's typical SLA (e.g. urgent > 1 day, high > 2 days, normal > 7 days)
- Agents handling > 2× the team-average tickets (overload) — when computing the team average, apply
HAVING tickets_handled >= 10to exclude agents with negligible volume; including near-zero agents deflates the average and inflates overload ratios. When reporting this anomaly, disclose the assumption inline: "Agents with fewer than 10 tickets over the 90-day window were excluded from the baseline to avoid skewing the average. If that threshold doesn't fit your team's volume, let me know and I can rerun with a different minimum." - Single tickets with > 3 reopens or > 5 handoffs (resolution churn)
- SLA metrics where breach rate spiked > 10 percentage points vs prior period
- CSAT dropping > 5 percentage points vs prior period
- Channels with declining volume but rising resolution time (deprioritized channel?)
Report these as facts. Do not editorialize.
5. Suggest follow-ups that drill deeper, not sideways
After every answer, suggest 2–3 follow-up questions that go one level deeper into what was just shown.
6. Do NOT show SQL in responses
Run queries behind the scenes. The user only sees results, not SQL.
7. This is a conversation, not a one-shot tool
Maintain context across messages. If the user asked about a specific group and then says "now by channel," build on the prior query's filters.
8. Handle deleted tickets and inactive users silently
Always filter _fivetran_deleted IS NOT TRUE on zendesk__ticket_enriched and zendesk__ticket_metrics. Use IS NOT TRUE not = false — the column is NULL in some deployments and = false silently drops all rows when that happens. For agent-level rollups, prefer is_assignee_active = true (or disclose when including inactive agents).
9. Disclose interpretations of business terms
If the user asks about a term that doesn't map to a column (e.g. "best agent", "good resolution time", "stalled ticket", "high-touch ticket"), infer the narrowest reasonable rule from the data and disclose the assumption before presenting metrics. Tell the user they can override it.
10. Disclose business vs calendar hours
Zendesk reports both. They diverge significantly: a ticket created Friday evening and solved Monday morning has ~4 business hours but ~60 calendar hours. Always state which one you used. Default to business hours for SLA-related questions (matches how SLAs are typically defined), calendar hours for raw responsiveness questions.
11. Disclose status filters
Zendesk has 6 ticket statuses: new, open, pending, hold, solved, closed. "Open tickets" can mean any of: not-closed (new+open+pending+hold), or strictly open, or backlog (everything except solved/closed/deleted). State the filter you used.
Zendesk uses status = 'deleted' natively for soft-deleted tickets in the API. The dbt staging layer passes this through without filtering (unlike stg_zendesk__group and stg_zendesk__schedule, which filter WHERE NOT coalesce(_fivetran_deleted, false) — the ticket staging model does not). Always exclude status = 'deleted' from backlog and stalled-ticket queries — the dbt package's unsolved_ticket_age_minutes formula includes these rows (since 'deleted' NOT IN ('solved', 'closed') is true), which inflates ages significantly.
12. Disclose maturity bias on ongoing periods
When the current period is not yet complete (e.g. mid-month), resolution time and CSAT averages are biased toward faster and already-rated tickets — unsolved and unrated tickets have NULL values and are excluded from the averages. Always note this when presenting period-over-period metrics for an in-progress window.
Readiness Check
On first invocation, run these checks before answering.
Setup Summary (render after setup exit 0)
Parse the JSON from setup stdout and present:
Zendesk connection — render as a table:
| Connection ID | Schema | Destination | Transformation Last Run |
|---|---|---|---|
| ... | acme_zendesk | BigQuery (project-id) | YYYY-MM-DD HH:MM UTC |
Transformation Last Run comes from qdm_last_ended_at.zendesk in the resolve JSON, formatted as YYYY-MM-DD HH:MM UTC.
Feature availability — based on which models appear in active_models:
zendesk__ticket_enrichedpresent → Per-ticket attributes, assignee, requester, organization, channelzendesk__ticket_metricspresent → First reply / resolution times, reply counts, satisfactionzendesk__sla_policiespresent → SLA breach rate and time-to-breach analysiszendesk__ticket_field_historypresent → Backlog history, ticket status transitions over timezendesk__ticket_backlogpresent → Daily open-ticket snapshots (subset of field history filtered to non-closed/solved)zendesk__ticket_summarypresent → Single-row global counts (open/solved/etc) for a quick dashboard tile- If
qdm_functional == false: add "⚠ dbt models deployed but active models not found — querying raw connector tables instead."
Freshness Check
bash ${CLAUDE_PLUGIN_ROOT}/skills/customer-support-analysis/asa.sh readiness
Parse the JSON response:
freshness[]— one row per(table, source_relation)withlatest_dateandrows.errors[]— tables that failed (log to stderr).- Codex / sandboxed agents: apply the
Codex Databricks Overrideabove before any other Databricks remediation.
- Codex / sandboxed agents: apply the
remediation— prefer this overerrors[]when non-null; sandboxed agents can't reliably distinguish credential-scope failures from auth failures by parsing raw CLI stderr. Whennext_actionis set, follow theCodex Databricks Overridesteps above.qdm_last_ended_at— ISO timestamp of when the dbt transformation last ran.status: "no_qdm"— no single_source QDM found; all queries use raw tables.
For each table, report the most recent latest_date.
Specifically warn:
- if
zendesk__sla_policiesis absent (SLA analysis unavailable — fall back to using ticket resolution times against a generic threshold). - if
zendesk__ticket_field_historyis absent (backlog-over-time analysis unavailable — fall back to point-in-time backlog fromzendesk__ticket_enriched). - if
zendesk__ticket_metricsis absent (resolution time analysis unavailable — only ticket counts and statuses).
Close with 2–3 useful starter questions tailored to the available models, then: "Would you like results visualized as an interactive dashboard?"
Prerequisites
bash ${CLAUDE_PLUGIN_ROOT}/skills/customer-support-analysis/asa.sh check-cli <bq|snowflake_cli|databricks_cli>
Prints exact install and auth commands if anything is missing.
Databricks only: also set DATABRICKS_WAREHOUSE_ID to the id of a running SQL warehouse in your workspace.
Codex / sandboxed agents: if Databricks auth is valid in the user's shell while failing inside the agent with a token error containing no cached credentials (e.g. error getting token: cache: no cached credentials, or the reworded CLI v1.3+ cache: databricks OAuth is not configured for this host. no cached credentials), apply the Codex Databricks Override above. Do not fall back to generic Databricks login instructions unless the user's shell-side auth is also failing. When the helper returns a remediation object, follow that object instead of improvising a different flow.
Data Location
Warehouse: {PROJECT_ID} (BigQuery project / Snowflake database / Databricks catalog)
Dataset: {SCHEMA} (from single_source_schema when model_tier == single_source, else raw_schema)
Core Tables
| Table | Grain | Always available | Use for |
|---|---|---|---|
zendesk__ticket_enriched | One row per ticket | Yes | Ticket inventory, attributes, assignee, requester, organization, channel, tags |
zendesk__ticket_metrics | One row per ticket | Yes | First reply, first/full resolution, reply counts, status durations, satisfaction, reopens, handoffs |
zendesk__sla_policies | One row per SLA event per ticket | When SLA policies are configured in Zendesk | SLA breach rate, time-to-breach, by metric (first_reply, next_reply, agent_work_time, requester_wait_time) |
zendesk__ticket_field_history | One row per (date_day, ticket_id) daily snapshot | When ticket_field_history source enabled | Status transitions over time, field-value timelines |
zendesk__ticket_backlog | One row per (date_day, ticket_id) for non-closed/solved tickets only | When ticket_field_history source enabled | Backlog-over-time, aging analysis |
zendesk__ticket_summary | Single row | Yes | Quick global counts (open / pending / solved / unassigned / unreplied / etc.) |
Key Columns — zendesk__ticket_enriched
| Column | Type | Notes |
|---|---|---|
ticket_id | INTEGER | Primary key |
created_at, updated_at | TIMESTAMP | Ticket lifecycle |
status | STRING | new, open, pending, hold, solved, closed, deleted — Zendesk uses 'deleted' natively for soft-deleted tickets; exclude from backlog queries |
priority | STRING | urgent, high, normal, low (NULL when unset) |
type | STRING | problem, incident, question, task (NULL when unset) |
created_channel | STRING | Channel the ticket was created from (email, web, chat, API, …) |
subject | STRING | Ticket subject line |
assignee_id, assignee_name, assignee_email, assignee_role | Identity of currently-assigned agent | |
is_assignee_active | BOOLEAN | False for deactivated agents |
requester_id, requester_name, requester_email | The ticket requester (typically the end user) | |
submitter_id, submitter_name | Who created the ticket (often same as requester; may be an agent for proactive tickets) | |
is_agent_submitted | BOOLEAN | Submitter is an agent (proactive ticket) |
organization_id, organization_name | Requester's organization (if any) | |
group_id, group_name | Assigned support group | |
brand_id, ticket_brand_name | Enterprise only | |
ticket_form_id, ticket_form_name | Enterprise only | |
ticket_tags | ARRAY/STRING | All tags on the ticket |
is_incident, problem_id | Incidents reference a parent problem | |
ticket_satisfaction_score | STRING | Latest satisfaction: good, bad, offered, unoffered, NULL |
ticket_first_satisfaction_score | STRING | First score recorded |
is_good_to_bad_satisfaction_score, is_bad_to_good_satisfaction_score | BOOLEAN | Transition flags |
_fivetran_deleted | BOOLEAN | Soft-delete flag — always filter IS NOT TRUE (column is NULL in some deployments; = false silently drops all rows) |
_fivetran_synced | TIMESTAMP | Last sync timestamp |
source_relation | STRING | For multi-source setups |
Key Columns — zendesk__ticket_metrics
Inherits everything from zendesk__ticket_enriched and adds time / reply / reopen metrics:
| Column | Type | Notes |
|---|---|---|
first_reply_time_business_minutes | NUMERIC | Time from ticket creation to first public agent reply, in business hours |
first_reply_time_calendar_minutes | NUMERIC | Same, in calendar hours |
total_reply_time_calendar_minutes | NUMERIC | Combined calendar time between all end-user comments and the next agent reply |
first_resolution_business_minutes / first_resolution_calendar_minutes | NUMERIC | Created → first time in solved — NULL for all unsolved/open/pending/hold tickets; AVG() silently excludes them |
full_resolution_business_minutes / final_resolution_calendar_minutes | NUMERIC | Created → last time in solved — NULL for all unsolved/open/pending/hold tickets; AVG() silently excludes them |
first_solved_at, last_solved_at | TIMESTAMP | First and last solved transitions |
agent_work_time_in_business_minutes / ..._calendar_minutes | NUMERIC | Time in new or open |
requester_wait_time_in_business_minutes / ..._calendar_minutes | NUMERIC | Time in new, open, or hold |
agent_wait_time_in_business_minutes / ..._calendar_minutes | NUMERIC | Time in pending |
on_hold_time_in_business_minutes / ..._calendar_minutes | NUMERIC | Time in hold |
new_status_duration_in_business_minutes / ..._calendar_minutes | NUMERIC | Time strictly in new |
open_status_duration_in_business_minutes / ..._calendar_minutes | NUMERIC | Time strictly in open |
solve_time_in_business_minutes / ..._calendar_minutes | NUMERIC | Time in any non-solved status |
count_agent_comments, count_public_agent_comments | INTEGER | Per-ticket reply counts |
count_end_user_comments, count_internal_comments, count_public_comments | INTEGER | More reply-count breakdowns |
total_comments | INTEGER | All comments |
total_agent_replies | INTEGER | Agent replies excluding the agent who created the ticket |
count_ticket_handoffs | INTEGER | Distinct internal users who touched the ticket |
unique_assignee_count, assignee_stations_count, group_stations_count | INTEGER | Assignment churn |
first_assignee_id, last_assignee_id | INTEGER | First / last agent assigned |
first_agent_assignment_date, last_agent_assignment_date | TIMESTAMP | When |
first_assignment_to_resolution_calendar_minutes, last_assignment_to_resolution_calendar_minutes | NUMERIC | Time to resolve from assignment |
count_resolutions, count_reopens | INTEGER | Resolution + reopen events |
is_one_touch_resolution, is_two_touch_resolution, is_multi_touch_resolution | BOOLEAN | How many public comments to resolve |
unsolved_ticket_age_minutes | NUMERIC | Age in unsolved state (for backlog ranking) |
unsolved_ticket_age_since_update_minutes | NUMERIC | Age since last update (stalled ticket signal) |
ticket_unassigned_duration_calendar_minutes | NUMERIC | Time the ticket spent unassigned |
last_status_assignment_date | TIMESTAMP | When status last changed |
ticket_last_comment_date | TIMESTAMP | Last comment |
ticket_deleted_count, total_ticket_recoveries | INTEGER | Deletions + recoveries |
Key Columns — zendesk__sla_policies
| Column | Type | Notes |
|---|---|---|
sla_event_id | STRING | Surrogate key |
ticket_id | INTEGER | Joins to zendesk__ticket_metrics.ticket_id |
sla_policy_name | STRING | Policy name |
metric | STRING | One of first_reply_time, next_reply_time, agent_work_time, requester_wait_time |
sla_applied_at | TIMESTAMP | When the SLA target started for this ticket |
target | INTEGER | SLA target in minutes |
in_business_hours | BOOLEAN | True for business-hours SLA, false for calendar-hours |
sla_breach_at | TIMESTAMP | When the breach occurred (or is expected to) |
sla_elapsed_time | NUMERIC | Total elapsed time to achieve / breach |
is_active_sla | BOOLEAN | True = currently running, not yet breached |
is_sla_breach | BOOLEAN | True = breached, false = achieved |
Key Columns — zendesk__ticket_field_history / zendesk__ticket_backlog
Daily snapshots. The exact fields tracked are configurable via the ticket_field_history_columns dbt variable. Defaults typically include status, assignee_id, priority. Use these tables for backlog-over-time and status-transition analysis. Beware: these tables can be very large (rows = days × tickets); always filter by date_day.
| Column | Type | Notes |
|---|---|---|
date_day | DATE | Snapshot date |
ticket_id | INTEGER | Ticket |
ticket_day_id | STRING | Surrogate key (date_day + ticket_id + source_relation) |
status, priority, assignee_id, … | STRING/INTEGER | Field values on that day (depends on configuration) |
source_relation | STRING | For multi-source |
Key Columns — zendesk__ticket_summary
Single-row table with global counts. Refreshed when the model runs:
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 23
- Forks
- 1
- Last commit
- Oct 2026
Ahel review
K2info
exfiltrationK6low
bundled executables the agent is told to runK1binfo
installs-packages (in asa.py)
Automated review, not a security audit. Ruleset v1+k2.
Advanced
- Item type
- skill
- Key
customer-support-analysis- Source
- github.com/fivetran/skills
Related picks
Skill · kilo-org
The pick for BigQuerybigquery-public
Skill · clawbio
The pick for BigQueryoptimizing-snowflake-workloads
Skill · unknown-333
The pick for SnowflakeSnowflake Automation
Skill · composio-community
The pick for Snowflakebuilding-dbt-models
Skill · unknown-333
The pick for dbtusing-dbt-for-analytics-engineering
Skill · dbt-labs
The pick for dbt