id: linear-customer-support-escalation-sentinel
namespace: company.operations
inputs:
- id: team_key
type: STRING
displayName: Linear Team Key
defaults: "ENG"
description: Team identifier key in Linear (e.g., `ENG`, `PROD`, `SUP`).
- id: escalation_label
type: STRING
displayName: Escalation Label Name
defaults: "Customer Escalation"
description: Label applied to issues escalated from customer support or VIP accounts.
- id: max_inactivity_days
type: INT
displayName: Maximum Inactivity Days
defaults: 5
description: Number of days without updates before an escalated issue is flagged
as stalled.
- id: issue_batch_limit
type: INT
displayName: Issue Batch Limit
defaults: 50
description: Maximum number of active escalated issues to inspect per execution.
triggers:
- id: daily_escalation_audit_schedule
type: io.kestra.plugin.core.trigger.Schedule
description: Daily audit at 08:30 UTC to highlight stalled customer escalations
before standup. Shipped disabled by default.
cron: "30 8 * * 1-5"
disabled: true
tasks:
- id: fetch_escalated_issues
type: io.kestra.plugin.graphql.Request
description: Query Linear GraphQL API for active issues with the customer
escalation label.
uri: "https://api.linear.app/graphql"
headers:
Authorization: "{{ secret('LINEAR_API_KEY') }}"
Content-Type: "application/json"
User-Agent: "Kestra-Linear-Escalation-Sentinel/1.0"
failOnGraphQLErrors: false
query: |
query ($teamKey: String!, $labelName: String!, $limit: Int!) {
issues(
filter: {
team: { key: { eq: $teamKey } }
labels: { name: { eq: $labelName } }
state: { type: { nin: ["completed", "canceled"] } }
}
first: $limit
orderBy: updatedAt
) {
nodes {
id
identifier
title
url
priority
priorityLabel
createdAt
updatedAt
assignee {
name
displayName
}
state {
name
type
}
}
}
}
variables:
teamKey: "{{ inputs.team_key }}"
labelName: "{{ inputs.escalation_label }}"
limit: "{{ inputs.issue_batch_limit }}"
- id: evaluate_escalation_sla
type: io.kestra.plugin.scripts.python.Script
description: Calculate days since last update for each escalated ticket and flag
SLA breaches.
containerImage: python:3.11-slim
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
env:
RESPONSE_BODY: "{{ outputs.fetch_escalated_issues.body }}"
MAX_INACTIVITY_DAYS: "{{ inputs.max_inactivity_days }}"
TEAM_KEY: "{{ inputs.team_key }}"
LABEL_NAME: "{{ inputs.escalation_label }}"
script: |
import os
import json
from datetime import datetime, timezone
from kestra import Kestra
raw_data = os.environ.get("RESPONSE_BODY", "{}")
max_inactivity = int(os.environ.get("MAX_INACTIVITY_DAYS", 5))
team_key = os.environ.get("TEAM_KEY", "")
label_name = os.environ.get("LABEL_NAME", "")
try:
data = json.loads(raw_data)
except Exception:
data = {}
issues = data.get("data", {}).get("issues", {}).get("nodes", [])
now = datetime.now(timezone.utc)
stalled_issues = []
for issue in issues:
updated_at_str = issue.get("updatedAt")
if not updated_at_str:
continue
try:
updated_at = datetime.fromisoformat(updated_at_str.replace("Z", "+00:00"))
inactivity_days = (now - updated_at).days
except Exception:
inactivity_days = 0
if inactivity_days >= max_inactivity:
assignee = issue.get("assignee")
assignee_name = assignee.get("displayName") or assignee.get("name") if assignee else "Unassigned"
stalled_issues.append({
"identifier": issue.get("identifier"),
"title": issue.get("title"),
"url": issue.get("url"),
"priority": issue.get("priorityLabel", "No priority"),
"assignee": assignee_name,
"inactivity_days": inactivity_days,
"state": issue.get("state", {}).get("name", "Unknown")
})
summary = f"Found {len(stalled_issues)} open issue(s) tagged '{label_name}' in team '{team_key}' inactive for >={max_inactivity} days."
formatted_lines = [
f"• <{item['url']}|{item['identifier']}>: *{item['title']}* (Priority: `{item['priority']}`, Assignee: `{item['assignee']}`, Inactive: `{item['inactivity_days']}d`)"
for item in stalled_issues[:5]
]
Kestra.outputs({
"stalled_count": len(stalled_issues),
"summary": summary,
"stalled_issues": stalled_issues,
"details_formatted": "\n".join(formatted_lines)
})
- id: check_stalled_threshold
type: io.kestra.plugin.core.flow.If
description: Trigger Slack alert if any customer escalated issues exceed inactivity SLA.
condition: "{{ outputs.evaluate_escalation_sla.vars.stalled_count > 0 }}"
then:
- id: send_slack_escalation_alert
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Notify customer engineering and support teams in Slack regarding
inactive escalations.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"channel": "#customer-escalations",
"username": "Kestra Linear Escalation Watchdog",
"icon_emoji": ":fire_engine:",
"attachments": [
{
"color": "#e01e5a",
"title": "Stalled Linear Customer Escalations Detected",
"title_link": "https://linear.app",
"text": "{{ outputs.evaluate_escalation_sla.vars.summary }}",
"fields": [
{
"title": "Team",
"value": "`{{ inputs.team_key }}`",
"short": true
},
{
"title": "Tag Filter",
"value": "`{{ inputs.escalation_label }}`",
"short": true
},
{
"title": "Stalled Issues Count",
"value": "{{ outputs.evaluate_escalation_sla.vars.stalled_count }}",
"short": true
},
{
"title": "Allowed Inactivity Threshold",
"value": "{{ inputs.max_inactivity_days }} days",
"short": true
},
{
"title": "Escalated Issues Needing Attention",
"value": "{{ outputs.evaluate_escalation_sla.vars.details_formatted }}",
"short": false
}
],
"footer": "Kestra Support SLA Automation",
"ts": "{{ now() | date('X') }}"
}
]
}
errors:
- id: send_failure_alert
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Dispatch notification if Linear GraphQL query or SLA calculation
encounters an error.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"channel": "#devops-alerts",
"username": "Kestra Sentinel Watchdog",
"icon_emoji": ":rotating_light:",
"attachments": [
{
"color": "#dc3545",
"title": "Linear Customer Escalation Sentinel Failed",
"text": "Execution *{{ execution.id }}* failed during SLA evaluation for Linear team *{{ inputs.team_key }}*.",
"fields": [
{
"title": "Flow ID",
"value": "{{ flow.id }}",
"short": true
},
{
"title": "Namespace",
"value": "{{ flow.namespace }}",
"short": true
}
],
"footer": "Kestra Sentinel Watchdog",
"ts": "{{ now() | date('X') }}"
}
]
}