Service Desk SLA Triage
A worked example of an advanced workflow: a Claude-managed scheduled agent that, every hour, finds HaloPSA tickets that are breaching or at risk of breaching SLA (plus unassigned aging tickets), skips anything it already flagged this cycle, ranks the rest by urgency, and posts a prioritised triage digest to Slack so the service desk sees what needs attention before SLAs breach. There are no servers and no code to deploy: the agent is a saved prompt plus a schedule plus two MCP connectors, running in Claude's cloud.
What it builds
The finished workflow runs unattended on an hourly cron. Each run it queries open HaloPSA tickets, classifies them as breached, at risk, or fine, skips any ticket it has already surfaced this cycle (using a per-ticket triage note as the guard), ranks the remainder by urgency, and delivers a canvas-backed digest to Slack — turning a reactive inbox into a proactive SLA watch. Writes are bounded: the agent adds a private triage note and posts to Slack; it never reassigns, closes, or changes a ticket's status.
Prerequisites
Before building, your Claude account needs all of the following:
| Requirement | Notes |
|---|---|
| WYRE MCP Gateway connector | Connected in claude.ai (https://mcp.wyre.ai/v1/mcp). Provides the halopsa-official tools. See the Gateway overview. |
HaloPSA enabled in the gateway (halopsa-official) | The gateway's HaloPSA API user must be able to read tickets and write ticket notes. This guide uses the halopsa-official connector (the first-party MCP) — not the separate halopsa connector. |
| Slack connector | The first-party Slack connector connected in claude.ai (https://mcp.slack.com/mcp). Provides slack_create_canvas and slack_send_message. |
| Scheduled routines access | Created via the /schedule capability; managed at claude.ai/code/routines. |
| A destination Slack channel | e.g. #support-sla, plus its channel ID. The Slack connector must have access to it. |
Known gotchas
These are the things that cost real time the first time through. Account for them up front and the build genuinely takes minutes.
- The idempotency guard is not optional for any hourly triage routine.
Without a per-ticket marker, every hourly run re-flags the same breaching tickets
and spams the channel with duplicates. The triage note (the cycle marker
[sla-triage YYYY-MM-DD-HH]) is the guard: a rerun reads existing notes and skips any ticket already marked this cycle. This is the same archetype-B move as the Autotask Ticket Triage agent — the triage note here plays the same role that advancing a ticket to In Progress plays there. - Bounded writes — the routine should never make an irreversible service-desk change.
This agent writes a private triage note and posts to Slack; it does not reassign
tickets, change status, or alter priority. Keep unattended routines bounded: a human
reviews the digest and acts. Authorising status or assignment changes in
permitted_toolswould let an hourly routine silently modify the queue without oversight. - Two HaloPSA connectors exist in this ecosystem — attach the right one.
The gateway exposes both a
halopsaconnector and ahalopsa-official(first-party MCP) connector. This guide useshalopsa-officialand its tools (search_tickets,get_one_ticket,add_note_to_ticket). Attachinghalopsainstead will give you a different tool set and the routine will not work as written. -
permitted_toolsmust be populated per connector. A routine with a connector attached but an emptypermitted_toolslist runs with no tools and silently does nothing — no error, no output. List the exact tool names the routine needs. - A routine reaches only its attached connectors. The routine sandbox blocks arbitrary network egress, so notifications must go through the Slack connector's tools, not an outbound webhook. Attach every connector the workflow touches.
- One-hour minimum cadence. Claude-managed routines reject any cron
faster than hourly —
*/15 * * * *fails outright. Cron runs in UTC: to fire at 09:00 America/New_York (UTC−4 in summer), use0 13 * * *. - If Slack doesn't appear in the
/scheduleconnector list, read its details from an existing routine. Don't conclude Slack is unsupported. Read itsconnector_uuidandurlfrom an existing routine that already uses Slack (RemoteTriggerlist → get →mcp_connections). - Prefer
search_ticketsover per-record gets. The 60-second tool timeout makes a loop ofget_one_ticketcalls across a large queue risky. Pull as much as possible from thesearch_ticketspayload and callget_one_ticketonly when necessary detail is missing. If SLA fields are not surfaced bysearch_tickets, useCF_Portal_SLA_BreachedorCF_Portal_Unassigned_Ticketsas supplementary discovery tools.
The one-shot build prompt
With the connectors above in place, paste this to Claude. It confirms the gateway, creates the routine, and verifies the idempotency guard end to end.
Build me a scheduled Service Desk SLA Triage agent using HaloPSA. Do all of this end to end:
1. Confirm the WYRE MCP Gateway works and HaloPSA is reachable: call
search_tickets (halopsa-official connector) and verify it returns open
tickets. Note: there are two HaloPSA gateway connectors in this ecosystem
(halopsa and halopsa-official). This routine uses halopsa-official — the
first-party MCP — so attach that one, not the other.
2. Confirm a Slack connector is connected. Note the destination channel name
and ID (e.g. #support-sla). If Slack does not show in the /schedule
connector list, read its connector_uuid and url from an existing routine
that already uses Slack (RemoteTrigger list -> get -> mcp_connections).
3. Create a Claude-managed scheduled routine named "Service Desk SLA Triage":
- Schedule: hourly (cron "0 * * * *"). Faster-than-hourly cadences are
rejected. Cron runs in UTC — if your team works America/New_York,
"0 13 * * *" is 09:00 ET, for example.
- Attach TWO connectors, each with permitted_tools populated:
* WYRE MCP Gateway (halopsa-official): search_tickets, get_one_ticket,
add_note_to_ticket
* Slack: slack_create_canvas, slack_send_message
An empty permitted_tools list = the routine runs with no tools.
- Routine logic (see the routine prompt below): every run, find open tickets
that are breaching SLA or within the risk threshold; skip any ticket that
already has this cycle's triage note (the idempotency guard); rank the
rest by urgency; add the triage-note guard to each newly surfaced ticket;
build a prioritised digest; post a Slack canvas with the full triage list
and a one-line summary linking it. Do NOT change ticket status or
assignment — writes are bounded to the triage note and Slack only.
4. Trigger a manual run and verify: a canvas was created, the triage note was
added to each flagged ticket, a one-line summary landed in the destination
channel, and triggering a second run immediately does NOT re-flag the same
tickets (the note guard prevents it).
Alternative discovery: if search_tickets does not surface SLA-breaching tickets
directly, the halopsa-official connector also exposes CF_Portal_SLA_Breached
and CF_Portal_Unassigned_Tickets as report-style tools — use those as a
supplementary discovery path if needed. The resulting routine prompt
This is the lean prompt the build process installs into the scheduled routine itself. Substitute your destination Slack channel name and ID.
You are the Service Desk SLA Triage agent. You run once per hour. Keep it lean.
Use ONLY: search_tickets, get_one_ticket, add_note_to_ticket,
slack_create_canvas, slack_send_message.
Definitions:
- BREACHED: ticket's SLA response or resolution deadline has already passed.
- AT RISK: ticket is within 60 minutes of its SLA deadline (or unassigned and
open for more than 2 hours — these are at risk by default).
- CYCLE MARKER: a triage note whose text starts with "[sla-triage YYYY-MM-DD-HH]"
where YYYY-MM-DD-HH is the current UTC date and hour (e.g. [sla-triage 2026-05-27-14]).
A ticket that already carries this exact marker was surfaced in this cycle;
skip it — do not re-flag, do not post it again.
Steps:
1. search_tickets: retrieve all open tickets. Request enough fields to determine
SLA status and whether a cycle marker note already exists for this hour.
2. For each open ticket:
a. Classify as BREACHED, AT RISK, or neither. Tickets that are neither — skip.
b. Check for a cycle marker note matching today's UTC date-hour. If present — skip.
c. For tickets still in scope, call get_one_ticket only if you need additional
detail not present in the search payload (minimise per-record calls).
3. For each newly surfaced ticket (not skipped):
a. add_note_to_ticket: post a private triage note. The note MUST begin with
the cycle marker, e.g.:
[sla-triage 2026-05-27-14] SLA triage: ticket flagged as BREACHED/AT RISK.
No action required — this note is an automated idempotency marker.
b. Record the ticket for the digest.
4. Sort the digest: BREACHED tickets first (oldest deadline first), then AT RISK
(soonest deadline first), then unassigned aging tickets.
5. slack_create_canvas titled "SLA Triage - <today's date> <current UTC hour>:00 UTC"
with the full ranked list: ticket number, subject, status, SLA deadline,
classification (BREACHED / AT RISK / UNASSIGNED), and time remaining or
overdue by.
6. slack_send_message to #support-sla: a one-line summary
("SLA triage: N breached, M at risk — see canvas") linking the canvas.
7. If no tickets are in scope after filtering, post nothing (do not send an
empty canvas).
8. If a ticket cannot be read or the triage note cannot be written, list it in
a "could not check" section of the canvas instead of skipping it silently.
Bounded writes: add triage notes and post to Slack only. Do NOT change ticket
status, priority, assignment, or any other ticket field. How it works
Hourly cadence and why
SLA deadlines are typically measured in hours, not days — a two-hour response target can breach between a weekly or even daily run. Hourly is the fastest cadence Claude-managed routines support, which makes it the natural fit for SLA watch. The idempotency guard (below) ensures that running hourly doesn't flood the channel: a ticket is surfaced at most once per UTC hour, regardless of how many runs execute.
At-risk vs. breached classification
The routine draws a line between two states. A ticket is breached when its SLA deadline has already passed; it is at risk when the deadline is within 60 minutes, or when it is unassigned and has been open for more than two hours. Everything else is skipped. The two-tier classification lets the digest lead with the most urgent items (breached, oldest first) and trail with the approaching ones (at risk, soonest deadline first), giving the service desk a clear action order at a glance.
The triage-note idempotency guard — the archetype-B move
Before posting any ticket to Slack, the agent writes a private triage note whose
text begins with a stable, greppable marker: [sla-triage YYYY-MM-DD-HH].
At the start of every run the agent reads existing notes on each candidate ticket
and skips any that already carry this hour's marker. The marker is the "already
handled" signal — no database, no timestamp window, no separate state store. This is
the same structural move the
Autotask Ticket Triage
agent makes by advancing a ticket to In Progress: both routines embed the
idempotency guard into the primary data source so reruns are self-correcting.
Bounded writes — the human stays in control
The only writes this routine makes are a private triage note (the guard) and a Slack message. It never changes a ticket's status, priority, or assignment. That constraint is deliberate: an unattended hourly routine that can silently reassign or close tickets is a liability. The digest gives the service desk the full picture; remediation stays human. If a ticket cannot be read or the note cannot be written, the routine lists it in a "could not check" section of the canvas rather than skipping it silently.
Canvas plus summary delivery
The triage digest can span many tickets, so the routine publishes it as a Slack canvas — a durable, scrollable document — and posts only a one-line count summary to the channel linking it. The channel stays readable across multiple hourly runs; the detail lives in each canvas. See delivery adapters for other ways to surface a routine's output.
Extending it
The routine is PSA-agnostic in structure. To run the same SLA triage against
Autotask, swap the halopsa-official tools (search_tickets,
get_one_ticket, add_note_to_ticket) for the gateway's
autotask_search_tickets, autotask_get_ticket_details, and
autotask_create_ticket_note equivalents — the idempotency pattern, the
at-risk classification, and the Slack delivery are identical. See the
Autotask Ticket Triage
guide for that connector's setup. Alternatively, to escalate a breached ticket
beyond a Slack digest — for example, opening a PagerDuty incident or an internal
escalation ticket — add a third connector to the routine and extend the bounded-write
step; the core classification and guard logic does not change.
Questions or a workflow you'd like documented?
Open an issue
in the msp-claude-plugins repository.