Customer Support Analyst

SkillDatabases & data

Answer 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.

Add Ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Customer Support AnalystStart free

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:

  1. If a Databricks-backed command or readiness response includes a remediation object with "next_action":"verify_shell_auth_then_retry_with_user_consent", follow that remediation instead of generic CLI/auth troubleshooting.

  2. 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.

  3. 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).

  4. If that user-approved retry succeeds, continue normally.

  5. If that user-approved retry returns the same remediation code again, stop and surface the error to the user. Do not retry further.

  6. Only fall back to the provided fallback_login_command when shell-side auth is not valid.

  7. Validate the local profile.

    bash ${CLAUDE_PLUGIN_ROOT}/skills/customer-support-analysis/asa.sh validate
    

    Exit codes: 0 ready · 60 missing (run setup below) · 61 invalid/secret detected (run setup below) · 62 credentials missing (run setup below).

  8. First-run setup (only when validate exits 60, 61, or 62).

    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-analysis
    

    It 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:

    • validate returns 0 → profile is ready. Continue to Step 3.
    • validate still returns 60 → 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) or 71 (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-schema to clear persisted overrides.
  9. Resolve connector context.

    bash ${CLAUDE_PLUGIN_ROOT}/skills/customer-support-analysis/asa.sh resolve zendesk
    

    Returns a single-line JSON describing the connection, destination, and active dbt models.

    Select the dataset for queries based on model_tier:

    • single_source → use single_source_schema as {SCHEMA}. Query only active_models.
    • raw → use raw_schema as {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."

    database maps to {PROJECT_ID} for BigQuery.

    On relation not found: retry with --refresh-on-miss.

  10. 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."
  11. 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-miss
    

    If 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 >= 10 to 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 IDSchemaDestinationTransformation Last Run
...acme_zendeskBigQuery (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_enriched present → Per-ticket attributes, assignee, requester, organization, channel
  • zendesk__ticket_metrics present → First reply / resolution times, reply counts, satisfaction
  • zendesk__sla_policies present → SLA breach rate and time-to-breach analysis
  • zendesk__ticket_field_history present → Backlog history, ticket status transitions over time
  • zendesk__ticket_backlog present → Daily open-ticket snapshots (subset of field history filtered to non-closed/solved)
  • zendesk__ticket_summary present → 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) with latest_date and rows.
  • errors[] — tables that failed (log to stderr).
    • Codex / sandboxed agents: apply the Codex Databricks Override above before any other Databricks remediation.
  • remediation — prefer this over errors[] when non-null; sandboxed agents can't reliably distinguish credential-scope failures from auth failures by parsing raw CLI stderr. When next_action is set, follow the Codex Databricks Override steps 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_policies is absent (SLA analysis unavailable — fall back to using ticket resolution times against a generic threshold).
  • if zendesk__ticket_field_history is absent (backlog-over-time analysis unavailable — fall back to point-in-time backlog from zendesk__ticket_enriched).
  • if zendesk__ticket_metrics is 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

TableGrainAlways availableUse for
zendesk__ticket_enrichedOne row per ticketYesTicket inventory, attributes, assignee, requester, organization, channel, tags
zendesk__ticket_metricsOne row per ticketYesFirst reply, first/full resolution, reply counts, status durations, satisfaction, reopens, handoffs
zendesk__sla_policiesOne row per SLA event per ticketWhen SLA policies are configured in ZendeskSLA breach rate, time-to-breach, by metric (first_reply, next_reply, agent_work_time, requester_wait_time)
zendesk__ticket_field_historyOne row per (date_day, ticket_id) daily snapshotWhen ticket_field_history source enabledStatus transitions over time, field-value timelines
zendesk__ticket_backlogOne row per (date_day, ticket_id) for non-closed/solved tickets onlyWhen ticket_field_history source enabledBacklog-over-time, aging analysis
zendesk__ticket_summarySingle rowYesQuick global counts (open / pending / solved / unassigned / unreplied / etc.)

Key Columns — zendesk__ticket_enriched

ColumnTypeNotes
ticket_idINTEGERPrimary key
created_at, updated_atTIMESTAMPTicket lifecycle
statusSTRINGnew, open, pending, hold, solved, closed, deleted — Zendesk uses 'deleted' natively for soft-deleted tickets; exclude from backlog queries
prioritySTRINGurgent, high, normal, low (NULL when unset)
typeSTRINGproblem, incident, question, task (NULL when unset)
created_channelSTRINGChannel the ticket was created from (email, web, chat, API, …)
subjectSTRINGTicket subject line
assignee_id, assignee_name, assignee_email, assignee_roleIdentity of currently-assigned agent
is_assignee_activeBOOLEANFalse for deactivated agents
requester_id, requester_name, requester_emailThe ticket requester (typically the end user)
submitter_id, submitter_nameWho created the ticket (often same as requester; may be an agent for proactive tickets)
is_agent_submittedBOOLEANSubmitter is an agent (proactive ticket)
organization_id, organization_nameRequester's organization (if any)
group_id, group_nameAssigned support group
brand_id, ticket_brand_nameEnterprise only
ticket_form_id, ticket_form_nameEnterprise only
ticket_tagsARRAY/STRINGAll tags on the ticket
is_incident, problem_idIncidents reference a parent problem
ticket_satisfaction_scoreSTRINGLatest satisfaction: good, bad, offered, unoffered, NULL
ticket_first_satisfaction_scoreSTRINGFirst score recorded
is_good_to_bad_satisfaction_score, is_bad_to_good_satisfaction_scoreBOOLEANTransition flags
_fivetran_deletedBOOLEANSoft-delete flag — always filter IS NOT TRUE (column is NULL in some deployments; = false silently drops all rows)
_fivetran_syncedTIMESTAMPLast sync timestamp
source_relationSTRINGFor multi-source setups

Key Columns — zendesk__ticket_metrics

Inherits everything from zendesk__ticket_enriched and adds time / reply / reopen metrics:

ColumnTypeNotes
first_reply_time_business_minutesNUMERICTime from ticket creation to first public agent reply, in business hours
first_reply_time_calendar_minutesNUMERICSame, in calendar hours
total_reply_time_calendar_minutesNUMERICCombined calendar time between all end-user comments and the next agent reply
first_resolution_business_minutes / first_resolution_calendar_minutesNUMERICCreated → first time in solved — NULL for all unsolved/open/pending/hold tickets; AVG() silently excludes them
full_resolution_business_minutes / final_resolution_calendar_minutesNUMERICCreated → last time in solved — NULL for all unsolved/open/pending/hold tickets; AVG() silently excludes them
first_solved_at, last_solved_atTIMESTAMPFirst and last solved transitions
agent_work_time_in_business_minutes / ..._calendar_minutesNUMERICTime in new or open
requester_wait_time_in_business_minutes / ..._calendar_minutesNUMERICTime in new, open, or hold
agent_wait_time_in_business_minutes / ..._calendar_minutesNUMERICTime in pending
on_hold_time_in_business_minutes / ..._calendar_minutesNUMERICTime in hold
new_status_duration_in_business_minutes / ..._calendar_minutesNUMERICTime strictly in new
open_status_duration_in_business_minutes / ..._calendar_minutesNUMERICTime strictly in open
solve_time_in_business_minutes / ..._calendar_minutesNUMERICTime in any non-solved status
count_agent_comments, count_public_agent_commentsINTEGERPer-ticket reply counts
count_end_user_comments, count_internal_comments, count_public_commentsINTEGERMore reply-count breakdowns
total_commentsINTEGERAll comments
total_agent_repliesINTEGERAgent replies excluding the agent who created the ticket
count_ticket_handoffsINTEGERDistinct internal users who touched the ticket
unique_assignee_count, assignee_stations_count, group_stations_countINTEGERAssignment churn
first_assignee_id, last_assignee_idINTEGERFirst / last agent assigned
first_agent_assignment_date, last_agent_assignment_dateTIMESTAMPWhen
first_assignment_to_resolution_calendar_minutes, last_assignment_to_resolution_calendar_minutesNUMERICTime to resolve from assignment
count_resolutions, count_reopensINTEGERResolution + reopen events
is_one_touch_resolution, is_two_touch_resolution, is_multi_touch_resolutionBOOLEANHow many public comments to resolve
unsolved_ticket_age_minutesNUMERICAge in unsolved state (for backlog ranking)
unsolved_ticket_age_since_update_minutesNUMERICAge since last update (stalled ticket signal)
ticket_unassigned_duration_calendar_minutesNUMERICTime the ticket spent unassigned
last_status_assignment_dateTIMESTAMPWhen status last changed
ticket_last_comment_dateTIMESTAMPLast comment
ticket_deleted_count, total_ticket_recoveriesINTEGERDeletions + recoveries

Key Columns — zendesk__sla_policies

ColumnTypeNotes
sla_event_idSTRINGSurrogate key
ticket_idINTEGERJoins to zendesk__ticket_metrics.ticket_id
sla_policy_nameSTRINGPolicy name
metricSTRINGOne of first_reply_time, next_reply_time, agent_work_time, requester_wait_time
sla_applied_atTIMESTAMPWhen the SLA target started for this ticket
targetINTEGERSLA target in minutes
in_business_hoursBOOLEANTrue for business-hours SLA, false for calendar-hours
sla_breach_atTIMESTAMPWhen the breach occurred (or is expected to)
sla_elapsed_timeNUMERICTotal elapsed time to achieve / breach
is_active_slaBOOLEANTrue = currently running, not yet breached
is_sla_breachBOOLEANTrue = 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.

ColumnTypeNotes
date_dayDATESnapshot date
ticket_idINTEGERTicket
ticket_day_idSTRINGSurrogate key (date_day + ticket_id + source_relation)
status, priority, assignee_id, …STRING/INTEGERField values on that day (depends on configuration)
source_relationSTRINGFor 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
    exfiltration
  • K6low
    bundled executables the agent is told to run
  • K1binfo
    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