Alert your support lead in Slack when Gorgias tickets approach SLA deadline using n8n

medium complexityCost: $0-24/moRecommended

Prerequisites

Prerequisites
  • n8n instance (cloud or self-hosted)
  • Gorgias account with REST API access and API credentials (email + API key)
  • Slack app with Bot Token and chat:write scope
  • A Slack channel for SLA warnings (e.g., #support-sla-warnings)
  • Your SLA targets defined: first-response time per channel or priority level

Overview

This workflow runs on a schedule (every 15 minutes), fetches all open Gorgias tickets that haven't received a first response, calculates how much SLA time remains for each, and posts a Slack alert for any ticket that has crossed your warning threshold. The lead gets actionable context — ticket subject, customer name, time remaining, and a direct link — so they can reassign or escalate before the SLA actually breaches.

Step 1: Create the Schedule trigger

Add a Schedule Trigger node:

  • Interval: Every 15 minutes
  • Timezone: Your support team's primary timezone

15 minutes is a good default. For aggressive SLAs (under 1 hour), consider running every 5 minutes.

Why scheduled polling instead of webhooks?

Gorgias webhooks fire on ticket creation and updates, but not on SLA deadlines approaching. Since SLA risk is a function of elapsed time (not a discrete event), polling on a schedule is the right pattern.

Step 2: Fetch open tickets awaiting first response

Add an HTTP Request node:

  • Method: GET
  • URL: https://{{ $vars.GORGIAS_DOMAIN }}.gorgias.com/api/tickets
  • Authentication: Basic Auth (Gorgias email + API key)
  • Query Parameters:
    • status: open
    • limit: 100

Then add a Code node to filter to tickets with no agent reply:

const tickets = $input.first().json.data || [];
const noReply = tickets.filter(t => {
  const messages = t.messages || [];
  // Check if any message is from an agent (not the customer)
  const hasAgentReply = messages.some(
    m => m.source?.type === 'internal' || m.sender?.type === 'agent'
  );
  return !hasAgentReply;
});
 
return noReply.map(t => ({ json: t }));

Step 3: Calculate SLA time remaining

Add a Code node to compute how much time each ticket has left:

// Define SLA targets in minutes per channel
const SLA_TARGETS = {
  email: 240,    // 4 hours
  chat: 15,      // 15 minutes
  contact_form: 240,
  default: 240,
};
 
// Warning threshold: alert when this percentage of SLA has elapsed
const WARN_THRESHOLD = 0.75;
 
const now = new Date();
const results = [];
 
for (const item of $input.all()) {
  const ticket = item.json;
  const channel = ticket.channel || 'default';
  const slaMinutes = SLA_TARGETS[channel] || SLA_TARGETS.default;
 
  const created = new Date(ticket.created_datetime);
  const elapsedMs = now - created;
  const elapsedMinutes = elapsedMs / 60000;
 
  const remainingMinutes = slaMinutes - elapsedMinutes;
  const percentElapsed = elapsedMinutes / slaMinutes;
 
  if (percentElapsed >= WARN_THRESHOLD) {
    results.push({
      json: {
        ticketId: ticket.id,
        subject: ticket.subject || '(no subject)',
        customerName: ticket.requester?.name || ticket.requester?.email || 'Unknown',
        customerEmail: ticket.requester?.email || '',
        channel,
        slaMinutes,
        elapsedMinutes: Math.round(elapsedMinutes),
        remainingMinutes: Math.round(Math.max(remainingMinutes, 0)),
        breached: remainingMinutes <= 0,
        percentElapsed: Math.round(percentElapsed * 100),
      }
    });
  }
}
 
return results;
Business hours vs. calendar time

The calculation above uses wall-clock time. If your SLA policy counts only business hours, you'll need to adjust the elapsed time calculation to exclude nights and weekends. Add a helper function that subtracts non-business hours from the elapsed duration, or store your business-hours schedule in a config node.

Step 4: Deduplicate alerts

Add a Code node to avoid re-alerting on the same ticket within a short window. Use n8n's static data to track recently alerted ticket IDs:

const staticData = $getWorkflowStaticData('global');
if (!staticData.alertedTickets) {
  staticData.alertedTickets = {};
}
 
const now = Date.now();
const COOLDOWN_MS = 60 * 60 * 1000; // 1 hour cooldown
 
// Clean up old entries
for (const [id, timestamp] of Object.entries(staticData.alertedTickets)) {
  if (now - timestamp > COOLDOWN_MS) {
    delete staticData.alertedTickets[id];
  }
}
 
// Filter to tickets not recently alerted
const newAlerts = [];
for (const item of $input.all()) {
  const ticketId = String(item.json.ticketId);
  if (!staticData.alertedTickets[ticketId]) {
    staticData.alertedTickets[ticketId] = now;
    newAlerts.push(item);
  }
}
 
return newAlerts;

Step 5: Post the Slack alert

Add a Slack node:

  • Resource: Message
  • Operation: Send a Message
  • Channel: #support-sla-warnings
  • Message Type: Block Kit
{
  "blocks": [
    {
      "type": "header",
      "text": {
        "type": "plain_text",
        "text": "⏱️ SLA Warning — Ticket Approaching Deadline"
      }
    },
    {
      "type": "section",
      "fields": [
        {
          "type": "mrkdwn",
          "text": "*Customer*\n{{ $json.customerName }}"
        },
        {
          "type": "mrkdwn",
          "text": "*Channel*\n{{ $json.channel }}"
        },
        {
          "type": "mrkdwn",
          "text": "*Time Remaining*\n{{ $json.breached ? '❌ BREACHED' : $json.remainingMinutes + ' min' }}"
        },
        {
          "type": "mrkdwn",
          "text": "*SLA Target*\n{{ $json.slaMinutes }} min"
        }
      ]
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*Subject*\n{{ $json.subject }}"
      }
    },
    {
      "type": "actions",
      "elements": [
        {
          "type": "button",
          "text": { "type": "plain_text", "text": "Open in Gorgias" },
          "url": "https://{{ $vars.GORGIAS_DOMAIN }}.gorgias.com/app/ticket/{{ $json.ticketId }}",
          "style": "danger"
        }
      ]
    }
  ]
}

Step 6: Batch multiple warnings (optional)

If your queue regularly has multiple at-risk tickets, posting one message per ticket creates noise. Add a Code node before the Slack node to batch all warnings into a single message:

const items = $input.all();
if (items.length === 0) return [];
 
const lines = items.map(item => {
  const t = item.json;
  const status = t.breached ? '❌ BREACHED' : `⏱️ ${t.remainingMinutes} min left`;
  return `• *<https://your-store.gorgias.com/app/ticket/${t.ticketId}|#${t.ticketId}>* — ${t.subject} (${t.customerName}) — ${status}`;
});
 
return [{
  json: {
    summary: `${items.length} ticket(s) approaching SLA deadline`,
    details: lines.join('\n'),
  }
}];

Then update the Slack node to post {{ $json.summary }} as the header and {{ $json.details }} as the body.

Step 7: Activate and test

  1. Set one of your SLA targets temporarily low (e.g., 5 minutes) for testing
  2. Create a test ticket and wait for it to age past the warning threshold
  3. Verify the Slack alert appears with correct time-remaining data
  4. Reset your SLA targets and toggle the workflow to Active
Monitor your alert volume for the first week

If you're seeing more than 10 SLA warnings per day, either your SLA targets are too tight for your current staffing level or your warning threshold is too aggressive. Adjust the WARN_THRESHOLD from 0.75 to 0.85 to reduce noise while still giving your team lead time.

Cost

  • n8n Cloud: ~6 node executions per poll (every 15 minutes = ~576 executions/day). Well within the free tier for light usage; small teams will stay under the Starter plan limits.
  • Self-hosted: Free.

Need help implementing this?

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