Runtime Connections
Governed Incident Response with Perplexity and PagerDuty
Introduction
Perplexity Sonar can research and propose answers. PagerDuty can alert and escalate. Neither product, by itself, produces a compliance-grade record of what the AI did, what sources it used, and who approved the next action. Loop Engine closes that gap: you model the flow as explicit states and transitions, attach evidence at each step, and only then hand off to PagerDuty when policy allows.
The architecture is linear: Perplexity Sonar supplies grounded text and citations; Loop Engine runs a finite-state loop (for example a governed-incident-response definition) with guards and human gates; PagerDuty receives Events API v2 triggers when the loop reaches an alerting state. The audit trail is written from the loop side — one entry per material step, with hashes and references you can tie back to incidents.
Architecture
1 ┌── audit trail (tamper-evident entries per step)2 │3Perplexity Sonar ──►│ Loop Engine — governed-incident-response FSM4 │5 └──► PagerDuty (Events API v2 — trigger / dedupe / custom_details)The governed-incident-response loop
The catalog id gov.governed-incident-response (version 0.1.0) uses the following state set and transitions. Your runtime should register the definition and drive transitions through the same actor boundaries.
States
1[2 "idle",3 "researching",4 "classifying",5 "auto_approving",6 "human_review",7 "escalating",8 "alerting",9 "complete",10 "rejected",11 "blocked",12 "failed"13]Key states (annotated)
| State | Role |
| --- | --- |
| researching | Sonar call with domain filter and citation requirement (unless you pre-seed citations and skip research). |
| classifying | Derive risk tier from Sonar output — typically LOW, HIGH, or BLOCKED. |
| human_review | Configurable gate; timeout paths can escalate through PagerDuty or move to escalating. |
| blocked | No PagerDuty alert; the audit trail records the block decision and rationale. |
| alerting | Build an Events API payload (see below) including audit_ref in custom_details. |
Full transition graph:
1{2 "transitions": [3 { "from": "idle", "to": "researching", "actor": "automation" },4 { "from": "researching", "to": "classifying", "actor": "automation" },5 { "from": "researching", "to": "failed", "actor": "automation" },6 { "from": "classifying", "to": "auto_approving", "actor": "automation" },7 { "from": "classifying", "to": "human_review", "actor": "automation" },8 { "from": "classifying", "to": "blocked", "actor": "human" },9 { "from": "auto_approving", "to": "alerting", "actor": "automation" },10 { "from": "human_review", "to": "alerting", "actor": "human" },11 { "from": "human_review", "to": "rejected", "actor": "human" },12 { "from": "human_review", "to": "escalating", "actor": "automation" },13 { "from": "escalating", "to": "human_review", "actor": "automation" },14 { "from": "escalating", "to": "failed", "actor": "automation" },15 { "from": "alerting", "to": "complete", "actor": "automation" },16 { "from": "alerting", "to": "failed", "actor": "automation" }17 ]18}Code walkthrough
1. Initialize adapters
You wire PerplexityAdapter to Sonar and implement a small PagerDuty port that posts to the Events API with your routing key.
1import type { AdapterInput } from "@loop-engine/core";2import type { PagerDutyEventPayload } from "@loop-engine/adapter-pagerduty";3import { PerplexityAdapter } from "@loop-engine/adapter-perplexity";4 5const perplexity = new PerplexityAdapter({6 apiKey: process.env.PERPLEXITY_API_KEY!,7 defaultModel: "sonar-pro",8 defaultSearchRecency: "month",9 timeout: 30_000,10 retries: 3,11});12 13async function sendPagerDutyTrigger(payload: PagerDutyEventPayload): Promise<{ dedupKey: string }> {14 const res = await fetch("https:">//events.pagerduty.com/v2/enqueue", {15 method: "POST",16 headers: { "Content-Type": "application/json" },17 body: JSON.stringify(payload),18 });19 if (!res.ok) {20 throw new Error(`PagerDuty enqueue failed: ${res.status}`);21 }22 const body = (await res.json()) as { dedup_key?: string };23 return { dedupKey: body.dedup_key ?? "" };24}25 26"cmt">// Your governed-incident module composes { perplexity, pagerduty, auditTrail } into the FSM above.2. Run one incident (pre-seeded citations)
When you already have authoritative citations, you skip the live Sonar call and still run classify → gate → alert. The exact function name lives in your governed-AI layer; the inputs and outputs below match the pattern we use in production demos.
1type Citation = { url: string; title: string; snippet?: string };2 3const result = await yourGovernedIncidentExecutor.run(4 {5 loopId: crypto.randomUUID(),6 perplexitySessionId: "session-abc",7 taskSummary: "PHI access outside approved hours",8 preSeedCitations: [9 { url: "https:">//www.hhs.gov/hipaa/for-professionals/breach-notification/index.html", title: "HIPAA Breach Notification" },10 ],11 tenantId: "your-tenant-id",12 riskSignal: "unusual_off_hours_phi_access",13 },14 {15 sonarModel: "sonar-pro",16 skipResearchIfCitationsProvided: true,17 euAiActLogging: true,18 hipaaIncidentLogging: true,19 },20 {21 perplexity,22 pagerduty: { sendTrigger: sendPagerDutyTrigger },23 auditTrail: yourAuditTrailWriter,24 },25);26 27"cmt">// result.state: 'complete' | 'awaiting_review' | 'blocked' | 'failed' | 'rejected'28"cmt">// result.auditRef: tamper-evident audit trail reference for the last persisted step3. PagerDuty custom_details
Include the audit reference so responders and downstream SIEM exports can correlate the incident with Loop evidence.
1const examplePayload: PagerDutyEventPayload = {2 routing_key: process.env.PAGERDUTY_INTEGRATION_KEY!,3 event_action: "trigger",4 dedup_key: "loop-your-loop-id",5 payload: {6 summary: "Governed incident: PHI access outside approved hours",7 severity: "critical",8 source: "loop-engine/governed-incident-response",9 custom_details: {10 loop_id: "8b7c…",11 audit_ref: "a1f2c3d4e5f67890",12 risk_classification: "HIGH",13 citation_count: 2,14 reviewer_id: "auto-approved",15 sonar_model: "sonar-pro",16 prompt_tokens: 1200,17 completion_tokens: 180,18 eu_ai_act_logging: true,19 },20 },21 links: [{ href: "https:">//www.hhs.gov/…", text: "HIPAA Breach Notification" }],22};Audit trail fields
Each row is what you persist per step (names align with common governed-AI implementations). Map them to your control framework columns in the same way you map any other security log schema.
| Field | Compliance mapping (typical) |
| --- | --- |
| loopId | Correlation id across systems; ties PagerDuty dedupe to Loop execution |
| tenantId | Tenant isolation evidence for multi-entity operators |
| stepName | Which FSM phase produced the row (researching, classifying, human_review, …) |
| timestamp | Ordering and retention policy scope |
| inputHash / outputHash | Integrity — detects tampering of serialized step input/output |
| auditRef | External reference surfaced in PagerDuty and executive reporting |
| perplexitySessionId | Session-level trace for Sonar usage |
| sonarModel | Model transparency (EU AI Act Art. 13 style disclosures) |
| sonarPromptTokens / sonarCompletionTokens | Usage metering and cost attribution |
| citationCount / citationUrls | Source provenance for verifiable AI outputs |
| riskClassification | Policy outcome (LOW / HIGH / BLOCKED) |
| classificationBasis | Textual rationale or structured reason code |
| humanInLoop | Whether a human gate applied |
| reviewerId / reviewDecision / reviewDurationMs | SOC 2 change / access review style evidence |
| pagerdutyIncidentId | Back-link from Loop to on-call ticket |
| euAiActLogging / hipaaIncidentLogging | Flags for jurisdiction-specific retention and DLP |
| error | Failure capture for failed transitions |
Environment variables
1PERPLEXITY_API_KEY=2PERPLEXITY_DEFAULT_MODEL=sonar-pro3PAGERDUTY_INTEGRATION_KEY=4LOOP_ENGINE_REGISTRY_URL= # loop catalog HTTP base URL (env name uses REGISTRY_ for compatibility)5LOOP_ENGINE_REGISTRY_ORG=6LOOP_ENGINE_JWT_SECRET=7LOOP_ENGINE_AUTH_ISSUER=