Developer Handover Pack
Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.
Developer Handover Pack
SLA Monitoring & Breach Alerting
[YourCompany.com] · Customer Support Department · Prepared by FullSpec · [Today's Date]
This document provides the complete technical specification for building the SLA Monitoring and Breach Alerting automation. It covers the current-state process map with bottleneck annotations, full agent specifications with IO contracts, the end-to-end data flow across both agents, and the recommended build stack with estimated effort. The FullSpec team uses this document to guide every build and configuration decision. No external developer or agency is involved.
01Process snapshot
02Agent specifications
Fires on a fixed 15-minute schedule and polls Zendesk for every open ticket. For each ticket it retrieves the linked client identifier and queries HubSpot to determine the contracted SLA tier, response window, and resolution window. It then calculates exact business-hours time elapsed since ticket creation and the time remaining against the contracted deadline, applying per-tier timezone-aware business-hours calendars. Each ticket exits this agent classified as safe (under 75% of window consumed), at-risk (75% to 94%), or critical (95% and above, or already breached). Estimated build time: 14 hours. Complexity: Moderate.
// Input
zendesk.tickets.list({ status: 'open' })
-> ticket_id: string
-> subject: string
-> created_at: ISO8601 timestamp
-> assignee_id: integer
-> organization_id: integer // used as client_id for HubSpot lookup
-> zendesk_custom_field: sla_tier_override (optional, string)
hubspot.companies.get({ id: client_id })
-> hs_object_id: string
-> sla_tier: string // e.g. 'Tier1', 'Tier2', 'Tier3'
-> sla_response_hours: integer
-> sla_resolution_hours: integer
-> business_hours_calendar: string // named calendar ID
-> client_timezone: string // IANA timezone e.g. 'America/New_York'
// Output
classified_ticket_list: array of {
ticket_id: string,
subject: string,
assignee_id: integer,
client_id: string,
client_name: string,
sla_tier: string,
sla_response_hours: integer,
sla_resolution_hours: integer,
business_hours_elapsed: float,
business_hours_remaining: float,
sla_pct_consumed: float, // 0.0 to 1.0+
urgency_flag: enum('safe', 'at_risk', 'critical', 'breached'),
alert_sent_75: boolean, // dedupe flag, read from Zendesk tag or state store
alert_sent_95: boolean // dedupe flag
}- Zendesk API tier must support organization field reads and custom field retrieval. Confirm the account is on Team plan or above before build starts; the Essential plan does not expose all custom fields via REST.
- The HubSpot SLA tier property must be named exactly 'sla_tier' on the Company object. Confirm property name and enumeration values with the client before building the lookup step. If the field is missing or blank the agent must fail gracefully and route the ticket to a fallback classification of 'at_risk' with a warning flag, never silently drop it.
- Business-hours calendar logic must account for public holidays per client timezone. Build a configurable holiday list as a static lookup table within the automation platform rather than relying on an external calendar API, to reduce external dependencies.
- Deduplification for the alert_sent_75 and alert_sent_95 flags must persist across poll cycles. Use a lightweight state store (a Supabase table or equivalent) keyed on ticket_id. Without this, the agent will resend alerts every 15 minutes for the same ticket.
- If the Zendesk poll returns more than 100 tickets, pagination must be handled explicitly using the Zendesk cursor-based pagination API. The agent must loop until next_page is null.
- Confirm the Zendesk organization_id field is consistently populated on all tickets. Tickets with a null organization_id cannot be matched to a HubSpot company and must be routed to the fallback path, not discarded.
Receives the classified ticket list from the SLA Monitor Agent after every poll cycle and routes graded outputs to five downstream systems based on each ticket's urgency flag and current alert state. For at-risk tickets (75% threshold crossed, alert not yet sent) it posts a structured Slack warning to the assigned agent's channel. For critical or breached tickets (95% threshold, escalation not yet fired) it raises a PagerDuty incident to the on-call support manager. For all tickets that triggered any alert it posts a timestamped internal note to the Zendesk ticket. For confirmed breaches it creates a breach activity record on the HubSpot company record. Every Monday morning at 08:00 it compiles the prior week's breach and near-breach data and delivers a formatted summary email via Gmail. Estimated build time: 14 hours. Complexity: Moderate.
// Input
classified_ticket_list: array (from SLA Monitor Agent output, see above)
filtered to tickets where urgency_flag IN ('at_risk', 'critical', 'breached')
and (alert_sent_75 == false OR alert_sent_95 == false)
// Output — per ticket, routed conditionally
// Slack (urgency_flag == 'at_risk' AND alert_sent_75 == false)
slack.chat.postMessage({
channel: assignee_slack_id, // resolved via Zendesk assignee_id -> Slack user map
text: '[SLA WARNING] Ticket #{{ticket_id}} — {{client_name}} ({{sla_tier}}) is at {{sla_pct_consumed}}% of SLA window. {{business_hours_remaining}} business hours remaining.',
blocks: structured_card_with_ticket_link
})
// PagerDuty (urgency_flag IN ('critical', 'breached') AND alert_sent_95 == false)
pagerduty.events.send({
routing_key: PAGERDUTY_INTEGRATION_KEY,
event_action: 'trigger',
dedup_key: 'sla-{{ticket_id}}-95',
payload: {
summary: 'SLA critical: Ticket #{{ticket_id}} for {{client_name}} at {{sla_pct_consumed}}%',
severity: 'critical',
source: 'SLA Monitor Automation',
custom_details: { ticket_id, client_id, sla_tier, business_hours_remaining }
}
})
// Zendesk internal note (all alerted tickets)
zendesk.tickets.update({
id: ticket_id,
comment: {
body: '[AUTOMATED] SLA alert sent at {{sla_pct_consumed}}% threshold. Notified: {{recipient}}. Timestamp: {{iso_timestamp}}.',
public: false
}
})
// HubSpot breach record (urgency_flag == 'breached')
hubspot.crm.objects.notes.create({
associations: [{ to: { id: hs_object_id }, type: 'company_to_note' }],
properties: {
hs_note_body: 'SLA breach: Ticket #{{ticket_id}}, Tier {{sla_tier}}, {{minutes_overrun}} min overrun. Breached at {{breach_timestamp}}.',
hs_timestamp: breach_timestamp
}
})
// Gmail weekly summary (Monday 08:00 schedule branch)
gmail.send({
to: MANAGER_EMAIL,
subject: 'Weekly SLA Report — week ending {{week_end_date}}',
body: compiled_html_summary // breach count, near-breach count, tier breakdown, ticket list
})- PagerDuty escalation policy and on-call schedule must be created and tested in PagerDuty before this agent is built. The PAGERDUTY_INTEGRATION_KEY must correspond to the correct service. Confirm the on-call rotation includes at least one override for holidays.
- Slack user ID mapping from Zendesk assignee_id requires a maintained lookup table (Zendesk agent ID to Slack user ID). Build this as a configurable map in the credential or config store. If a Slack ID is not found for an assignee, fall back to posting to a designated support-alerts channel rather than failing silently.
- The PagerDuty dedup_key pattern 'sla-{{ticket_id}}-95' ensures that repeated poll cycles do not create duplicate PagerDuty incidents for the same ticket. Confirm PagerDuty deduplication behaviour in the target account tier (Professional or above required for full dedup support).
- Gmail send must use OAuth 2.0 with the https://www.googleapis.com/auth/gmail.send scope only. Do not request broader Gmail scopes. The sending account must be a shared service account, not a personal inbox.
- The weekly Gmail report data source is the Supabase breach log table populated during the week, not a live Zendesk query. Confirm the table schema is finalised before building this branch.
- HubSpot breach notes must be associated to the Company object, not the Contact object. Confirm the correct object type with the client's HubSpot admin before build. If the client uses Deals to track SLA contracts, an additional association may be required.
- Confirm PagerDuty account tier supports the Events API v2 endpoint. Some legacy PagerDuty accounts are on v1 only and require a different payload structure.
03End-to-end data flow
// ============================================================
// SLA MONITORING & BREACH ALERTING — END-TO-END DATA FLOW
// ============================================================
// TRIGGER: Automation platform scheduler fires every 15 minutes
// ____________________________________________________________
SCHEDULER.fire({ interval: '15m', timezone: 'UTC' })
-> initiates: SLA_MONITOR_AGENT run
// ============================================================
// AGENT 1: SLA MONITOR AGENT
// ============================================================
// Step 1: Fetch all open tickets from Zendesk
ZENDESK.GET /api/v2/tickets.json?status=open&per_page=100
-> paginate via next_page cursor until null
-> extract per ticket: {
ticket_id, // string, Zendesk ticket ID
subject, // string
created_at, // ISO8601 UTC timestamp
assignee_id, // integer, Zendesk agent ID
organization_id, // integer, maps to HubSpot company
custom_field[sla_tier_override] // string or null
}
// Step 2: For each ticket, fetch SLA tier from HubSpot
// Keyed on: organization_id -> HubSpot company hs_object_id
HUBSPOT.GET /crm/v3/objects/companies/{hs_object_id}
?properties=sla_tier,sla_response_hours,sla_resolution_hours,
business_hours_calendar,client_timezone
-> extract: {
sla_tier, // string: 'Tier1' | 'Tier2' | 'Tier3'
sla_response_hours, // integer: e.g. 4 | 8 | 24
sla_resolution_hours, // integer: e.g. 8 | 24 | 72
business_hours_calendar,// string: named calendar ID
client_timezone // string: IANA e.g. 'America/Chicago'
}
// FALLBACK: if organization_id is null or HubSpot returns 404,
// classify ticket as urgency_flag='at_risk', log warning to Supabase.
// Step 3: Calculate business-hours time remaining
// Inputs: created_at, sla_response_hours, business_hours_calendar, client_timezone
CALC.businessHoursElapsed({
start: created_at,
now: SCHEDULER.fired_at,
calendar: business_hours_calendar, // weekday start/end, holiday list
timezone: client_timezone
})
-> business_hours_elapsed: float // e.g. 3.5
-> business_hours_remaining: float // sla_response_hours - elapsed
-> sla_pct_consumed: float // elapsed / sla_response_hours
// Step 4: Classify urgency
IF sla_pct_consumed < 0.75 -> urgency_flag = 'safe'
IF sla_pct_consumed >= 0.75 AND < 0.95 -> urgency_flag = 'at_risk'
IF sla_pct_consumed >= 0.95 AND <= 1.0 -> urgency_flag = 'critical'
IF sla_pct_consumed > 1.0 -> urgency_flag = 'breached'
// Step 5: Read dedupe flags from Supabase state store
SUPABASE.SELECT alert_sent_75, alert_sent_95
FROM sla_alert_state WHERE ticket_id = ticket_id
-> alert_sent_75: boolean
-> alert_sent_95: boolean
// AGENT HANDOFF: SLA Monitor Agent -> Alert Dispatch Agent
// Payload: classified_ticket_list (full array, safe tickets excluded)
// Only tickets where urgency_flag != 'safe' are forwarded
// ============================================================
// AGENT 2: ALERT DISPATCH AGENT
// ============================================================
// Branch A: Slack warning (at_risk, alert_sent_75 == false)
SLACK.POST /api/chat.postMessage
-> channel: assignee_slack_id // from Zendesk_to_Slack lookup map
-> text: '[SLA WARNING] Ticket #{{ticket_id}} — {{client_name}}'
+ ' ({{sla_tier}}) at {{sla_pct_consumed}}%.'
+ ' {{business_hours_remaining}} business hours remaining.'
-> blocks: structured card with ticket deep link
// On success: SUPABASE.UPDATE sla_alert_state SET alert_sent_75=true
// WHERE ticket_id = ticket_id
// Fallback: if assignee_slack_id not found, post to #support-alerts channel
// Branch B: PagerDuty escalation (critical or breached, alert_sent_95 == false)
PAGERDUTY.POST /v2/enqueue
-> routing_key: PAGERDUTY_INTEGRATION_KEY
-> event_action: 'trigger'
-> dedup_key: 'sla-{{ticket_id}}-95'
-> payload.summary: 'SLA critical: Ticket #{{ticket_id}} {{sla_pct_consumed}}%'
-> payload.severity: 'critical'
-> payload.custom_details: { ticket_id, client_id, sla_tier,
business_hours_remaining, assignee_id }
// On success: SUPABASE.UPDATE sla_alert_state SET alert_sent_95=true
// WHERE ticket_id = ticket_id
// Branch C: Zendesk internal note (all alerted tickets)
ZENDESK.PUT /api/v2/tickets/{{ticket_id}}.json
-> comment.body: '[AUTOMATED] SLA alert fired at {{sla_pct_consumed}}%.'
+ ' Notified: {{recipient}}. At: {{iso_timestamp}}.'
-> comment.public: false
// Branch D: HubSpot breach record (urgency_flag == 'breached')
HUBSPOT.POST /crm/v3/objects/notes
-> properties.hs_note_body: 'SLA breach: Ticket #{{ticket_id}},'
+ ' Tier {{sla_tier}},'
+ ' {{minutes_overrun}} min overrun.'
-> properties.hs_timestamp: breach_timestamp
-> associations: [{ to: hs_object_id, type: 'company_to_note' }]
// Branch E: Gmail weekly summary (Monday 08:00 UTC schedule branch)
// Data source: Supabase sla_breach_log table, prior 7 days
SUPABASE.SELECT ticket_id, client_name, sla_tier, minutes_overrun,
breach_timestamp, near_breach_flag
FROM sla_breach_log
WHERE breach_timestamp >= NOW() - INTERVAL '7 days'
-> compile: breach_count, near_breach_count, tier_breakdown, ticket_list
GMAIL.POST /gmail/v1/users/me/messages/send
-> to: MANAGER_EMAIL
-> subject: 'Weekly SLA Report — week ending {{week_end_date}}'
-> body: compiled_html_summary (breach_count, near_breach_count,
tier_breakdown table, full ticket list with client names)
// ============================================================
// END OF FLOW — cycle repeats on next 15-minute scheduler tick
// ============================================================04Recommended build stack
More documents for this process
Every document generated for SLA Monitoring & Breach Alerting.