Alert your support lead in Slack when Gorgias tickets approach SLA deadline using an agent skill

low complexityCost: Usage-based

Prerequisites

Prerequisites
  • Claude Code, Cursor, or another AI coding agent that supports skills
  • GORGIAS_EMAIL, GORGIAS_API_KEY, GORGIAS_DOMAIN environment variables
  • SLACK_BOT_TOKEN with chat:write scope
  • SLACK_CHANNEL_ID — the channel ID for SLA warning alerts
  • Your SLA targets documented (first-response time per channel)

Overview

This agent skill polls Gorgias for open tickets that haven't received a first agent response, calculates how much SLA time remains for each, and posts a Slack alert for any ticket approaching its deadline. It uses a local JSON file to track already-alerted tickets so you never get duplicate warnings. Because it runs as a simple Python script, you can trigger it manually, via skill invocation, or on a cron schedule.

Step 1: Create the skill directory

mkdir -p .claude/skills/sla-alert/scripts

Step 2: Write the SKILL.md

Create .claude/skills/sla-alert/SKILL.md:

---
name: sla-alert
description: Checks open Gorgias tickets for approaching SLA deadlines and posts warnings to Slack so the team lead can reassign or escalate before a breach occurs.
disable-model-invocation: true
allowed-tools: Bash(python *)
---
 
Check for tickets approaching their SLA deadline and alert the team:
 
1. Run: `python $SKILL_DIR/scripts/check_sla.py`
2. Review output — it lists each ticket checked, its SLA status, and whether an alert was sent
3. Check your Slack channel to verify alert formatting

Step 3: Write the SLA check script

Create .claude/skills/sla-alert/scripts/check_sla.py:

#!/usr/bin/env python3
"""
Gorgias SLA Breach Alert
Polls open tickets → calculates time remaining → posts Slack warning for at-risk tickets.
"""
import os
import json
from datetime import datetime, timezone, timedelta
from pathlib import Path
 
try:
    import requests
    from slack_sdk import WebClient
except ImportError:
    os.system("pip install requests slack_sdk -q")
    import requests
    from slack_sdk import WebClient
 
GORGIAS_EMAIL  = os.environ["GORGIAS_EMAIL"]
GORGIAS_KEY    = os.environ["GORGIAS_API_KEY"]
GORGIAS_DOMAIN = os.environ["GORGIAS_DOMAIN"]
SLACK_TOKEN    = os.environ["SLACK_BOT_TOKEN"]
SLACK_CHANNEL  = os.environ["SLACK_CHANNEL_ID"]
 
BASE_URL = f"https://{GORGIAS_DOMAIN}.gorgias.com/api"
AUTH     = (GORGIAS_EMAIL, GORGIAS_KEY)
 
# SLA targets in minutes per channel
SLA_TARGETS = {
    "email": 240,          # 4 hours
    "chat": 15,            # 15 minutes
    "contact-form": 240,   # 4 hours
    "default": 240,
}
 
# Alert when this fraction of SLA time has elapsed
WARN_THRESHOLD = 0.75
 
# Track alerted tickets to avoid duplicates
SEEN_FILE = Path(__file__).parent / ".sla_alerted.json"
alerted: dict = json.loads(SEEN_FILE.read_text()) if SEEN_FILE.exists() else {}
 
slack = WebClient(token=SLACK_TOKEN)
 
 
def get_open_tickets() -> list:
    """Fetch open tickets from Gorgias."""
    resp = requests.get(
        f"{BASE_URL}/tickets",
        auth=AUTH,
        params={"status": "open", "limit": 100},
    )
    resp.raise_for_status()
    return resp.json().get("data", [])
 
 
def has_agent_reply(ticket: dict) -> bool:
    """Check if any message in the ticket is from an agent."""
    for msg in ticket.get("messages", []):
        source_type = (msg.get("source") or {}).get("type", "")
        sender_type = (msg.get("sender") or {}).get("type", "")
        if source_type == "internal" or sender_type == "agent":
            return True
    return False
 
 
def calculate_sla_status(ticket: dict) -> dict | None:
    """Calculate SLA time remaining. Returns dict if at risk, None otherwise."""
    if has_agent_reply(ticket):
        return None
 
    channel = ticket.get("channel", "default")
    sla_minutes = SLA_TARGETS.get(channel, SLA_TARGETS["default"])
 
    created_raw = ticket.get("created_datetime", "")
    if not created_raw:
        return None
 
    try:
        created = datetime.fromisoformat(created_raw.replace("Z", "+00:00"))
    except ValueError:
        return None
 
    now = datetime.now(timezone.utc)
    elapsed = (now - created).total_seconds() / 60
    remaining = sla_minutes - elapsed
    pct_elapsed = elapsed / sla_minutes
 
    if pct_elapsed < WARN_THRESHOLD:
        return None
 
    return {
        "ticket_id": ticket["id"],
        "subject": ticket.get("subject", "(no subject)"),
        "customer_name": (ticket.get("requester") or {}).get("name")
            or (ticket.get("requester") or {}).get("email", "Unknown"),
        "customer_email": (ticket.get("requester") or {}).get("email", ""),
        "channel": channel,
        "sla_minutes": sla_minutes,
        "elapsed_minutes": round(elapsed),
        "remaining_minutes": max(round(remaining), 0),
        "breached": remaining <= 0,
    }
 
 
def post_slack_alert(info: dict) -> None:
    """Post an SLA warning to Slack."""
    status = "BREACHED" if info["breached"] else f"{info['remaining_minutes']} min remaining"
    ticket_url = f"https://{GORGIAS_DOMAIN}.gorgias.com/app/ticket/{info['ticket_id']}"
 
    blocks = [
        {
            "type": "header",
            "text": {"type": "plain_text", "text": "SLA Warning — Ticket Approaching Deadline"},
        },
        {
            "type": "section",
            "fields": [
                {"type": "mrkdwn", "text": f"*Customer*\n{info['customer_name']}"},
                {"type": "mrkdwn", "text": f"*Channel*\n{info['channel']}"},
                {"type": "mrkdwn", "text": f"*Time Remaining*\n{status}"},
                {"type": "mrkdwn", "text": f"*SLA Target*\n{info['sla_minutes']} min"},
                {"type": "mrkdwn", "text": f"*Subject*\n{info['subject']}"},
            ],
        },
        {
            "type": "actions",
            "elements": [
                {
                    "type": "button",
                    "text": {"type": "plain_text", "text": "Open in Gorgias"},
                    "url": ticket_url,
                    "style": "danger",
                }
            ],
        },
    ]
 
    slack.chat_postMessage(
        channel=SLACK_CHANNEL,
        text=f"SLA warning: {info['subject']}{status}",
        blocks=blocks,
    )
 
 
def main() -> None:
    print("Checking open tickets for SLA risk...")
    tickets = get_open_tickets()
    print(f"Found {len(tickets)} open ticket(s)\n")
 
    # Clean up stale entries (older than 24 hours)
    now_ts = datetime.now(timezone.utc).timestamp()
    for tid in list(alerted.keys()):
        if now_ts - alerted[tid] > 86400:
            del alerted[tid]
 
    alerts_sent = 0
    for ticket in tickets:
        info = calculate_sla_status(ticket)
        ticket_id = str(ticket["id"])
 
        if info is None:
            continue
 
        status_label = "BREACHED" if info["breached"] else f"{info['remaining_minutes']}m left"
        print(f"  #{ticket['id']}  {info['subject'][:50]!r}  {status_label}")
 
        if ticket_id not in alerted:
            post_slack_alert(info)
            alerted[ticket_id] = now_ts
            alerts_sent += 1
            print(f"    -> Slack alert sent")
        else:
            print(f"    -> Already alerted (skipping)")
 
    # Persist alerted set
    SEEN_FILE.write_text(json.dumps(alerted))
    print(f"\nDone. {alerts_sent} alert(s) sent.")
 
 
if __name__ == "__main__":
    main()
Wall-clock time vs. business hours

This script calculates elapsed time using wall-clock hours. If your SLA counts only business hours, you'll need to add logic to subtract non-business hours from the elapsed duration. A simple approach: define your business hours (e.g., 9am-6pm Mon-Fri) and only count minutes within those windows.

Step 4: Run the skill

# Via Claude Code
/sla-alert
 
# Or directly
python .claude/skills/sla-alert/scripts/check_sla.py

A typical run:

Checking open tickets for SLA risk...
Found 34 open ticket(s)
 
  #18201  'Shipping delay on order #4892'    42m left
    -> Slack alert sent
  #18195  'Wrong item received'              BREACHED
    -> Slack alert sent
  #18210  'Return label request'             15m left
    -> Already alerted (skipping)
 
Done. 2 alert(s) sent.

Step 5: Schedule it

# crontab -e — run every 15 minutes during business hours
*/15 8-18 * * 1-5 cd /path/to/project && python .claude/skills/sla-alert/scripts/check_sla.py

For 24/7 SLA monitoring (e.g., chat support), remove the hour restriction:

*/15 * * * * cd /path/to/project && python .claude/skills/sla-alert/scripts/check_sla.py
Adjust SLA_TARGETS to match your policy

The default targets in the script (4 hours for email, 15 minutes for chat) are starting points. Update the SLA_TARGETS dictionary to match your actual SLA commitments. If you have per-priority targets, extend the lookup to check ticket.priority as well.

Step 6: Customize for multiple SLA tiers

If different customer segments have different SLA targets, update the calculate_sla_status function to check for customer tags:

# Example: VIP customers get tighter SLAs
VIP_SLA_TARGETS = {
    "email": 60,     # 1 hour
    "chat": 5,       # 5 minutes
    "default": 60,
}
 
def get_sla_targets(ticket: dict) -> dict:
    customer_tags = (ticket.get("requester") or {}).get("meta", {}).get("tags", [])
    is_vip = any(t.get("name") == "vip" for t in customer_tags)
    return VIP_SLA_TARGETS if is_vip else SLA_TARGETS

Cost

  • No Claude API calls — uses Gorgias and Slack APIs directly
  • Slack SDK: free
  • Gorgias API: included in your plan
  • Server cost for cron: negligible (runs for a few seconds every 15 minutes)

Need help implementing this?

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