Auto-triage Gorgias tickets using an agent skill

low complexityCost: Usage-based

Prerequisites

Prerequisites
  • Claude Code, Cursor, or another AI coding agent that supports skills
  • GORGIAS_EMAIL — your Gorgias account email
  • GORGIAS_API_KEY — your Gorgias REST API key (Settings → API → REST API)
  • GORGIAS_DOMAIN — your store subdomain (e.g., your-store)
  • ANTHROPIC_API_KEY — your Anthropic API key

Overview

This approach uses an agent skill that fetches untagged open tickets from Gorgias, passes each one to Claude (claude-haiku-4-5) for topic classification, and writes the tag back via the Gorgias API. Because Claude reads the full ticket body and understands intent rather than matching keywords, it handles vague, multilingual, and conversational tickets that keyword Rules would miss.

Step 1: Create the skill directory

mkdir -p .claude/skills/ticket-triage/scripts

Step 2: Write the SKILL.md

Create .claude/skills/ticket-triage/SKILL.md:

---
name: ticket-triage
description: Classifies untagged open Gorgias tickets by reading subject and body, then applies a topic tag (billing, shipping, returns, technical, account, or feedback) via the Gorgias API.
disable-model-invocation: true
allowed-tools: Bash(python *)
---
 
Classify untagged support tickets:
 
1. Run: `python $SKILL_DIR/scripts/triage.py`
2. Review the output — it lists each ticket classified and the tag applied
3. Spot-check a few in Gorgias to verify accuracy

Step 3: Write the triage script

Create .claude/skills/ticket-triage/scripts/triage.py:

#!/usr/bin/env python3
"""
Gorgias Ticket Triage
Fetches untagged open tickets → classifies with Claude → applies tags via API.
"""
import os
 
try:
    import requests
    from anthropic import Anthropic
except ImportError:
    os.system("pip install requests anthropic -q")
    import requests
    from anthropic import Anthropic
 
GORGIAS_EMAIL  = os.environ["GORGIAS_EMAIL"]
GORGIAS_KEY    = os.environ["GORGIAS_API_KEY"]
GORGIAS_DOMAIN = os.environ["GORGIAS_DOMAIN"]
BASE_URL       = f"https://{GORGIAS_DOMAIN}.gorgias.com/api"
AUTH           = (GORGIAS_EMAIL, GORGIAS_KEY)
 
CATEGORIES = ["billing", "shipping", "returns", "technical", "account", "feedback", "other"]
client = Anthropic()
 
 
def get_untagged_tickets(limit: int = 50) -> list:
    resp = requests.get(
        f"{BASE_URL}/tickets",
        auth=AUTH,
        params={"status": "open", "limit": limit},
    )
    resp.raise_for_status()
    tickets = resp.json().get("data", [])
    return [t for t in tickets if not t.get("tags")]
 
 
def classify(subject: str, body: str) -> str:
    prompt = (
        f"Classify this customer support ticket into exactly one category.\n\n"
        f"Categories: {', '.join(CATEGORIES)}\n\n"
        f"Subject: {subject}\n"
        f"Body (first 500 chars): {body[:500]}\n\n"
        f"Reply with only the category name, nothing else."
    )
    message = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=20,
        messages=[{"role": "user", "content": prompt}],
    )
    tag = message.content[0].text.strip().lower()
    return tag if tag in CATEGORIES else "other"
 
 
def apply_tag(ticket_id: int, new_tag: str, existing: list) -> None:
    # Merge to avoid replacing existing tags
    merged = list({t["name"] for t in existing} | {new_tag})
    resp = requests.put(
        f"{BASE_URL}/tickets/{ticket_id}",
        auth=AUTH,
        json={"tags": [{"name": t} for t in merged]},
    )
    resp.raise_for_status()
 
 
def main() -> None:
    print("Fetching untagged open tickets...")
    tickets = get_untagged_tickets()
    print(f"Found {len(tickets)} untagged tickets\n")
 
    if not tickets:
        print("Nothing to triage.")
        return
 
    for ticket in tickets:
        subject  = ticket.get("subject", "")
        messages = ticket.get("messages", [])
        body     = messages[0].get("body_text", "") if messages else ""
        tag      = classify(subject, body)
        apply_tag(ticket["id"], tag, ticket.get("tags", []))
        print(f"  #{ticket['id']}  {subject[:60]!r}{tag}")
 
    print(f"\nTagged {len(tickets)} ticket(s).")
 
 
if __name__ == "__main__":
    main()

Step 4: Run the skill

# Via Claude Code
/ticket-triage
 
# Or run the script directly
python .claude/skills/ticket-triage/scripts/triage.py

The script processes up to 50 untagged open tickets per run. At claude-haiku-4-5 pricing, classifying 50 tickets costs roughly $0.05 in API calls.

Step 5: Schedule it

Option A: Cron

# crontab -e — run every hour on weekdays
0 8-18 * * 1-5 cd /path/to/project && python .claude/skills/ticket-triage/scripts/triage.py

Option B: GitHub Actions

name: Ticket Triage
on:
  schedule:
    - cron: '0 13-22 * * 1-5'  # 8 AM–5 PM ET, weekdays
  workflow_dispatch: {}
jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install requests anthropic
      - run: python .claude/skills/ticket-triage/scripts/triage.py
        env:
          GORGIAS_EMAIL:    ${{ secrets.GORGIAS_EMAIL }}
          GORGIAS_API_KEY:  ${{ secrets.GORGIAS_API_KEY }}
          GORGIAS_DOMAIN:   ${{ secrets.GORGIAS_DOMAIN }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

When to use this approach

  • You need semantic classification — Claude understands "my parcel hasn't arrived" as a shipping issue even without the word "shipping"
  • You have multilingual customers and keyword Rules fail on non-English tickets
  • You want to test category definitions before hardcoding them into Gorgias Rules

Cost

  • Claude Haiku: ~$0.001 per ticket classified
  • 1,000 tickets/month ≈ $1 in API costs

Need help implementing this?

We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.