Auto-close stale Gorgias tickets using an agent skill
Prerequisites
- Claude Code, Cursor, or another AI coding agent that supports skills
GORGIAS_EMAIL,GORGIAS_API_KEY,GORGIAS_DOMAINenvironment variables- Python 3.9+ with
requestsinstalled
Overview
This agent skill finds Gorgias tickets where an agent has replied but the customer hasn't responded within a configurable time window. It sends a polite follow-up message and, on the next run, closes tickets where the follow-up was already sent and the customer still hasn't replied. Unlike static Rules, the skill can apply custom logic — checking tags, ticket age, or even CRM data before deciding whether to close.
Step 1: Create the skill directory
mkdir -p .claude/skills/auto-close-stale/scriptsStep 2: Write the SKILL.md
Create .claude/skills/auto-close-stale/SKILL.md:
---
name: auto-close-stale
description: Finds Gorgias tickets with no customer reply after 48 hours and sends a follow-up message. Closes tickets where the follow-up was sent and 24 more hours have passed.
disable-model-invocation: true
allowed-tools: Bash(python *)
---
Close stale support tickets:
1. Run: `python $SKILL_DIR/scripts/close_stale.py`
2. Review the output — it shows follow-ups sent and tickets closed
3. Use `--dry-run` to preview without making changesStep 3: Write the close script
Create .claude/skills/auto-close-stale/scripts/close_stale.py:
#!/usr/bin/env python3
"""
Gorgias Auto-Close Stale Tickets
Sends follow-up messages to stale tickets, then closes them if no reply arrives.
"""
import os
import sys
from datetime import datetime, timezone
try:
import requests
except ImportError:
os.system("pip install requests -q")
import requests
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)
FOLLOW_UP_HOURS = int(os.environ.get("FOLLOW_UP_HOURS", "48"))
CLOSE_HOURS = int(os.environ.get("CLOSE_HOURS", "72"))
EXCLUDE_TAGS = {"vip", "escalated", "priority"}
DRY_RUN = "--dry-run" in sys.argv
FOLLOW_UP_MESSAGE = """Hi {name},
Just checking in — did our previous reply resolve your question? If you still need help, simply reply to this message and we'll pick things right back up.
If we don't hear from you, we'll close this ticket shortly to keep things tidy. You can always reach out again anytime.
Best,
The Support Team"""
def get_pending_tickets(limit: int = 50) -> list:
resp = requests.get(
f"{BASE_URL}/tickets",
auth=AUTH,
params={"status": "pending", "limit": limit},
)
resp.raise_for_status()
return resp.json().get("data", [])
def hours_since_update(ticket: dict) -> float:
updated = datetime.fromisoformat(
ticket["updated_datetime"].replace("Z", "+00:00")
)
return (datetime.now(timezone.utc) - updated).total_seconds() / 3600
def get_tags(ticket: dict) -> set:
return {(t.get("name") or t) for t in ticket.get("tags", [])}
def last_message_is_agent(ticket: dict) -> bool:
messages = ticket.get("messages", [])
if not messages:
return False
return messages[-1].get("source", {}).get("type") == "helpdesk"
def send_follow_up(ticket_id: int, customer_name: str) -> None:
body = FOLLOW_UP_MESSAGE.format(name=customer_name or "there")
requests.post(
f"{BASE_URL}/tickets/{ticket_id}/messages",
auth=AUTH,
json={
"body_text": body,
"channel": "email",
"from_agent": True,
"source": {
"type": "helpdesk",
"from": {
"name": "Support Team",
"address": f"support@{GORGIAS_DOMAIN}.com",
},
},
},
).raise_for_status()
# Tag to track that follow-up was sent
requests.put(
f"{BASE_URL}/tickets/{ticket_id}",
auth=AUTH,
json={"tags": [{"name": "auto-close-sent"}]},
).raise_for_status()
def close_ticket(ticket_id: int) -> None:
requests.put(
f"{BASE_URL}/tickets/{ticket_id}",
auth=AUTH,
json={
"status": "closed",
"tags": [{"name": "auto-closed"}],
},
).raise_for_status()
def main() -> None:
if DRY_RUN:
print("DRY RUN — no changes will be made\n")
print("Fetching pending tickets...")
tickets = get_pending_tickets()
print(f"Found {len(tickets)} pending ticket(s)\n")
follow_ups = 0
closes = 0
for ticket in tickets:
tags = get_tags(ticket)
# Skip excluded tickets
if tags & EXCLUDE_TAGS:
continue
# Only process tickets where the last message is from an agent
if not last_message_is_agent(ticket):
continue
hours = hours_since_update(ticket)
tid = ticket["id"]
subject = ticket.get("subject", "")[:50]
name = ticket.get("requester", {}).get("firstname", "")
if "auto-close-sent" not in tags and hours >= FOLLOW_UP_HOURS:
if DRY_RUN:
print(f" WOULD send follow-up: #{tid} {subject!r} ({hours:.0f}h)")
else:
send_follow_up(tid, name)
print(f" Sent follow-up: #{tid} {subject!r} ({hours:.0f}h)")
follow_ups += 1
elif "auto-close-sent" in tags and hours >= CLOSE_HOURS:
if DRY_RUN:
print(f" WOULD close: #{tid} {subject!r} ({hours:.0f}h)")
else:
close_ticket(tid)
print(f" Closed: #{tid} {subject!r} ({hours:.0f}h)")
closes += 1
print(f"\nDone. Follow-ups sent: {follow_ups}, Tickets closed: {closes}")
if __name__ == "__main__":
main()Always run with --dry-run the first time to preview which tickets would be affected. This prevents accidentally closing tickets that should stay open.
Step 4: Test the skill
# Preview what would happen (no changes made)
python .claude/skills/auto-close-stale/scripts/close_stale.py --dry-run
# Run for real
python .claude/skills/auto-close-stale/scripts/close_stale.py
# Or via Claude Code
/auto-close-staleStep 5: Customize the timing
Adjust the follow-up and close windows via environment variables:
# Send follow-up after 24 hours, close after 48 hours total
FOLLOW_UP_HOURS=24 CLOSE_HOURS=48 python .claude/skills/auto-close-stale/scripts/close_stale.pyIf FOLLOW_UP_HOURS is 48 and CLOSE_HOURS is 72, the follow-up goes out at 48 hours and the ticket closes at 72 hours (24 hours after the follow-up). Make sure CLOSE_HOURS is always greater than FOLLOW_UP_HOURS.
Step 6: Schedule with cron
# crontab -e — run every hour during business hours
0 * * * * cd /path/to/project && python .claude/skills/auto-close-stale/scripts/close_stale.py >> /tmp/auto-close.log 2>&1For 24/7 coverage, remove the hour restriction. The script is safe to run frequently since it only acts on tickets that have exceeded the time threshold.
Cost
- No AI model calls — this skill uses only the Gorgias REST API
- Gorgias API usage is included in all paid plans
- If running via cron on a server: minimal compute cost
Need help implementing this?
We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.