Skill: clawdbotworker/pagerduty-agent (version 1.0.0)

--- name: pagerduty-agent description: "Manage PagerDuty incidents, on-call schedules, services, and maintenance windows directly from your agent." version: 1.0.0 emoji: "🚨" homepage: https://github.com/openclaw/skills metadata: openclaw: requires: env: - PAGERDUTY_API_KEY bins: - node anyBins: [] primaryEnv: PAGERDUTY_API_KEY always: false tags: - pagerduty - incidents - oncall - devops - observability - alerting --- # PagerDuty Agent > ⚠️ This is an unofficial community skill and is not affiliated with or endorsed by PagerDuty, Inc. Trigger, acknowledge, and resolve PagerDuty incidents, check who's on-call, manage maintenance windows, and inspect services — all without leaving your agent workflow. ## Setup 1. Generate a PagerDuty API key: **PagerDuty → Integrations → API Access Keys → Create New API Key** 2. Export required environment variables: ```bash export PAGERDUTY_API_KEY="your-v2-api-key" export PAGERDUTY_FROM_EMAIL="[REDACTED_SECRET]" # required for write operations ``` > `PAGERDUTY_FROM_EMAIL` must be the email of a valid PagerDuty user in your > account. It is required by PagerDuty's REST API for any POST/PUT request. ## Usage All commands are invoked by piping a `{ command, params }` JSON object to `node pagerduty.js`. The skill returns structured JSON on stdout. ```bash echo '{"command":"list_incidents","params":{"status":"triggered"}}' | node pagerduty.js ``` --- ## Commands ### Incidents --- #### `trigger_incident` Create a new incident on a service. **Params** | Field | Type | Required | Description | |---|---|---|---| | `service_id` | string | ✓ | PagerDuty service ID (e.g. `"P1ABCDE"`) | | `title` | string | ✓ | Incident title / summary | | `severity` | string | | `"critical"`, `"error"`, `"warning"`, `"info"` — maps to urgency | | `body` | string | | Detailed description / runbook context | **Example** ```json { "command": "trigger_incident", "params": { "service_id": "P1ABCDE", "title": "Database replication lag > 60s", "severity": "critical", "body": "Replica db-02 is 90s behind primary. Check pg_stat_replication." } } ``` **Response** ```json { "id": "Q2W3E4R", "incident_number": 1042, "title": "Database replication lag > 60s", "status": "triggered", "urgency": "high", "html_url": "https://your-subdomain.pagerduty.com/incidents/Q2W3E4R", "created_at": "2026-03-04T12:00:00Z", "service": { "id": "P1ABCDE", "name": "Production Database" } } ``` --- #### `acknowledge_incident` Acknowledge an incident to signal it is being worked on. **Params** | Field | Type | Required | Description | |---|---|---|---| | `incident_id` | string | ✓ | PagerDuty incident ID | **Example** ```json { "command": "acknowledge_incident", "params": { "incident_id": "Q2W3E4R" } } ``` **Response** ```json { "id": "Q2W3E4R", "incident_number": 1042, "title": "Database replication lag > 60s", "status": "acknowledged", "acknowledged_at": "2026-03-04T12:03:00Z" } ``` --- #### `resolve_incident` Mark an incident as resolved. **Params** | Field | Type | Required | Description | |---|---|---|---| | `incident_id` | string | ✓ | PagerDuty incident ID | **Example** ```json { "command": "resolve_incident", "params": { "incident_id": "Q2W3E4R" } } ``` **Response** ```json { "id": "Q2W3E4R", "incident_number": 1042, "title": "Database replication lag > 60s", "status": "resolved", "resolved_at": "2026-03-04T12:45:00Z" } ``` --- #### `list_incidents` List incidents, optionally filtered by status. **Params** | Field | Type | Required | Description | |---|---|---|---| | `status` | string | | `"triggered"`, `"acknowledged"`, `"resolved"` | | `limit` | number | | Max results (default 25) | **Example** ```json { "command": "list_incidents", "params": { "status": "triggered", "limit": 10 } } ``` **Response** ```json { "total": 3, "limit": 10, "incidents": [ { "id": "Q2W3E4R", "incident_number": 1042, "title": "Database replication lag > 60s", "status": "triggered", "urgency": "high", "created_at": "2026-03-04T12:00:00Z", "service": { "id": "P1ABCDE", "name": "Production Database" }, "assigned_to": ["Alice Smith"] } ] } ``` --- #### `get_incident` Fetch full details for a single incident. **Params** | Field | Type | Required | Description | |---|---|---|---| | `incident_id` | string | ✓ | PagerDuty incident ID | **Example** ```json { "command": "get_incident", "params": { "incident_id": "Q2W3E4R" } } ``` **Response** ```json { "id": "Q2W3E4R", "incident_number": 1042, "title": "Database replication lag > 60s", "status": "acknowledged", "urgency": "high", "html_url": "https://your-subdomain.pagerduty.com/incidents/Q2W3E4R", "created_at": "2026-03-04T12:00:00Z", "last_status_change_at": "2026-03-04T12:03:00Z", "service": { "id": "P1ABCDE", "name": "Production Database" }, "assigned_to": ["Alice Smith"], "escalation_policy": { "id": "P9XYZ12", "name": "Eng On-Call" }, "body": "Replica db-02 is 90s behind primary. Check pg_stat_replication." } ``` --- #### `add_incident_note` Append a timestamped note to an incident's timeline. **Params** | Field | Type | Required | Description | |---|---|---|---| | `incident_id` | string | ✓ | PagerDuty incident ID | | `content` | string | ✓ | Note text (supports Markdown) | **Example** ```json { "command": "add_incident_note", "params": { "incident_id": "Q2W3E4R", "content": "Root cause identified: checkpoint completion time spiked to 95%. Increased max_wal_size and restarting standby." } } ``` **Response** ```json { "id": "NOTE123", "content": "Root cause identified...", "created_at": "2026-03-04T12:20:00Z", "user": "Alice Smith" } ``` --- ### On-Call & Schedules --- #### `get_oncall` List who is currently on-call, optionally scoped to a schedule. **Params** | Field | Type | Required | Description | |---|---|---|---| | `schedule_id` | string | | Filter to a specific schedule ID | **Example** ```json { "command": "get_oncall", "params": { "schedule_id": "SCHED01" } } ``` **Response** ```json { "oncalls": [ { "user": { "id": "UABC123", "name": "Alice Smith" }, "schedule": { "id": "SCHED01", "name": "Primary On-Call" }, "escalation_policy": { "id": "P9XYZ12", "name": "Eng On-Call" }, "escalation_level": 1, "start": "2026-03-04T08:00:00Z", "end": "2026-03-11T08:00:00Z" } ] } ``` --- #### `list_schedules` Return all on-call schedules in the account. **Params** — none **Example** ```json { "command": "list_schedules", "params": {} } ``` **Response** ```json { "total": 2, "schedules": [ { "id": "SCHED01", "name": "Primary On-Call", "description": "Weekly rotation — eng team", "time_zone": "America/New_York", "html_url": "https://your-subdomain.pagerduty.com/schedules/SCHED01", "users": [ { "id": "UABC123", "name": "Alice Smith" }, { "id": "UDEF456", "name": "Bob Jones" } ] } ] } ``` --- ### Services --- #### `list_services` Return all PagerDuty services. **Params** — none **Example** ```json { "command": "list_services", "params": {} } ``` **Response** ```json { "total": 3, "services": [ { "id": "P1ABCDE", "name": "Production Database", "description": "RDS Postgres cluster", "status": "active", "html_url": "https://your-subdomain.pagerduty.com/services/P1ABCDE", "escalation_policy": { "id": "P9XYZ12", "name": "Eng On-Call" } } ] } ``` --- #### `get_service` Fetch details for a single service including integrations. **Params** | Field | Type | Required | Description | |---|---|---|---| | `service_id` | string | ✓ | PagerDuty service ID | **Example** ```json { "command": "get_service", "params": { "service_id": "P1ABCDE" } } ``` **Response** ```json { "id": "P1ABCDE", "name": "Production Database", "description": "RDS Postgres cluster", "status": "active", "html_url": "https://your-subdomain.pagerduty.com/services/P1ABCDE", "created_at": "2024-01-15T09:00:00Z", "escalation_policy": { "id": "P9XYZ12", "name": "Eng On-Call" }, "integrations": [ { "id": "INT001", "name": "Datadog" } ], "alert_grouping": "intelligent", "alert_grouping_timeout": 300 } ``` --- ### Maintenance --- #### `create_maintenance_window` Put one or more services into maintenance mode to suppress alerts. **Params** | Field | Type | Required | Description | |---|---|---|---| | `service_ids` | string[] | ✓ | Array of service IDs to put in maintenance | | `start_time` | string | ✓ | ISO 8601 start time (e.g. `"2026-03-04T22:00:00Z"`) | | `end_time` | string | ✓ | ISO 8601 end time | | `description` | string | | Reason for maintenance | **Example** ```json { "command": "create_maintenance_window", "params": { "service_ids": ["P1ABCDE", "P2FGHIJ"], "start_time": "2026-03-04T22:00:00Z", "end_time": "2026-03-05T00:00:00Z", "description": "Scheduled DB migration — expect brief connectivity drops" } } ``` **Response** ```json { "id": "MW00123", "description": "Scheduled DB migration — expect brief connectivity drops", "start_time": "2026-03-04T22:00:00Z", "end_time": "2026-03-05T00:00:00Z", "html_url": "https://your-subdomain.pagerduty.com/maintenance_windows/MW00123", "services": [ { "id": "P1ABCDE", "name": "Production Database" }, { "id": "P2FGHIJ", "name": "API Gateway" } ] } ``` --- #### `list_maintenance_windows` Return all current and upcoming maintenance windows. **Params** — none **Example** ```json { "command": "list_maintenance_windows", "params": {} } ``` **Response** ```json { "total": 1, "maintenance_windows": [ { "id": "MW00123", "description": "Scheduled DB migration", "start_time": "2026-03-04T22:00:00Z", "end_time": "2026-03-05T00:00:00Z", "html_url": "https://your-subdomain.pagerduty.com/maintenance_windows/MW00123", "services": [ { "id": "P1ABCDE", "name": "Production Database" } ] } ] } ``` --- ## Error Responses All errors return a JSON object with a single `error` key: ```json { "error": "PAGERDUTY_API_KEY environment variable is not set" } { "error": "params.incident_id is required" } { "error": "Incident Not Found" } ``` ## Tips - Use `list_services` to discover service IDs before triggering incidents. - Use `list_schedules` to find schedule IDs before calling `get_oncall`. - `severity` values `"critical"` and `"error"` map to PagerDuty `urgency: high`; everything else maps to `urgency: low`. - Times in `create_maintenance_window` must be ISO 8601 UTC strings. - `PAGERDUTY_FROM_EMAIL` is required for all write operations (trigger, acknowledge, resolve, add note, create maintenance window).

path: CHANGELOG.md
content: ## [1.0.0] - 2026-03-04
- Initial launch


path: package.json
content: name: pagerduty-agent
version: 1.0.0
description: PagerDuty agent for AI workflows — trigger, acknowledge and resolve incidents, query on-call schedules, manage services and maintenance windows via REST API. Zero dependencies.
main: pagerduty.js
author: clawdbotworker
license: MIT

path: pagerduty.js
content: #!/usr/bin/env node
/**
 * pagerduty-agent — OpenClaw skill
 * Zero dependencies. Native Node.js https. { command, params } dispatch.
 * All responses are structured JSON.
 *
 * Usage:
 *   echo '{"command":"list_incidents","params":{"status":"triggered","limit":10}}' | node pagerduty.js
 */

"use strict";

const https = require("https");
const http = require("http");

const BASE_URL = process.env.PAGERDUTY_BASE_URL || "https://api.pagerduty.com";
const [REDACTED_SECRET];
const FROM_EMAIL = process.env.PAGERDUTY_FROM_EMAIL;

// ---------------------------------------------------------------------------
// HTTP client
// ---------------------------------------------------------------------------

function request(method, path, body) {
  return new Promise((resolve, reject) => {
    if (!API_KEY) {
      return reject(new Error("PAGERDUTY_API_KEY environment variable is not set"));
    }

    const payload = body ? JSON.stringify(body) : null;
    const url = new URL(BASE_URL + path);

    const headers = {
      "Authorization": `Token [REDACTED_SECRET]`,
      "Accept": "application/vnd.pagerduty+json;version=2",
      "Content-Type": "application/json",
    };

    if (FROM_EMAIL) {
      headers["From"] = FROM_EMAIL;
    }

    if (payload) {
      headers["Content-Length"] = Buffer.byteLength(payload);
    }

    const transport = url.protocol === "https:" ? https : http;
    const options = {
      hostname: url.hostname,
      port: url.port || (url.protocol === "https:" ? 443 : 80),
      path: url.pathname + url.search,
      method,
      headers,
    };

    const req = transport.request(options, (res) => {
      const chunks = [];
      res.on("data", (c) => chunks.push(c));
      res.on("end", () => {
        const raw = Buffer.concat(chunks).toString();
        let data = null;
        try { data = JSON.parse(raw); } catch (_) { data = raw; }

        if (res.statusCode >= 400) {
          const apiMsg =
            (data && data.error && data.error.message) ||
            (data && data.error) ||
            null;

          let msg;
          if (res.statusCode === 401) {
            msg = "Invalid API key — check your PAGERDUTY_API_KEY";
          } else if (res.statusCode === 403) {
            msg = "Permission denied — your API key does not have access to this resource";
          } else if (res.statusCode === 404) {
            // Derive a contextual hint from the request path
            const p = options.path.split("?")[0];
            if (p.match(/\/incidents\/[^/]+\/notes/)) {
              msg = "Incident not found — verify incident_id exists in your PagerDuty account";
            } else if (p.match(/\/incidents\/[^/]+/)) {
              msg = "Incident not found — verify incident_id exists in your PagerDuty account";
            } else if (p.match(/\/services\/[^/]+/)) {
              msg = "Service not found — verify service_id exists in your PagerDuty account";
            } else if (p.match(/\/schedules\/[^/]+/)) {
              msg = "Schedule not found — verify schedule_id exists in your PagerDuty account";
            } else if (p.match(/\/maintenance_windows\/[^/]+/)) {
              msg = "Maintenance window not found — verify the ID exists in your PagerDuty account";
            } else {
              msg = apiMsg || "Resource not found — verify the ID exists in your PagerDuty account";
            }
          } else if (res.statusCode === 429) {
            const retryAfter = res.headers["retry-after"];
            msg = retryAfter
              ? `Rate limit exceeded — retry after ${retryAfter}s`
              : "Rate limit exceeded — slow down requests or contact PagerDuty support";
          } else if (res.statusCode >= 500) {
            msg = `PagerDuty server error (HTTP ${res.statusCode}) — try again shortly`;
          } else {
            msg = apiMsg || `HTTP ${res.statusCode}`;
          }

          return reject(new Error(msg));
        }
        resolve(data);
      });
    });

    req.on("error", reject);
    if (payload) req.write(payload);
    req.end();
  });
}

function get(path) { return request("GET", path, null); }
function post(path, body) { return request("POST", path, body); }
function put(path, body) { return request("PUT", path, body); }

// ---------------------------------------------------------------------------
// Command handlers
// ---------------------------------------------------------------------------

async function trigger_incident({ service_id, title, severity, body }) {
  if (!service_id) throw new Error("params.service_id is required");
  if (!title) throw new Error("params.title is required");

  const urgency = ["critical", "error"].includes(severity) ? "high" : "low";

  const payload = {
    incident: {
      type: "incident",
      title,
      urgency,
      service: { id: service_id, type: "service_reference" },
    },
  };

  if (body) {
    payload.incident.body = { type: "incident_body", details: body };
  }

  const data = await post("/incidents", payload);
  const inc = data.incident;
  return {
    id: inc.id,
    incident_number: inc.incident_number,
    title: inc.title,
    status: inc.status,
    urgency: inc.urgency,
    html_url: inc.html_url,
    created_at: inc.created_at,
    service: { id: inc.service.id, name: inc.service.summary },
  };
}

async function acknowledge_incident({ incident_id }) {
  if (!incident_id) throw new Error("params.incident_id is required");

  const data = await put(`/incidents/${incident_id}`, {
    incident: { type: "incident", status: "acknowledged" },
  });
  const inc = data.incident;
  return {
    id: inc.id,
    incident_number: inc.incident_number,
    title: inc.title,
    status: inc.status,
    acknowledged_at: inc.last_status_change_at,
  };
}

async function resolve_incident({ incident_id }) {
  if (!incident_id) throw new Error("params.incident_id is required");

  const data = await put(`/incidents/${incident_id}`, {
    incident: { type: "incident", status: "resolved" },
  });
  const inc = data.incident;
  return {
    id: inc.id,
    incident_number: inc.incident_number,
    title: inc.title,
    status: inc.status,
    resolved_at: inc.last_status_change_at,
  };
}

async function list_incidents({ status, limit } = {}) {
  const qs = new URLSearchParams();
  if (status) qs.set("statuses[]", status);
  qs.set("limit", limit ? String(limit) : "25");
  qs.set("total", "true");

  const data = await get(`/incidents?${qs}`);

  return {
    total: data.total,
    more: data.more,
    limit: data.limit,
    offset: data.offset,
    incidents: (data.incidents || []).map((inc) => ({
      id: inc.id,
      incident_number: inc.incident_number,
      title: inc.title,
      status: inc.status,
      urgency: inc.urgency,
      created_at: inc.created_at,
      service: { id: inc.service.id, name: inc.service.summary },
      assigned_to: (inc.assignments || []).map((a) => a.assignee.summary),
    })),
  };
}

async function get_incident({ incident_id }) {
  if (!incident_id) throw new Error("params.incident_id is required");

  const data = await get(`/incidents/${incident_id}`);
  const inc = data.incident;
  return {
    id: inc.id,
    incident_number: inc.incident_number,
    title: inc.title,
    status: inc.status,
    urgency: inc.urgency,
    html_url: inc.html_url,
    created_at: inc.created_at,
    last_status_change_at: inc.last_status_change_at,
    service: { id: inc.service.id, name: inc.service.summary },
    assigned_to: (inc.assignments || []).map((a) => a.assignee.summary),
    escalation_policy: inc.escalation_policy
      ? { id: inc.escalation_policy.id, name: inc.escalation_policy.summary }
      : null,
    body: inc.body ? inc.body.details : null,
  };
}

async function add_incident_note({ incident_id, content }) {
  if (!incident_id) throw new Error("params.incident_id is required");
  if (!content) throw new Error("params.content is required");

  const data = await post(`/incidents/${incident_id}/notes`, {
    note: { content },
  });
  const note = data.note;
  return {
    id: note.id,
    content: note.content,
    created_at: note.created_at,
    user: note.user ? note.user.summary : null,
  };
}

async function get_oncall({ schedule_id } = {}) {
  const qs = new URLSearchParams();
  qs.set("limit", "25");
  if (schedule_id) qs.set("schedule_ids[]", schedule_id);

  const data = await get(`/oncalls?${qs}`);
  return {
    oncalls: (data.oncalls || []).map((oc) => ({
      user: { id: oc.user.id, name: oc.user.summary },
      schedule: oc.schedule
        ? { id: oc.schedule.id, name: oc.schedule.summary }
        : null,
      escalation_policy: oc.escalation_policy
        ? { id: oc.escalation_policy.id, name: oc.escalation_policy.summary }
        : null,
      escalation_level: oc.escalation_level,
      start: oc.start,
      end: oc.end,
    })),
  };
}

async function list_schedules() {
  const data = await get("/schedules?limit=100");
  return {
    total: data.total,
    schedules: (data.schedules || []).map((s) => ({
      id: s.id,
      name: s.name,
      description: s.description,
      time_zone: s.time_zone,
      html_url: s.html_url,
      users: (s.users || []).map((u) => ({ id: u.id, name: u.summary })),
    })),
  };
}

async function list_services() {
  const data = await get("/services?limit=100");
  return {
    total: data.total,
    services: (data.services || []).map((s) => ({
      id: s.id,
      name: s.name,
      description: s.description,
      status: s.status,
      html_url: s.html_url,
      escalation_policy: s.escalation_policy
        ? { id: s.escalation_policy.id, name: s.escalation_policy.summary }
        : null,
    })),
  };
}

async function get_service({ service_id }) {
  if (!service_id) throw new Error("params.service_id is required");

  const data = await get(`/services/${service_id}`);
  const s = data.service;
  return {
    id: s.id,
    name: s.name,
    description: s.description,
    status: s.status,
    html_url: s.html_url,
    created_at: s.created_at,
    escalation_policy: s.escalation_policy
      ? { id: s.escalation_policy.id, name: s.escalation_policy.summary }
      : null,
    integrations: (s.integrations || []).map((i) => ({
      id: i.id,
      name: i.summary,
    })),
    alert_grouping: s.alert_grouping || null,
    alert_grouping_timeout: s.alert_grouping_timeout || null,
  };
}

async function create_maintenance_window({
  service_ids,
  start_time,
  end_time,
  description,
}) {
  if (!service_ids || !service_ids.length)
    throw new Error("params.service_ids (array) is required");
  if (!start_time) throw new Error("params.start_time is required (ISO 8601)");
  if (!end_time) throw new Error("params.end_time is required (ISO 8601)");

  const data = await post("/maintenance_windows", {
    maintenance_window: {
      type: "maintenance_window",
      start_time,
      end_time,
      description: description || "",
      services: service_ids.map((id) => ({ id, type: "service_reference" })),
    },
  });
  const mw = data.maintenance_window;
  return {
    id: mw.id,
    description: mw.description,
    start_time: mw.start_time,
    end_time: mw.end_time,
    html_url: mw.html_url,
    services: (mw.services || []).map((s) => ({ id: s.id, name: s.summary })),
  };
}

async function list_maintenance_windows() {
  const data = await get("/maintenance_windows?limit=100");
  return {
    total: data.total,
    maintenance_windows: (data.maintenance_windows || []).map((mw) => ({
      id: mw.id,
      description: mw.description,
      start_time: mw.start_time,
      end_time: mw.end_time,
      html_url: mw.html_url,
      services: (mw.services || []).map((s) => ({ id: s.id, name: s.summary })),
    })),
  };
}

// ---------------------------------------------------------------------------
// Dispatch
// ---------------------------------------------------------------------------

const COMMANDS = {
  trigger_incident,
  acknowledge_incident,
  resolve_incident,
  list_incidents,
  get_incident,
  add_incident_note,
  get_oncall,
  list_schedules,
  list_services,
  get_service,
  create_maintenance_window,
  list_maintenance_windows,
};

function fail(obj) {
  process.exitCode = 1;
  process.stdout.write(JSON.stringify(obj) + "\n");
}

async function main() {
  let input = "";
  process.stdin.setEncoding("utf8");
  for await (const chunk of process.stdin) input += chunk;

  let parsed;
  try {
    parsed = JSON.parse(input.trim());
  } catch (_) {
    return fail({ error: "Invalid JSON input. Expected { command, params }." });
  }

  const { command, params = {} } = parsed;

  if (!command) {
    return fail({
      error: "Missing `command` field.",
      available_commands: Object.keys(COMMANDS),
    });
  }

  const handler = COMMANDS[command];
  if (!handler) {
    return fail({
      error: `Unknown command: "${command}"`,
      available_commands: Object.keys(COMMANDS),
    });
  }

  try {
    const result = await handler(params);
    process.stdout.write(JSON.stringify(result, null, 2) + "\n");
  } catch (err) {
    fail({ error: err.message });
  }
}

main();


path: skill.json
content: {
  "pricing": {
    "model": "per-call",
    "currency": "USDC",
    "amount": 0.001
  }
}


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)