# beeps documentation beeps routes alerts from monitoring systems to on-call engineers and AI agents. --- # What is beeps? > Source: https://beeps.dev/docs/ beeps routes alerts from your monitoring systems to on-call engineers and AI agents. When an incident occurs, beeps can notify humans and kick off automated triage at the same time — or sequentially, however you want to set it up. ## The Flow: How Alerts Move Through the System Here's what happens when an incident occurs: ``` Monitoring System (Sentry, Datadog, etc.) ↓ Sends webhook to beeps ↓ Alert Created ↓ Relay (routing pipeline) ↓ Relay Rules (ordered actions) ├─ [Group: agents] → AI Agent starts working │ (uses Integration credentials) │ └─ [Group: humans] → Find on-call user (from Schedule) → Send notification (via Contact Methods) ``` ## Core concepts ### Relays Relays are alert routing pipelines. When an alert arrives, the relay runs it through an ordered set of rules. Think of a relay as "what should happen when this alert fires." You'll typically have one per service or environment ("Production Alerts", "Staging Alerts"). Set `externalKey` on relays so your config scripts are idempotent. ### Relay rules Rules are the individual actions inside a relay. Three types: - `schedule_notify` — notify the on-call user from a schedule - `webhook` — POST to an external URL (Slack, PagerDuty, etc.) - `agent` — invoke an AI agent (Devin, Cursor) Rules have a `group` and an `order`. Same group = sequential (order 1 runs, then 2, then 3). Different groups = parallel. This is how you control whether agents and humans get notified at the same time or one after the other: ```typescript // Parallel — different groups, both fire immediately Rule 1: { group: "agents", order: 1, ruleType: "agent" } Rule 2: { group: "humans", order: 1, ruleType: "schedule_notify" } // Sequential — same group, agent tries first Rule 1: { group: "default", order: 1, ruleType: "agent" } Rule 2: { group: "default", order: 2, ruleType: "schedule_notify" } ``` ### Schedules Schedules define on-call rotations — who is on-call and when it rotates. A schedule becomes active at `startAt`, then hands off on a daily or weekly cadence using `handoffDay` and `handoffTime` (UTC). If you omit `startAt`, beeps starts the schedule immediately. Members rotate in the order they were added. ### Contact methods Each user needs at least one contact method (email or SMS) to receive notifications. We recommend both. Phone numbers should be in E.164 format (`+14155551234`). ### Integrations Integrations store API credentials for AI agents (Devin, Cursor). The keys are encrypted and never returned in API responses. Don't confuse these with observability integrations — agent integrations tell beeps how to talk to agents, while observability integrations are webhooks that monitoring tools use to send alerts into beeps. ### Webhooks Each relay gets a webhook URL (`https://hooks.beeps.dev/{webhookKey}`) that monitoring systems POST to. When a request comes in, beeps detects the provider, normalizes the payload, creates an alert, and kicks off the relay. ### Alerts Alerts are incidents created from webhook payloads — things like "High CPU usage on web-01" with a severity of `critical`. They carry a `title`, `severity` (critical/high/medium/low/info), optional `message`, and arbitrary `metadata` JSON for context like server names or error rates. ## Alert Lifecycle 1. **Created** - Alert arrives via webhook 2. **Responded** - Someone (human or AI) is working on it 3. **Assigned** - Explicitly assigned to a specific user 4. **Resolved** - Issue is fixed Responding to an alert cancels any pending relay executions, preventing duplicate notifications. ## Typical Setup Order When configuring beeps for the first time, follow this order: 1. **Create a Relay** - The routing pipeline 2. **Create a Schedule** - Define on-call rotation 3. **Add Schedule Members** - Add team members to rotation 4. **Add Contact Methods** - Configure email/SMS for users 5. **Create Integration** (optional) - Set up AI agent credentials 6. **Create Relay Rules** - Define what happens when alerts arrive 7. **Test** - Send a test alert to verify everything works **Ready to get started?** Follow the [Quickstart](/docs/quickstart) to set up relays, schedules, notifications, and AI agents. ## Next Steps - [Quickstart](/docs/quickstart) - Set up humans and AI agents - [Best Practices](/docs/best-practices/) - Configuration guidelines - [Relays](/docs/relays/overview/) - Alert routing in depth - [Schedules](/docs/schedules/overview/) - On-call rotations ## Questions? **Q: What's the difference between Agent Integrations and Observability Integrations?** A: Agent Integrations store credentials for AI agents (Devin, Cursor Cloud Agent) so beeps can invoke them. Observability Integrations are webhooks that monitoring systems (Sentry, Datadog) use to send alerts into beeps. **Q: Can I have multiple relays?** A: Yes. You might have separate relays for production vs staging, or different relays for different services. **Q: What happens if no one is on-call?** A: The `schedule_notify` rule won't send notifications. Make sure your schedule always has at least one member. **Q: Can I test without sending real alerts?** A: Yes — just POST a test payload to your webhook URL, or use the SDK to create test alerts programmatically. --- # Quickstart > Source: https://beeps.dev/docs/quickstart/ import { Tabs, TabItem } from "@astrojs/starlight/components"; Set up a relay, schedule, AI agent, and parallel routing. Pick whichever surface you want to drive it from. New to beeps? Read [Core Concepts](/docs/) first to understand how the pieces fit together. ## Prerequisites - A [beeps](https://beeps.dev) account. - An access token from **Settings → Access Tokens** (looks like `bat_xxxxxxxx`). ## Pick your surface The same eight-step setup is available three ways. The CLI is the fastest path to your first alert. Pick it unless you have a reason not to. Install + authenticate: ```bash npm install -g @beepsdev/cli export BEEPS_ACCESS_TOKEN="bat_your_token_here" ``` ### 1. Create a relay ```bash beeps relay create --name "production relay" \ --external-key production::relay \ --description "routes critical production incidents" \ --json ``` The response includes a relay id (`rly_…`) and a webhook URL. Save both; you'll need them in steps 2 and 8. ### 2. Create an on-call schedule ```bash beeps schedule create \ --name "Primary On-Call" \ --relay-id rly_REPLACE_ME \ --type weekly \ --handoff-day monday \ --handoff-time 09:00 \ --external-key primary::schedule \ --json ``` `startAt` is omitted on purpose. beeps defaults it to now, so you can test alerts immediately. ### 3. Add team members ```bash beeps schedule add-member --schedule-id sch_REPLACE_ME --email alice@example.com beeps schedule add-member --schedule-id sch_REPLACE_ME --email bob@example.com ``` Members rotate in the order they're added. ### 4. Contact methods (one-time, in the dashboard) Your signup email is auto-added as a verified contact method. To add SMS or another email, go to **Settings → Profile** in the dashboard. SMS requires verification via a confirmation text. :::tip[Stopping here is fine] If you don't need AI agents, skip to step 7 and create only the schedule_notify rule (the `humans` group). ::: ### 5. Set up an AI agent integration Get a [Cursor cloud agent API token](https://cursor.com/dashboard?tab=cloud-agents). **Enable Privacy Mode in Cursor account settings.** Without it, every API call returns 403. ```bash export CURSOR_API_KEY="cursor_api_key_here" beeps integration create \ --provider cursor \ --name "Cursor Agent" \ --api-key-env CURSOR_API_KEY \ --json ``` ### 6. Create parallel relay rules ```bash # AI agent rule beeps relay rule create --relay-id rly_REPLACE_ME \ --name "AI Agent Auto-Triage" \ --rule-type agent \ --group agents \ --order 1 \ --external-key agents::cursor \ --config '{"agentType":"cursor","integrationId":"int_REPLACE_ME","repository":"https://github.com/your-org/your-repo","autoCreatePr":true}' # Human notify rule beeps relay rule create --relay-id rly_REPLACE_ME \ --name "Notify On-Call Engineer" \ --rule-type schedule_notify \ --group humans \ --order 1 \ --external-key humans::primary \ --config '{"scheduleId":"sch_REPLACE_ME"}' ``` Different groups (`agents` vs `humans`) run in parallel. ### 7. Verify ```bash beeps relay list beeps relay rule list --relay-id rly_REPLACE_ME beeps schedule on-call --schedule-id sch_REPLACE_ME ``` ### 8. Fire a test alert Use the webhook URL from step 1: ```bash curl -X POST https://hooks.beeps.dev/YOUR_WEBHOOK_KEY \ -H "Content-Type: application/json" \ -d '{"title":"Test Alert","message":"Testing the on-call system","severity":"high"}' ``` You should see Cursor start triaging at the same time the on-call engineer gets notified. ```bash beeps alert list --active ``` The CLI tab above is faster for first-time setup. Use the SDK when you want this in your application code or in a [config-as-code](/docs/cli/config/) file. ### 1. Install and initialize ```bash npm install @beepsdev/sdk ``` ```bash pnpm add @beepsdev/sdk ``` ```bash bun add @beepsdev/sdk ``` ```bash export BEEPS_ACCESS_TOKEN="bat_your_access_token_here" ``` Create `beeps.config.ts`: ```typescript import { BeepsClient } from "@beepsdev/sdk"; const client = new BeepsClient({ accessToken: process.env.BEEPS_ACCESS_TOKEN, }); ``` ### 2. Create a relay ```typescript const relay = await client.relay.create({ name: "production relay", description: "routes critical production incidents", externalKey: "production::relay", }); ``` :::tip[Use externalKey for idempotency] Always set `externalKey` on relays, schedules, and rules. This makes your config safe to re-run without creating duplicates. Think of `externalKey` as a stable identifier you control, while `id` is generated by beeps. ::: ### 3. Create a schedule ```typescript const schedule = await client.schedule.create({ name: "Primary On-Call", relayId: relay.id, type: "weekly", handoffDay: "monday", handoffTime: "09:00", // UTC externalKey: "primary::schedule", }); ``` Omitting `startAt` is intentional. The schedule starts immediately, so you can test right away. ### 4. Add team members ```typescript await client.schedule.addMember(schedule.id, { email: "alice@example.com" }); await client.schedule.addMember(schedule.id, { email: "bob@example.com" }); ``` Members rotate in the order they're added. ### 5. Contact methods (one-time, in the dashboard) Signup email is auto-added as a verified contact method. Add SMS or extra emails at **Settings → Profile**. ### 6. AI agent integration Get a [Cursor cloud agent API token](https://cursor.com/dashboard?tab=cloud-agents). **Enable Privacy Mode in Cursor account settings.** Without it, every API call returns 403. ```typescript const integration = await client.integration.create({ name: "Cursor Agent", provider: "cursor", apiKey: process.env.CURSOR_API_KEY, }); ``` ### 7. Create parallel relay rules ```typescript // AI agent await client.relay.rules.create(relay.id, { name: "AI Agent Auto-Triage", externalKey: "agents::cursor", ruleType: "agent", group: "agents", order: 1, config: { agentType: "cursor", integrationId: integration.id, repository: "https://github.com/your-org/your-repo", autoCreatePr: true, }, }); // Human notify await client.relay.rules.create(relay.id, { name: "Notify On-Call Engineer", externalKey: "humans::primary::schedule-notify", ruleType: "schedule_notify", group: "humans", order: 1, config: { scheduleId: schedule.id }, }); ``` Different groups run in parallel. ### 8. Fire a test alert ```bash curl -X POST https://hooks.beeps.dev/YOUR_WEBHOOK_KEY \ -H "Content-Type: application/json" \ -d '{"title":"Test Alert","message":"Testing","severity":"high"}' ``` For a full runnable config file (`beeps.config.ts`) and CI deploy via `beeps relay apply`, see [Config as Code](/docs/config-as-code/). Drive setup from Claude Code, Codex, or any MCP-compatible client. Best when you live in your editor and don't want to context-switch. ### Connect the MCP server ```bash claude mcp add --transport http --scope user beeps https://mcp.beeps.dev/mcp ``` ```bash codex mcp add beeps --url https://mcp.beeps.dev/mcp codex mcp login beeps --scopes "openid,profile,email,offline_access,beeps.tools.read,beeps.tools.write" ``` From here, just talk to your agent. ### 1. Create a relay ```text Create a relay called "production relay" with externalKey "production::relay". ``` The agent calls `beeps_create_relay` and reports back the relay id and webhook URL. Save the webhook URL for step 8. ### 2. Create a schedule ```text Create a weekly schedule called "Primary On-Call" on the production relay, handoff Monday at 09:00, externalKey "primary::schedule". ``` ### 3. Add team members ```text Add alice@example.com and bob@example.com to the primary schedule. ``` ### 4. Contact methods (one-time, in the dashboard) Open **Settings → Profile** to add SMS or extra emails. Your signup email is already verified. ### 5. AI agent integration (one-time, outside MCP) MCP can't create the Cursor credential. That's the only step that needs the CLI or dashboard. Get a [Cursor cloud agent API token](https://cursor.com/dashboard?tab=cloud-agents) (**enable Privacy Mode in Cursor account settings**), then either: ```bash export CURSOR_API_KEY="cursor_api_key_here" beeps integration create --provider cursor --name "Cursor Agent" --api-key-env CURSOR_API_KEY --json ``` …or use **Settings → Integrations → New integration** in the dashboard. Either way, grab the returned integration id (`int_…`) so you can reference it in the next step. ### 6. Create parallel relay rules ```text On the production relay, create two rules in parallel: 1. An "agent" rule named "AI Agent Auto-Triage", group "agents", using my Cursor integration int_REPLACE_ME, repository "https://github.com/your-org/your-repo", autoCreatePr true. 2. A "schedule_notify" rule named "Notify On-Call Engineer", group "humans", pointing at the Primary On-Call schedule. ``` Different groups fire in parallel. ### 7. Verify ```text List the rules on the production relay and tell me who's on call right now. ``` ### 8. Fire a test alert ```bash curl -X POST https://hooks.beeps.dev/YOUR_WEBHOOK_KEY \ -H "Content-Type: application/json" \ -d '{"title":"Test Alert","message":"Testing","severity":"high"}' ``` Then back in your editor: ```text Show me active alerts. ``` ## Next steps - [Config as Code](/docs/config-as-code/) — check `beeps.config.ts` into your repo and deploy via `beeps relay apply` in CI. - [Relay Rules](/docs/relays/rules/) — escalation, webhooks, sequential vs parallel. - [Schedules](/docs/schedules/manage/) — overrides, multiple rotations, secondary schedules. - [Agent Integrations](/docs/agent-integrations/overview/) — Devin, Cursor, Codex, Claude, AWS DevOps, OpenCode, Zup. - [Observability Integrations](/docs/observability-integrations/overview/) — Sentry, Datadog, Prometheus, generic webhooks. - [MCP Server](/docs/using-beeps/mcp-server/) — full tool list for ongoing alert triage from your editor. ## Troubleshooting - [Alerts not appearing](/docs/troubleshooting/#alerts-are-not-appearing) - [No one receiving notifications](/docs/troubleshooting/#alerts-appear-but-no-one-is-notified) - [AI agent not responding](/docs/troubleshooting/#ai-agent-never-responds) - [Rules not executing](/docs/troubleshooting/#relay-rules-not-firing) --- # Relays Overview > Source: https://beeps.dev/docs/relays/overview/ Relays are alert routing pipelines. They receive alerts via webhooks and process them through ordered rules to notify people, call external APIs, or invoke AI agents. ## Relay Rules Each relay has rules that execute in order. Rule types: - **schedule_notify** - Notify the on-call user from a schedule - **webhook** - Call an external URL (Slack, PagerDuty, etc.) - **agent** - Invoke an AI agent (Devin, Cursor Cloud Agent) Rules are organized into groups. Rules in the same group run sequentially; rules in different groups run in parallel. See [Best Practices](/docs/best-practices/#parallel-vs-sequential-execution) for examples. ## Common Use Cases ### Direct Schedule Notification Route all alerts directly to your on-call schedule: ```typescript const relay = await client.relay.create({ name: "Production Alerts", description: "Critical production incidents", externalKey: "production-relay", // Idempotent - safe to re-run }); await client.relay.rules.create(relay.id, { name: "Notify on-call engineer", externalKey: "notify-oncall", // Idempotent rule creation ruleType: "schedule_notify", config: { scheduleId: "sch_xyz123", }, }); ``` ### Escalation with AI Agent Try an AI agent first, then escalate to humans: ```typescript await client.relay.rules.create(relay.id, { name: "Try AI agent first", externalKey: "agent-first", // Idempotent order: 1, ruleType: "agent", config: { agentType: "devin", integrationId: "int_abc123", }, }); await client.relay.rules.create(relay.id, { name: "Escalate to on-call", externalKey: "escalate-oncall", // Idempotent order: 2, ruleType: "schedule_notify", config: { scheduleId: "sch_xyz123", }, }); ``` ### Webhook Integration Send alerts to external systems: ```typescript await client.relay.rules.create(relay.id, { name: "Post to Slack", externalKey: "slack-webhook", // Idempotent ruleType: "webhook", config: { endpoint: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL", method: "POST", headers: { "Content-Type": "application/json", }, payload: { text: "New alert received", }, }, }); ``` --- # Managing Relays > Source: https://beeps.dev/docs/relays/manage/ import { Tabs, TabItem, Code } from '@astrojs/starlight/components'; Create and manage relays using the SDK, API, and CLI. ## Creating a Relay ```typescript import { BeepsClient } from "@beepsdev/sdk"; const client = new BeepsClient({ accessToken: process.env.BEEPS_ACCESS_TOKEN, }); const relay = await client.relay.create({ name: "Production Alerts", description: "Routes critical production incidents", externalKey: "prod-alerts", // Optional: use for easier API access }); console.log(`Created relay: ${relay.id}`); ``` ### Safe Method (No Exceptions) ```typescript const result = await client.relay.createSafe({ name: "Production Alerts", description: "Routes critical production incidents", }); if (result.error) { console.error("Failed to create relay:", result.error.message); } else { console.log(`Created relay: ${result.data.id}`); } ``` ```bash curl -X POST https://api.beeps.dev/v0/relay \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Production Alerts", "description": "Routes critical production incidents", "externalKey": "prod-alerts" }' ``` **Response:** ```json { "relay": { "id": "rly_abc123", "organizationId": "org_xyz789", "name": "Production Alerts", "description": "Routes critical production incidents", "externalKey": "prod-alerts", "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z" } } ``` ```bash beeps relay create --name "Production Alerts" --external-key prod-alerts --description "Routes critical production incidents" ``` Use `--json` to get structured output: ```bash beeps relay create --name "Production Alerts" --external-key prod-alerts --description "Routes critical production incidents" --json ``` **Tool:** `beeps_create_relay` ```json { "name": "Production Alerts", "description": "Routes critical production incidents", "externalKey": "prod-alerts" } ``` ## Listing Relays ```typescript const relays = await client.relay.list(); console.log(`Found ${relays.length} relays:`); relays.forEach((relay) => { console.log(`- ${relay.name} (${relay.id})`); }); ``` ```bash curl -X GET https://api.beeps.dev/v0/relay \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "relays": [ { "id": "rly_abc123", "organizationId": "org_xyz789", "name": "Production Alerts", "description": "Routes critical production incidents", "externalKey": "prod-alerts", "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z" } ] } ``` ```bash beeps relay list ``` Use `--json` to get structured output: ```bash beeps relay list --json ``` **Tool:** `beeps_list_relays` ```json {} ``` ## Input Types ### CreateRelayInput ```typescript type CreateRelayInput = { name: string; // Required: Human-readable relay name description?: string; // Optional: Relay description externalKey?: string; // Optional: Custom identifier for API access }; ``` ### Relay ```typescript type Relay = { id: string; // Unique relay identifier (e.g., "rly_abc123") organizationId: string; // Organization this relay belongs to name: string; // Relay name description: string; // Relay description externalKey: string | null; // Custom external identifier createdAt: string; // ISO 8601 timestamp updatedAt: string; // ISO 8601 timestamp }; ``` ## Best Practices Always set `externalKey` for idempotent configuration. See [Best Practices](/docs/best-practices/) for details. ## Error Handling The SDK throws errors by default. Use the `*Safe` methods for error handling without exceptions: ```typescript const result = await client.relay.createSafe({ name: "New Relay", }); if (result.error) { console.error(`Error: ${result.error.message}`); } else { console.log(`Success: ${result.data.id}`); } ``` For validation failures, the SDK formats a short human-readable message and also keeps structured field-level issues on `error.details`: ```typescript import { ValidationError } from "@beepsdev/sdk"; try { await client.relay.rules.create(relay.id, { name: "Notify on-call engineer", ruleType: "schedule_notify", config: { schduleId: "sch_primary", // typo }, }); } catch (error) { if (error instanceof ValidationError) { console.error(error.message); console.error(error.details?.issues); } } ``` The message will look like: ```text Invalid config for relay rule `schedule_notify` - `config.scheduleId` is required. - `config.schduleId` Unknown field. Did you mean `scheduleId`? ``` --- # Relay Rules > Source: https://beeps.dev/docs/relays/rules/ import { Tabs, TabItem } from '@astrojs/starlight/components'; Relay rules define how alerts are processed and routed. Rules execute in order based on their `order` property. ## Key Concepts ### Groups: Parallel vs Sequential Execution The `group` field controls whether rules run simultaneously or sequentially: - **Same group** = sequential (order 1, then 2, then 3) - **Different groups** = parallel (run at the same time) See [Best Practices - Parallel vs Sequential](/docs/best-practices/#parallel-vs-sequential-execution) for examples. ### Idempotency Set `externalKey` on relay rules to make configuration idempotent and safe to re-run. See [Best Practices](/docs/best-practices/#idempotent-configuration). ## Rule Types ### Schedule Notify Notify team members based on a schedule: ```typescript const rule = await client.relay.rules.create(relayId, { name: "Notify on-call engineer", ruleType: "schedule_notify", order: 1, config: { scheduleId: "sch_xyz123", }, enabled: true, }); ``` ```bash curl -X POST https://api.beeps.dev/v0/relay/rly_abc123/rules \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Notify on-call engineer", "ruleType": "schedule_notify", "order": 1, "config": { "scheduleId": "sch_xyz123" }, "enabled": true }' ``` ### Webhook Call an external webhook: ```typescript const rule = await client.relay.rules.create(relayId, { name: "Post to Slack", ruleType: "webhook", order: 2, config: { endpoint: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL", method: "POST", headers: { "Content-Type": "application/json", }, payload: { text: "Alert received", severity: "high", }, timeout: 10000, // Optional: timeout in ms (default 30000) }, }); ``` ```bash curl -X POST https://api.beeps.dev/v0/relay/rly_abc123/rules \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Post to Slack", "ruleType": "webhook", "order": 2, "config": { "endpoint": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL", "method": "POST", "headers": { "Content-Type": "application/json" }, "payload": { "text": "Alert received" } } }' ``` ### AI Agent Invoke an AI agent to handle the alert: Supported `agentType` values: `"claude"`, `"codex"`, `"cursor"`, `"opencode"`, `"devin"`, `"aws_devops"`, `"zup"`. See [Agent Integrations](/docs/agent-integrations/overview/) for a full rundown of each. ```typescript const rule = await client.relay.rules.create(relayId, { name: "Invoke Devin agent", ruleType: "agent", order: 1, config: { agentType: "devin", // or "cursor", "aws_devops", "opencode" integrationId: "int_abc123", // References stored credentials endpoint: "https://api.devin.ai/v3/organizations/sessions", // Optional override config: { // Agent-specific configuration priority: "high", }, pollInterval: 30000, // Check status every 30s (default) maxPollAttempts: 120, // Give up after 120 attempts (default) }, }); ``` #### Cursor Agent Cursor requires a `repository` URL and can auto-create PRs: ```typescript const cursorRule = await client.relay.rules.create(relayId, { name: "Cursor Auto-Fix", ruleType: "agent", order: 1, config: { agentType: "cursor", integrationId: "int_cursor123", repository: "https://github.com/org/repo", // Required for Cursor autoCreatePr: true, // Default: true autoBranch: true, // Let Cursor auto-name the branch pollInterval: 30000, maxPollAttempts: 120, }, }); ``` #### AWS DevOps Agent Needs the Agent Space webhook URL as `endpoint`, plus the Agent Space ID and region so beeps can poll the backlog: ```typescript const awsRule = await client.relay.rules.create(relayId, { name: "AWS DevOps investigation", ruleType: "agent", order: 1, config: { agentType: "aws_devops", integrationId: "int_aws123", endpoint: "https://webhook.devopsagent.us-east-1.amazonaws.com/invoke/abc", awsAgentSpaceId: "space-abc123", awsRegion: "us-east-1", awsAuthVersion: "hmac_v1", // or "bearer_v2" pollInterval: 60000, maxPollAttempts: 60, }, }); ``` #### OpenCode Point `endpoint` at your running OpenCode server. `openCodeModel` is optional. If you leave it off, the server's default model is used: ```typescript const openCodeRule = await client.relay.rules.create(relayId, { name: "OpenCode investigation", ruleType: "agent", order: 1, config: { agentType: "opencode", integrationId: "int_oc123", endpoint: "https://opencode.internal.corp:4096", openCodeModel: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514", }, pollInterval: 15000, maxPollAttempts: 200, }, }); ``` ```bash curl -X POST https://api.beeps.dev/v0/relay/rly_abc123/rules \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Invoke Devin agent", "ruleType": "agent", "order": 1, "config": { "agentType": "devin", "integrationId": "int_abc123", "pollInterval": 30000, "maxPollAttempts": 120 } }' ``` #### Cursor Agent ```bash curl -X POST https://api.beeps.dev/v0/relay/rly_abc123/rules \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Cursor Auto-Fix", "ruleType": "agent", "order": 1, "config": { "agentType": "cursor", "integrationId": "int_cursor123", "repository": "https://github.com/org/repo", "autoCreatePr": true, "pollInterval": 30000, "maxPollAttempts": 120 } }' ``` ## Validation Errors Rule configs are validated strictly. If a required field is missing or a field name is misspelled, the SDK throws a `ValidationError` with a short message and field-level issues. ```typescript import { ValidationError } from "@beepsdev/sdk"; try { await client.relay.rules.create(relayId, { name: "Notify on-call engineer", ruleType: "schedule_notify", order: 1, config: { schduleId: "sch_xyz123", // typo }, }); } catch (error) { if (error instanceof ValidationError) { console.error(error.message); console.error(error.details?.issues); } } ``` Example SDK message: ```text Invalid config for relay rule `schedule_notify` - `config.scheduleId` is required. - `config.schduleId` Unknown field. Did you mean `scheduleId`? ``` The API returns the same validation failure as structured JSON: ```json { "error": { "code": "validation_failed", "message": "Invalid config for relay rule `schedule_notify`", "issues": [ { "path": ["config", "scheduleId"], "code": "missing_required", "message": "is required." }, { "path": ["config", "schduleId"], "code": "unknown_field", "message": "Unknown field.", "suggestion": "scheduleId" } ] } } ``` When present, the response also includes an `X-Request-ID` header you can include in support requests. ## Managing Rules ### List Rules ```typescript // List all rules const allRules = await client.relay.rules.list(relayId); // Filter by enabled status const enabledRules = await client.relay.rules.list(relayId, { enabled: true, }); // Filter by rule type const webhookRules = await client.relay.rules.list(relayId, { ruleType: "webhook", }); ``` ```bash # List all rules curl -X GET https://api.beeps.dev/v0/relay/rly_abc123/rules \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" # Filter by enabled status curl -X GET "https://api.beeps.dev/v0/relay/rly_abc123/rules?enabled=true" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" # Filter by rule type curl -X GET "https://api.beeps.dev/v0/relay/rly_abc123/rules?ruleType=webhook" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Get a Specific Rule ```typescript const rule = await client.relay.rules.get(relayId, ruleId); console.log(`Rule: ${rule.name}, Order: ${rule.order}`); ``` ```bash curl -X GET https://api.beeps.dev/v0/relay/rly_abc123/rules/rul_xyz789 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Update a Rule ```typescript const updatedRule = await client.relay.rules.update(relayId, ruleId, { name: "Updated rule name", enabled: false, config: { // Updated config }, }); ``` ```bash curl -X PUT https://api.beeps.dev/v0/relay/rly_abc123/rules/rul_xyz789 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Updated rule name", "enabled": false }' ``` ### Delete a Rule ```typescript await client.relay.rules.delete(relayId, ruleId); console.log("Rule deleted successfully"); ``` ```bash curl -X DELETE https://api.beeps.dev/v0/relay/rly_abc123/rules/rul_xyz789 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Reorder Rules ```typescript const reorderedRules = await client.relay.rules.reorder(relayId, { rules: [ { id: "rul_first", order: 1 }, { id: "rul_second", order: 2 }, { id: "rul_third", order: 3 }, ], }); reorderedRules.forEach((rule) => { console.log(`${rule.order}: ${rule.name}`); }); ``` ```bash curl -X PUT https://api.beeps.dev/v0/relay/rly_abc123/rules/reorder \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "rules": [ {"id": "rul_first", "order": 1}, {"id": "rul_second", "order": 2}, {"id": "rul_third", "order": 3} ] }' ``` ## Rule Groups Organize related rules using the `group` property: ```typescript await client.relay.rules.create(relayId, { name: "Primary notification", ruleType: "schedule_notify", group: "primary", order: 1, config: { scheduleId: "sch_primary" }, }); await client.relay.rules.create(relayId, { name: "Backup notification", ruleType: "schedule_notify", group: "backup", order: 2, config: { scheduleId: "sch_backup" }, }); ``` ## Type Definitions ### CreateRelayRuleInput ```typescript type CreateRelayRuleInput = { externalKey?: string; name: string; ruleType: RelayRuleType; group?: string; order?: number; config: RelayRuleConfig; enabled?: boolean; }; ``` ### RelayRule ```typescript type RelayRule = { id: string; organizationId: string; relayId: string; group: string; externalKey: string | null; name: string; order: number; ruleType: RelayRuleType; config: Record; enabled: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; }; ``` ## Best Practices See [Best Practices](/docs/best-practices/) for relay rule configuration guidelines. --- # Relay API Reference > Source: https://beeps.dev/docs/relays/api/ Complete API reference for the Relay resource in the beeps SDK. ## SDK Client ```typescript import { BeepsClient } from "@beepsdev/sdk"; const client = new BeepsClient({ accessToken: process.env.BEEPS_ACCESS_TOKEN, baseURL: "https://api.beeps.dev", // Optional timeoutMs: 30000, // Optional: default 30s retries: { // Optional attempts: 3, backoffMs: 1000, }, }); ``` ## Relay Methods ### `client.relay.create(input)` Create a new relay. **Parameters:** - `input: CreateRelayInput` **Returns:** `Promise` **Example:** ```typescript const relay = await client.relay.create({ name: "Production Alerts", description: "Routes critical production incidents", externalKey: "prod-alerts", }); ``` ### `client.relay.list()` List all relays in your organization. **Returns:** `Promise` **Example:** ```typescript const relays = await client.relay.list(); ``` ### `client.relay.lint(relayId, input?)` Validate relay rules, schedules, and contact methods without side effects. **Parameters:** - `relayId: string` - `input?: RelayLintInput` **Returns:** `Promise` **Example:** ```typescript const result = await client.relay.lint("rly_abc123", { simulateAt: "2025-01-01T00:00:00Z", coverageDays: 7, }); if (result.issues.length > 0) { console.log(result.issues); } ``` ### `client.relay.simulate(relayId, input)` Preview relay execution without sending notifications. **Parameters:** - `relayId: string` - `input: RelaySimulationInput` **Returns:** `Promise` **Example:** ```typescript const result = await client.relay.simulate("rly_abc123", { simulateAt: "2025-01-01T00:00:00Z", payload: { action: "triggered", }, source: "sentry", }); console.log(result.groups); ``` ### `client.relay.createSafe(input)` Create a new relay without throwing exceptions. **Parameters:** - `input: CreateRelayInput` **Returns:** `Promise>` **Example:** ```typescript const result = await client.relay.createSafe({ name: "Production Alerts", }); if (result.error) { console.error(result.error.message); } else { console.log(result.data.id); } ``` For validation failures, `result.error` may be a `ValidationError` with field-level issues on `error.details?.issues`. ## Relay Rules Methods ### `client.relay.rules.create(relayId, input)` Create a new rule for a relay. **Parameters:** - `relayId: string` - `input: CreateRelayRuleInput` **Returns:** `Promise` **Example:** ```typescript const rule = await client.relay.rules.create("rly_abc123", { name: "Notify on-call", ruleType: "schedule_notify", order: 1, config: { scheduleId: "sch_xyz789", }, }); ``` If rule validation fails, the SDK throws a `ValidationError` with a formatted message and structured issues in `error.details?.issues`. The API returns the same issues in `error.issues` with code `validation_failed`. ### `client.relay.rules.list(relayId, params?)` List rules for a relay. **Parameters:** - `relayId: string` - `params?: ListRelayRulesParams` - `enabled?: boolean` - Filter by enabled status - `ruleType?: RelayRuleType` - Filter by rule type **Returns:** `Promise` **Example:** ```typescript const allRules = await client.relay.rules.list("rly_abc123"); const enabledRules = await client.relay.rules.list("rly_abc123", { enabled: true, }); ``` ### `client.relay.rules.get(relayId, ruleId)` Get a specific rule. **Parameters:** - `relayId: string` - `ruleId: string` **Returns:** `Promise` **Example:** ```typescript const rule = await client.relay.rules.get("rly_abc123", "rul_xyz789"); ``` ### `client.relay.rules.update(relayId, ruleId, input)` Update a rule. **Parameters:** - `relayId: string` - `ruleId: string` - `input: UpdateRelayRuleInput` **Returns:** `Promise` **Example:** ```typescript const updatedRule = await client.relay.rules.update( "rly_abc123", "rul_xyz789", { name: "Updated rule name", enabled: false, } ); ``` ### `client.relay.rules.delete(relayId, ruleId)` Delete a rule. **Parameters:** - `relayId: string` - `ruleId: string` **Returns:** `Promise<{ success: boolean }>` **Example:** ```typescript await client.relay.rules.delete("rly_abc123", "rul_xyz789"); ``` ### `client.relay.rules.reorder(relayId, input)` Reorder rules. **Parameters:** - `relayId: string` - `input: ReorderRelayRulesInput` **Returns:** `Promise` **Example:** ```typescript const reordered = await client.relay.rules.reorder("rly_abc123", { rules: [ { id: "rul_first", order: 1 }, { id: "rul_second", order: 2 }, ], }); ``` ## Safe Methods All mutation methods have corresponding `*Safe` variants that return `Result` instead of throwing: - `client.relay.lintSafe(relayId, input?)` - `client.relay.simulateSafe(relayId, input)` - `client.relay.rules.listSafe(relayId, params?)` - `client.relay.rules.createSafe(relayId, input)` - `client.relay.rules.getSafe(relayId, ruleId)` - `client.relay.rules.updateSafe(relayId, ruleId, input)` - `client.relay.rules.deleteSafe(relayId, ruleId)` - `client.relay.rules.reorderSafe(relayId, input)` ## Types ### RelayRuleType ```typescript type RelayRuleType = | "schedule_notify" | "webhook" | "agent" | "external_api" // deprecated | "wait" // deprecated | "conditional" // deprecated | "escalate"; // deprecated ``` ### ScheduleNotifyConfig ```typescript type ScheduleNotifyConfig = { scheduleId: string; notificationMethod?: "email" | "sms"; }; ``` ### WebhookConfig ```typescript type WebhookConfig = { endpoint: string; method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; headers?: Record; payload?: Record; timeout?: number; // Request timeout in ms (default 30000) }; ``` ### AgentConfig ```typescript type AgentConfig = { agentType: "devin" | "cursor"; integrationId?: string; // References stored credentials endpoint?: string; // Override endpoint if needed config?: Record; // Agent-specific config pollInterval?: number; // How often to poll in ms (default 30000) maxPollAttempts?: number; // Give up after N attempts (default 120) }; ``` ### RelayLintIssue ```typescript type RelayLintIssue = { severity: "error" | "warning" | "info"; code: string; message: string; relayId?: string; ruleId?: string; scheduleId?: string; details?: Record; }; ``` ### RelayLintResult ```typescript type RelayLintResult = { issues: RelayLintIssue[]; }; ``` ### RelaySimulationResult ```typescript type RelaySimulationResult = { relayId: string; simulateAt: string; alertPreview: { title: string; message?: string; severity: "critical" | "high" | "medium" | "low" | "info"; source: string; externalId?: string; metadata?: Record; }; groups: Array<{ groupName: string; steps: Array<{ ruleId: string; ruleType: "schedule_notify" | "webhook" | "agent" | string; name: string; enabled: boolean; }>; }>; issues: RelayLintIssue[]; }; ``` ## HTTP Endpoints ### Create Relay ``` POST /v0/relay ``` **Request Body:** ```json { "name": "Production Alerts", "description": "Routes critical production incidents", "externalKey": "prod-alerts" } ``` ### List Relays ``` GET /v0/relay ``` ### Create Relay Rule ``` POST /v0/relay/:relayId/rules ``` ### List Relay Rules ``` GET /v0/relay/:relayId/rules GET /v0/relay/:relayId/rules?enabled=true GET /v0/relay/:relayId/rules?ruleType=webhook ``` ### Lint Relay ``` POST /v0/relay/:relayId/lint ``` **Request Body:** ```json { "simulateAt": "2025-01-01T00:00:00Z", "coverageDays": 7 } ``` **Response:** ```json { "issues": [ { "severity": "warning", "code": "schedule.assignment_missing_contact_method", "message": "Schedule assignment has no verified contact method", "scheduleId": "sch_123", "details": { "userId": "usr_456" } } ] } ``` ### Simulate Relay ``` POST /v0/relay/:relayId/simulate ``` **Request Body:** ```json { "simulateAt": "2025-01-01T00:00:00Z", "payload": { "action": "triggered" }, "source": "sentry" } ``` **Response:** ```json { "relayId": "rly_123", "simulateAt": "2025-01-01T00:00:00Z", "alertPreview": { "title": "Test Alert", "severity": "medium", "source": "sentry" }, "groups": [], "issues": [] } ``` ### Get Relay Rule ``` GET /v0/relay/:relayId/rules/:ruleId ``` ### Update Relay Rule ``` PUT /v0/relay/:relayId/rules/:ruleId ``` ### Delete Relay Rule ``` DELETE /v0/relay/:relayId/rules/:ruleId ``` ### Reorder Relay Rules ``` PUT /v0/relay/:relayId/rules/reorder ``` ## Authentication All API requests require authentication using your access token: ```bash curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ https://api.beeps.dev/v0/relay ``` --- # Schedules Overview > Source: https://beeps.dev/docs/schedules/overview/ Schedules define on-call rotations. Each schedule becomes active at `startAt`, then rotates team members on a daily or weekly cadence using `handoffDay` and `handoffTime`. If you omit `startAt` when creating a schedule, beeps starts it immediately. ## Schedule Types ### Daily Rotation Rotates every day at a specified time: ```typescript const schedule = await client.schedule.create({ name: "Daily On-Call", relayId: "rly_abc123", type: "daily", handoffDay: "monday", // Not used for daily schedules handoffTime: "09:00", // Rotates at 9:00 AM every day externalKey: "daily-oncall", // Idempotent - safe to re-run }); ``` ### Weekly Rotation Rotates every week on a specified day and time: ```typescript const schedule = await client.schedule.create({ name: "Weekly On-Call", relayId: "rly_abc123", type: "weekly", handoffDay: "monday", // Rotates every Monday handoffTime: "09:00", // at 9:00 AM externalKey: "weekly-oncall", // Idempotent - safe to re-run }); ``` ## Schedule Members Add team members to the schedule to participate in the rotation: ```typescript await client.schedule.addMember(schedule.id, { userId: "usr_alice", }); await client.schedule.addMember(schedule.id, { userId: "usr_bob", }); ``` Members rotate in the order they were added. New members usually join at the next handoff boundary so the current on-call assignment is not interrupted. If the schedule has not started yet, they become active at `startAt`. If the schedule is active but currently has no active members, they become active immediately. ## On-Call Status Check who is currently on-call: ```typescript const onCall = await client.schedule.getOnCall(schedule.id); console.log(`${onCall.email} is on-call`); ``` ## Assignments View upcoming or past assignments to understand the rotation: ```typescript // Get the next 14 days of assignments const assignments = await client.schedule.getAssignments(schedule.id, { type: "days", count: 14, }); assignments.forEach((assignment) => { console.log( `Assignment ${assignment.assignmentNumber}: ${assignment.userId} from ${assignment.startDate} to ${assignment.endDate}` ); }); ``` `assignmentNumber` is the generated assignment index for the schedule timeline. It is not a permanent member position. ## Schedule Start Set `startAt` when you want a schedule to begin in the future: ```typescript const schedule = await client.schedule.create({ name: "Planned Cutover", relayId: "rly_abc123", type: "weekly", handoffDay: "monday", handoffTime: "09:00", startAt: "2025-03-15T09:00:00.000Z", }); ``` If you omit `startAt`, the server defaults it to the current time. ## Schedule Overrides Overrides let you temporarily replace the on-call user without changing the rotation itself. They're useful for vacation coverage, sick days, or mid-shift handoffs. An override applies to a specific time window (up to 30 days) and expires automatically. While active, `getOnCall` returns the override user instead of the regularly scheduled person. Overrides cannot overlap on the same schedule. ```typescript const override = await client.schedule.createOverride(schedule.id, { userId: "usr_bob", startAt: "2025-03-15T09:00:00.000Z", endAt: "2025-03-22T09:00:00.000Z", reason: "Covering for Alice — vacation", }); ``` See [Schedule Overrides](/docs/schedules/overrides/) for the full management guide. ## Common Use Cases ### Basic On-Call Rotation Create a simple weekly rotation: ```typescript const relay = await client.relay.create({ name: "Production Alerts", }); const schedule = await client.schedule.create({ name: "Weekly On-Call", relayId: relay.id, type: "weekly", handoffDay: "monday", handoffTime: "09:00", }); await client.schedule.addMember(schedule.id, { userId: "usr_alice" }); await client.schedule.addMember(schedule.id, { userId: "usr_bob" }); await client.schedule.addMember(schedule.id, { userId: "usr_charlie" }); await client.relay.rules.create(relay.id, { name: "Notify on-call engineer", ruleType: "schedule_notify", config: { scheduleId: schedule.id, }, }); ``` ### Multi-Tier On-Call Create primary and backup schedules: ```typescript const primarySchedule = await client.schedule.create({ name: "Primary On-Call", relayId: relay.id, type: "weekly", handoffDay: "monday", handoffTime: "09:00", }); const backupSchedule = await client.schedule.create({ name: "Backup On-Call", relayId: relay.id, type: "weekly", handoffDay: "monday", handoffTime: "09:00", }); await client.relay.rules.create(relay.id, { name: "Notify primary first", order: 1, ruleType: "schedule_notify", config: { scheduleId: primarySchedule.id }, }); await client.relay.rules.create(relay.id, { name: "Escalate to backup", order: 2, ruleType: "schedule_notify", config: { scheduleId: backupSchedule.id }, }); ``` ### Follow-the-Sun Rotation Create different schedules for different time zones: ```typescript const usSchedule = await client.schedule.create({ name: "US Business Hours", relayId: relay.id, type: "daily", handoffDay: "monday", handoffTime: "08:00", // 8 AM US time }); const euSchedule = await client.schedule.create({ name: "EU Business Hours", relayId: relay.id, type: "daily", handoffDay: "monday", handoffTime: "08:00", // 8 AM EU time }); ``` --- # Managing Schedules > Source: https://beeps.dev/docs/schedules/manage/ import { Tabs, TabItem } from '@astrojs/starlight/components'; Create schedules and add team members to on-call rotations. ## Creating a Schedule ```typescript import { BeepsClient } from "@beepsdev/sdk"; const client = new BeepsClient({ accessToken: process.env.BEEPS_ACCESS_TOKEN, }); const schedule = await client.schedule.create({ name: "Weekly On-Call", relayId: "rly_abc123", // Schedule must belong to a relay type: "weekly", // or "daily" handoffDay: "monday", // Day of week for weekly schedules handoffTime: "09:00", // Time when rotation occurs (HH:MM format) // startAt: "2025-03-15T09:00:00.000Z", // Optional: omit to start immediately externalKey: "prod-oncall", // Optional: custom identifier }); console.log(`Created schedule: ${schedule.id}`); ``` ### Daily Schedule ```typescript const dailySchedule = await client.schedule.create({ name: "Daily On-Call", relayId: "rly_abc123", type: "daily", handoffDay: "monday", // Not used for daily schedules handoffTime: "09:00", // Rotates every day at this time }); ``` ### Weekly Schedule ```typescript const weeklySchedule = await client.schedule.create({ name: "Weekly On-Call", relayId: "rly_abc123", type: "weekly", handoffDay: "friday", // Rotates every Friday handoffTime: "17:00", // at 5:00 PM }); ``` If you omit `startAt`, beeps starts the schedule immediately. Set it explicitly when you want the schedule to begin in the future. ```bash curl -X POST https://api.beeps.dev/v0/schedule \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Weekly On-Call", "relayId": "rly_abc123", "type": "weekly", "handoffDay": "monday", "handoffTime": "09:00", "externalKey": "prod-oncall" }' ``` **Response:** ```json { "schedule": { "id": "sch_xyz789", "organizationId": "org_abc123", "relayId": "rly_abc123", "name": "Weekly On-Call", "type": "weekly", "handoffDay": "monday", "handoffTime": "09:00", "startAt": "2025-01-15T10:30:00Z", "externalKey": "prod-oncall", "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z", "deletedAt": null } } ``` Schedules are created via config-as-code. See [CLI Config-as-Code](/docs/cli/config/). ```bash beeps relay apply ``` **Tool:** `beeps_create_schedule` ```json { "name": "Weekly On-Call", "relayId": "rly_abc123", "type": "weekly", "handoffDay": "monday", "handoffTime": "09:00" } ``` ## Listing Schedules ```typescript const schedules = await client.schedule.list(); console.log(`Found ${schedules.length} schedules:`); schedules.forEach((schedule) => { console.log(`- ${schedule.name} (${schedule.type})`); }); ``` ```bash curl -X GET https://api.beeps.dev/v0/schedule \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "schedules": [ { "id": "sch_xyz789", "organizationId": "org_abc123", "relayId": "rly_abc123", "name": "Weekly On-Call", "type": "weekly", "handoffDay": "monday", "handoffTime": "09:00", "startAt": "2025-01-15T10:30:00Z", "externalKey": "prod-oncall", "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z", "deletedAt": null } ] } ``` ```bash beeps schedule list ``` For JSON output: ```bash beeps schedule list --json ``` **Tool:** `beeps_list_schedules` ```json {} ``` ## Adding Schedule Members Members are added to the rotation in the order they're added. New members usually join at the next handoff boundary so the current on-call assignment is not interrupted. If the schedule has not started yet, they become active at `startAt`. If the schedule is active but currently has no active members, they become active immediately. You can add members by email address or user ID. ```typescript // Add by email address const member1 = await client.schedule.addMember(schedule.id, { email: "alice@example.com", }); console.log(`Added ${member1.userId} to schedule`); const member2 = await client.schedule.addMember(schedule.id, { email: "bob@example.com", }); console.log(`Added ${member2.userId} to schedule`); // Or add by user ID const member3 = await client.schedule.addMember(schedule.id, { userId: "usr_charlie", }); ``` ```bash # Add by email curl -X POST https://api.beeps.dev/v0/schedule/sch_xyz789/members \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "alice@example.com" }' # Or by user ID curl -X POST https://api.beeps.dev/v0/schedule/sch_xyz789/members \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "userId": "usr_alice" }' ``` **Response:** ```json { "member": { "id": "scm_abc123", "scheduleId": "sch_xyz789", "userId": "usr_alice", "createdAt": "2025-01-15T10:35:00Z", "effectiveAt": "2025-01-20T09:00:00Z", "removedAt": null } } ``` **Tool:** `beeps_add_schedule_member` ```json { "scheduleId": "sch_xyz789", "email": "alice@example.com" } ``` ## Getting Current On-Call User ```typescript const onCall = await client.schedule.getOnCall(schedule.id); console.log(`Current on-call: ${onCall.email}`); console.log(`User ID: ${onCall.userId}`); console.log(`Assignment #: ${onCall.assignmentNumber}`); ``` ### Safe Method ```typescript const result = await client.schedule.getOnCallSafe(schedule.id); if (result.error) { console.error("Failed to get on-call:", result.error.message); } else { console.log(`On-call: ${result.data.email}`); } ``` ```bash curl -X GET https://api.beeps.dev/v0/schedule/sch_xyz789/on-call \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "onCall": { "userId": "usr_alice", "email": "alice@example.com", "assignmentNumber": 7, "scheduleId": "sch_xyz789" } } ``` ```bash beeps schedule on-call --schedule-id sch_xyz789 ``` For JSON output: ```bash beeps schedule on-call --schedule-id sch_xyz789 --json ``` **Tool:** `beeps_get_on_call` ```json { "scheduleId": "sch_xyz789" } ``` ## Type Definitions ### CreateScheduleInput ```typescript type CreateScheduleInput = { name: string; // Schedule name relayId: string; // Parent relay ID type: "daily" | "weekly"; // Rotation frequency handoffDay: | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday"; // Day of week (used for weekly schedules) handoffTime: string; // Time of day in HH:MM format (24-hour) startAt?: string | Date; // Optional: omit to start immediately externalKey?: string; // Optional custom identifier }; ``` ### Schedule ```typescript type Schedule = { id: string; organizationId: string; relayId: string; name: string; type: "daily" | "weekly"; handoffDay: | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday"; handoffTime: string; startAt: string; externalKey: string | null; createdAt: string; updatedAt: string; deletedAt: string | null; }; ``` ### AddScheduleMemberInput ```typescript type AddScheduleMemberInput = | { userId: string } // Add by user ID | { email: string }; // Add by email address ``` ### ScheduleMember ```typescript type ScheduleMember = { id?: string; scheduleId: string; userId: string; createdAt?: string; effectiveAt?: string; removedAt?: string | null; }; ``` ### BeepsUser ```typescript type BeepsUser = { userId: string; // User ID of the person currently on-call email: string; // Email address assignmentNumber: number | null; // Current generated assignment index (null when schedule has no members) scheduleId: string; // Schedule ID isOverride?: boolean; // True if user is on-call via schedule override overrideId?: string; // ID of the schedule override (if applicable) }; ``` ## Best Practices Always set `externalKey` for idempotent configuration. All times are in UTC. See [Best Practices](/docs/best-practices/#schedules) for details. ## Common Patterns ### Setting Up a New Team ```typescript const relay = await client.relay.create({ name: "Backend Team Alerts", }); const schedule = await client.schedule.create({ name: "Backend Weekly Rotation", relayId: relay.id, type: "weekly", handoffDay: "monday", handoffTime: "09:00", }); const teamEmails = ["alice@example.com", "bob@example.com", "charlie@example.com"]; for (const email of teamEmails) { await client.schedule.addMember(schedule.id, { email }); } await client.relay.rules.create(relay.id, { name: "Notify on-call engineer", ruleType: "schedule_notify", config: { scheduleId: schedule.id, }, }); ``` ### Checking Who's On Call ```typescript const onCall = await client.schedule.getOnCall(schedule.id); console.log(`${onCall.email} is currently on-call`); ``` --- # Schedule Overrides > Source: https://beeps.dev/docs/schedules/overrides/ import { Tabs, TabItem } from '@astrojs/starlight/components'; Schedule overrides let you temporarily swap who is on-call. Use them for vacation coverage, sick days, or handing off to someone else mid-shift. An override takes effect for a specific time window and automatically expires when the window ends. Overrides cannot overlap with each other on the same schedule. The maximum duration is 30 days. ## Creating an Override ```typescript import { BeepsClient } from "@beepsdev/sdk"; const client = new BeepsClient({ accessToken: process.env.BEEPS_ACCESS_TOKEN, }); const override = await client.schedule.createOverride("sch_xyz789", { userId: "usr_bob", startAt: "2025-03-15T09:00:00.000Z", endAt: "2025-03-22T09:00:00.000Z", reason: "Alice on vacation, Bob covering", }); console.log(`Override created: ${override.id}`); console.log(`${override.userId} on-call from ${override.startAt} to ${override.endAt}`); ``` You can also pass `Date` objects: ```typescript const override = await client.schedule.createOverride("sch_xyz789", { userId: "usr_bob", startAt: new Date("2025-03-15T09:00:00.000Z"), endAt: new Date("2025-03-22T09:00:00.000Z"), reason: "Alice on vacation, Bob covering", }); ``` ```bash curl -X POST https://api.beeps.dev/v0/schedule/sch_xyz789/overrides \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "userId": "usr_bob", "startAt": "2025-03-15T09:00:00.000Z", "endAt": "2025-03-22T09:00:00.000Z", "reason": "Alice on vacation, Bob covering" }' ``` **Response:** ```json { "override": { "id": "ovr_abc123", "organizationId": "org_xyz789", "scheduleId": "sch_xyz789", "userId": "usr_bob", "startAt": "2025-03-15T09:00:00.000Z", "endAt": "2025-03-22T09:00:00.000Z", "reason": "Alice on vacation, Bob covering", "createdByUserId": "usr_alice", "createdAt": "2025-03-10T14:00:00Z", "updatedAt": "2025-03-10T14:00:00Z", "canceledAt": null, "deletedAt": null } } ``` ```bash beeps schedule override create \ --schedule-id sch_xyz789 \ --user-id usr_bob \ --start-at 2025-03-15T09:00:00.000Z \ --end-at 2025-03-22T09:00:00.000Z \ --reason "Alice on vacation, Bob covering" ``` Add `--json` for machine-readable output: ```bash beeps schedule override create \ --schedule-id sch_xyz789 \ --user-id usr_bob \ --start-at 2025-03-15T09:00:00.000Z \ --end-at 2025-03-22T09:00:00.000Z \ --reason "Alice on vacation, Bob covering" \ --json ``` **Tool:** `beeps_set_override` ```json { "scheduleId": "sch_xyz789", "userId": "usr_bob", "startAt": "2025-03-15T09:00:00.000Z", "endAt": "2025-03-22T09:00:00.000Z", "reason": "Alice on vacation, Bob covering" } ``` ## Listing Overrides ```typescript const overrides = await client.schedule.listOverrides("sch_xyz789"); overrides.forEach((override) => { console.log(`${override.userId}: ${override.startAt} - ${override.endAt}`); if (override.reason) { console.log(` Reason: ${override.reason}`); } }); ``` Filter by date range to find overrides within a specific window: ```typescript const overrides = await client.schedule.listOverrides("sch_xyz789", { startAt: "2025-03-01T00:00:00.000Z", endAt: "2025-03-31T23:59:59.999Z", }); ``` ```bash curl -X GET "https://api.beeps.dev/v0/schedule/sch_xyz789/overrides" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` With date range filtering: ```bash curl -X GET "https://api.beeps.dev/v0/schedule/sch_xyz789/overrides?startAt=2025-03-01T00:00:00.000Z&endAt=2025-03-31T23:59:59.999Z" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "overrides": [ { "id": "ovr_abc123", "scheduleId": "sch_xyz789", "userId": "usr_bob", "startAt": "2025-03-15T09:00:00.000Z", "endAt": "2025-03-22T09:00:00.000Z", "reason": "Alice on vacation, Bob covering", "createdByUserId": "usr_alice", "canceledAt": null } ] } ``` ```bash beeps schedule override list --schedule-id sch_xyz789 ``` With date range filtering: ```bash beeps schedule override list \ --schedule-id sch_xyz789 \ --start-at 2025-03-01T00:00:00.000Z \ --end-at 2025-03-31T23:59:59.999Z ``` Add `--json` for machine-readable output: ```bash beeps schedule override list --schedule-id sch_xyz789 --json ``` ## Updating an Override Change the time window or reason for an existing override. All fields are optional—only include what you want to change. ```typescript const updated = await client.schedule.updateOverride( "sch_xyz789", "ovr_abc123", { endAt: "2025-03-25T09:00:00.000Z", reason: "Extended coverage — Alice returning Thursday", }, ); console.log(`Override now ends at ${updated.endAt}`); ``` ```bash curl -X PATCH https://api.beeps.dev/v0/schedule/sch_xyz789/overrides/ovr_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "endAt": "2025-03-25T09:00:00.000Z", "reason": "Extended coverage — Alice returning Thursday" }' ``` **Response:** ```json { "override": { "id": "ovr_abc123", "endAt": "2025-03-25T09:00:00.000Z", "reason": "Extended coverage — Alice returning Thursday", "updatedAt": "2025-03-12T10:00:00Z" } } ``` ```bash beeps schedule override update \ --schedule-id sch_xyz789 \ --override-id ovr_abc123 \ --end-at 2025-03-25T09:00:00.000Z \ --reason "Extended coverage" ``` Add `--json` for machine-readable output: ```bash beeps schedule override update \ --schedule-id sch_xyz789 \ --override-id ovr_abc123 \ --end-at 2025-03-25T09:00:00.000Z \ --reason "Extended coverage" \ --json ``` **Tool:** `beeps_update_override` ```json { "scheduleId": "sch_xyz789", "overrideId": "ovr_abc123", "endAt": "2025-03-25T09:00:00.000Z", "reason": "Extended coverage" } ``` ## Canceling an Override Cancel an override to restore the normal rotation. The on-call user reverts to whoever the schedule assigns. ```typescript const canceled = await client.schedule.cancelOverride("sch_xyz789", "ovr_abc123"); console.log(`Override canceled at ${canceled.canceledAt}`); ``` ```bash curl -X DELETE https://api.beeps.dev/v0/schedule/sch_xyz789/overrides/ovr_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "override": { "id": "ovr_abc123", "canceledAt": "2025-03-12T10:00:00Z" } } ``` ```bash beeps schedule override cancel --schedule-id sch_xyz789 --override-id ovr_abc123 ``` Add `--json` for machine-readable output: ```bash beeps schedule override cancel --schedule-id sch_xyz789 --override-id ovr_abc123 --json ``` **Tool:** `beeps_cancel_override` ```json { "scheduleId": "sch_xyz789", "overrideId": "ovr_abc123" } ``` ## Type Definitions ### CreateScheduleOverrideInput ```typescript type CreateScheduleOverrideInput = { userId: string; startAt: string | Date; endAt: string | Date; reason?: string; }; ``` ### UpdateScheduleOverrideInput ```typescript type UpdateScheduleOverrideInput = { startAt?: string | Date; endAt?: string | Date; reason?: string; }; ``` ### ListScheduleOverridesParams ```typescript type ListScheduleOverridesParams = { startAt?: string | Date; endAt?: string | Date; }; ``` ### ScheduleOverride ```typescript type ScheduleOverride = { id: string; organizationId: string; scheduleId: string; userId: string; startAt: string; endAt: string; reason: string | null; createdByUserId: string; createdAt: string; updatedAt: string; canceledAt: string | null; deletedAt: string | null; }; ``` ## Common Patterns ### Vacation Coverage ```typescript const vacationStart = new Date("2025-06-01T00:00:00.000Z"); const vacationEnd = new Date("2025-06-14T00:00:00.000Z"); const override = await client.schedule.createOverride(schedule.id, { userId: "usr_bob", startAt: vacationStart, endAt: vacationEnd, reason: "Covering for Alice — vacation June 1-14", }); ``` ### Temporary Handoff ```typescript const now = new Date(); const fourHoursLater = new Date(now.getTime() + 4 * 60 * 60 * 1000); const override = await client.schedule.createOverride(schedule.id, { userId: "usr_charlie", startAt: now, endAt: fourHoursLater, reason: "Handing off for a few hours", }); ``` ### Checking Who Is Actually On-Call The `getOnCall` method already accounts for overrides. If an override is active, the returned user will have `isOverride: true` and `overrideId` set: ```typescript const onCall = await client.schedule.getOnCall(schedule.id); if (onCall.isOverride) { console.log(`${onCall.email} is on-call via override ${onCall.overrideId}`); } else { console.log(`${onCall.email} is on-call (assignment #${onCall.assignmentNumber})`); } ``` --- # Schedule Assignments > Source: https://beeps.dev/docs/schedules/assignments/ import { Tabs, TabItem } from '@astrojs/starlight/components'; Assignments show who is on-call during specific time periods. Use assignments to plan coverage, understand rotation patterns, and track historical data. ## Understanding Assignments Each assignment represents: - **Assignment Number**: The generated assignment index since the schedule started - **User ID**: Who is on-call for this assignment - **Start Date**: When this assignment begins (ISO 8601 format) - **End Date**: When this assignment ends (ISO 8601 format) ## Viewing Assignments You can retrieve assignments in three ways: ### By Number of Assignments Get a specific number of upcoming assignments: ```typescript const assignments = await client.schedule.getAssignments(schedule.id, { type: "assignments", count: 5, // Get next 5 assignments }); assignments.forEach((assignment) => { console.log( `Assignment ${assignment.assignmentNumber}: ${assignment.userId}` ); console.log(` ${assignment.startDate} → ${assignment.endDate}`); }); ``` ```bash curl -X GET "https://api.beeps.dev/v0/schedule/sch_xyz789/assignments?type=assignments&count=5" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash beeps schedule assignments --schedule-id sch_xyz789 --count 5 ``` For JSON output: ```bash beeps schedule assignments --schedule-id sch_xyz789 --count 5 --json ``` **Tool:** `beeps_get_schedule_assignments` ```json { "scheduleId": "sch_xyz789", "count": 5 } ``` ### By Number of Days Get all assignments covering a specific number of days: ```typescript const assignments = await client.schedule.getAssignments(schedule.id, { type: "days", count: 14, // Get assignments for next 14 days }); console.log(`Assignments covering next 14 days: ${assignments.length}`); ``` ```bash curl -X GET "https://api.beeps.dev/v0/schedule/sch_xyz789/assignments?type=days&count=14" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash beeps schedule assignments --schedule-id sch_xyz789 --type days --count 14 ``` For JSON output: ```bash beeps schedule assignments --schedule-id sch_xyz789 --type days --count 14 --json ``` **Tool:** `beeps_get_schedule_assignments` ```json { "scheduleId": "sch_xyz789", "type": "days", "count": 14 } ``` ### Until a Specific Date Get all assignments up to a specific date: ```typescript const assignments = await client.schedule.getAssignments(schedule.id, { type: "until", date: "2025-12-31T23:59:59.999Z", // Get assignments until end of year }); console.log(`Assignments until 2025-12-31: ${assignments.length}`); ``` ```bash curl -X GET "https://api.beeps.dev/v0/schedule/sch_xyz789/assignments?type=until&date=2025-12-31T23:59:59.999Z" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash beeps schedule assignments --schedule-id sch_xyz789 --type until --date 2025-12-31T23:59:59.999Z ``` For JSON output: ```bash beeps schedule assignments --schedule-id sch_xyz789 --type until --date 2025-12-31T23:59:59.999Z --json ``` **Tool:** `beeps_get_schedule_assignments` ```json { "scheduleId": "sch_xyz789", "type": "until", "date": "2025-12-31T23:59:59.999Z" } ``` ## API Response ```json { "assignments": [ { "userId": "usr_alice", "startDate": "2025-01-20T09:00:00Z", "endDate": "2025-01-27T09:00:00Z", "assignmentNumber": 7 }, { "userId": "usr_bob", "startDate": "2025-01-27T09:00:00Z", "endDate": "2025-02-03T09:00:00Z", "assignmentNumber": 8 }, { "userId": "usr_charlie", "startDate": "2025-02-03T09:00:00Z", "endDate": "2025-02-10T09:00:00Z", "assignmentNumber": 9 } ] } ``` ## Type Definitions ### GetAssignmentsParams ```typescript type GetAssignmentsParams = | { type?: "assignments"; // Default count?: number; // Number of assignments (optional) } | { type: "until"; date: string | Date; // ISO 8601 date or JavaScript Date } | { type: "days"; count: number; // Number of days }; ``` ### ScheduleAssignment ```typescript type ScheduleAssignment = { userId: string; // User on-call for this assignment startDate: string; // ISO 8601 timestamp endDate: string; // ISO 8601 timestamp assignmentNumber: number; // Generated assignment index since schedule start }; ``` ## Common Use Cases ### Show Who's On-Call This Week ```typescript const onCall = await client.schedule.getOnCall(schedule.id); const assignments = await client.schedule.getAssignments(schedule.id, { type: "days", count: 7, }); const currentAssignment = assignments.find( (a) => a.assignmentNumber === onCall.assignmentNumber ); if (currentAssignment) { console.log(`${onCall.email} is on-call until ${currentAssignment.endDate}`); } ``` ### Display Rotation Calendar ```typescript const assignments = await client.schedule.getAssignments(schedule.id, { type: "days", count: 30, }); console.log("On-Call Calendar (Next 30 Days):\n"); assignments.forEach((assignment) => { const start = new Date(assignment.startDate).toLocaleDateString(); const end = new Date(assignment.endDate).toLocaleDateString(); console.log(`${start} - ${end}: ${assignment.userId}`); }); ``` ### Find Next Assignment for a User ```typescript const assignments = await client.schedule.getAssignments(schedule.id, { type: "assignments", count: 10, }); const userId = "usr_alice"; const nextAssignment = assignments.find((a) => a.userId === userId); if (nextAssignment) { console.log(`${userId}'s next on-call starts: ${nextAssignment.startDate}`); } else { console.log(`${userId} not found in next 10 assignments`); } ``` ### Calculate On-Call Load ```typescript const assignments = await client.schedule.getAssignments(schedule.id, { type: "days", count: 90, }); const userCounts = new Map(); assignments.forEach((assignment) => { const count = userCounts.get(assignment.userId) || 0; userCounts.set(assignment.userId, count + 1); }); console.log("On-Call Load (Next 90 Days):"); userCounts.forEach((count, userId) => { console.log(`${userId}: ${count} assignments`); }); ``` ### Vacation Planning ```typescript function formatDate(date: Date): string { return date.toISOString().split("T")[0]; } const vacationStart = new Date("2025-03-15"); const vacationEnd = new Date("2025-03-22"); const assignments = await client.schedule.getAssignments(schedule.id, { type: "until", date: formatDate(vacationEnd), }); const conflictingAssignments = assignments.filter((assignment) => { const start = new Date(assignment.startDate); const end = new Date(assignment.endDate); return ( assignment.userId === "usr_alice" && start <= vacationEnd && end >= vacationStart ); }); if (conflictingAssignments.length > 0) { console.log("⚠️ On-call assignments during vacation:"); conflictingAssignments.forEach((a) => { console.log(` ${a.startDate} → ${a.endDate}`); }); } else { console.log("✓ No conflicts with vacation dates"); } ``` ## Best Practices All dates are in UTC—convert to local time zones for display. Cache assignments to reduce API calls since they don't change frequently. --- # Schedule API Reference > Source: https://beeps.dev/docs/schedules/api/ Complete API reference for the Schedule resource in the beeps SDK. If you omit `startAt` when creating a schedule, the server defaults it to the current time so the schedule starts immediately. ## Schedule Methods ### `client.schedule.create(input)` Create a new schedule. **Parameters:** - `input: CreateScheduleInput` **Returns:** `Promise` **Example:** ```typescript const schedule = await client.schedule.create({ name: "Weekly On-Call", relayId: "rly_abc123", type: "weekly", handoffDay: "monday", handoffTime: "09:00", externalKey: "prod-oncall", }); ``` ### `client.schedule.list()` List all schedules in your organization. **Returns:** `Promise` **Example:** ```typescript const schedules = await client.schedule.list(); ``` ### `client.schedule.addMember(scheduleId, input)` Add a team member to a schedule rotation. New members usually join at the next handoff boundary so the current on-call assignment is not interrupted. If the schedule has not started yet, they become active at `startAt`. If the schedule is active but currently has no active members, they become active immediately. **Parameters:** - `scheduleId: string` - `input: AddScheduleMemberInput` **Returns:** `Promise` **Examples:** ```typescript // Add by email address const member = await client.schedule.addMember("sch_xyz789", { email: "alice@example.com", }); // Or add by user ID const member = await client.schedule.addMember("sch_xyz789", { userId: "usr_alice", }); // Alice joins based on the schedule timing ``` ### `client.schedule.listMembers(scheduleId)` List active members of a schedule rotation. **Parameters:** - `scheduleId: string` **Returns:** `Promise` **Example:** ```typescript const members = await client.schedule.listMembers("sch_xyz789"); ``` ### `client.schedule.listMembersSafe(scheduleId)` List active schedule members without throwing exceptions. **Parameters:** - `scheduleId: string` **Returns:** `Promise>` **Example:** ```typescript const result = await client.schedule.listMembersSafe("sch_xyz789"); if (result.error) { console.error("Error:", result.error.message); } else { console.log(`Members: ${result.data.length}`); } ``` ### `client.schedule.removeMember(scheduleId, userId)` Remove a team member from a schedule rotation. The member will remain on-call through the end of their current shift, then be removed at the next rotation boundary. Cannot remove a member if it would leave the schedule with no active members. **Parameters:** - `scheduleId: string` - `userId: string` **Returns:** `Promise` **Example:** ```typescript const member = await client.schedule.removeMember("sch_xyz789", "usr_alice"); // Alice completes her current shift, then leaves the rotation ``` ### `client.schedule.getAssignments(scheduleId, params?)` Get schedule assignments. **Parameters:** - `scheduleId: string` - `params?: GetAssignmentsParams` **Returns:** `Promise` **Examples:** ```typescript // Get next 5 assignments const assignments = await client.schedule.getAssignments("sch_xyz789", { type: "assignments", count: 5, }); // Get assignments for next 14 days const assignments = await client.schedule.getAssignments("sch_xyz789", { type: "days", count: 14, }); // Get assignments until specific date (using Date object) const assignments = await client.schedule.getAssignments("sch_xyz789", { type: "until", date: new Date("2025-12-31T23:59:59.999Z"), }); // Or using ISO 8601 string with Z suffix (UTC timezone required) const assignments = await client.schedule.getAssignments("sch_xyz789", { type: "until", date: "2025-12-31T23:59:59.999Z", }); ``` ### `client.schedule.getOnCall(scheduleId)` Get the user currently on-call for a schedule. **Parameters:** - `scheduleId: string` **Returns:** `Promise` **Example:** ```typescript const onCall = await client.schedule.getOnCall("sch_xyz789"); console.log(`${onCall.email} is on-call`); ``` ### `client.schedule.getOnCallSafe(scheduleId)` Get the user currently on-call without throwing exceptions. **Parameters:** - `scheduleId: string` **Returns:** `Promise>` **Example:** ```typescript const result = await client.schedule.getOnCallSafe("sch_xyz789"); if (result.error) { console.error("Error:", result.error.message); } else { console.log(`On-call: ${result.data.email}`); } ``` ## Override Methods ### `client.schedule.createOverride(scheduleId, input)` Create a temporary on-call override. The override user takes over for the specified time window. **Parameters:** - `scheduleId: string` - `input: CreateScheduleOverrideInput` **Returns:** `Promise` **Example:** ```typescript const override = await client.schedule.createOverride("sch_xyz789", { userId: "usr_bob", startAt: "2025-03-15T09:00:00.000Z", endAt: "2025-03-22T09:00:00.000Z", reason: "Vacation coverage", }); ``` ### `client.schedule.listOverrides(scheduleId, params?)` List overrides for a schedule, optionally filtered by date range. **Parameters:** - `scheduleId: string` - `params?: ListScheduleOverridesParams` **Returns:** `Promise` **Example:** ```typescript const overrides = await client.schedule.listOverrides("sch_xyz789"); // With date range filter const overrides = await client.schedule.listOverrides("sch_xyz789", { startAt: "2025-03-01T00:00:00.000Z", endAt: "2025-03-31T23:59:59.999Z", }); ``` ### `client.schedule.updateOverride(scheduleId, overrideId, input)` Update an existing override. All fields are optional. **Parameters:** - `scheduleId: string` - `overrideId: string` - `input: UpdateScheduleOverrideInput` **Returns:** `Promise` **Example:** ```typescript const updated = await client.schedule.updateOverride("sch_xyz789", "ovr_abc123", { endAt: "2025-03-25T09:00:00.000Z", reason: "Extended coverage", }); ``` ### `client.schedule.cancelOverride(scheduleId, overrideId)` Cancel an override. The normal rotation resumes. **Parameters:** - `scheduleId: string` - `overrideId: string` **Returns:** `Promise` **Example:** ```typescript const canceled = await client.schedule.cancelOverride("sch_xyz789", "ovr_abc123"); ``` ## Types ### CreateScheduleInput ```typescript type CreateScheduleInput = { name: string; relayId: string; type: "daily" | "weekly"; handoffDay: | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday"; handoffTime: string; // HH:MM format (24-hour) startAt?: string | Date; externalKey?: string; }; ``` ### Schedule ```typescript type Schedule = { id: string; organizationId: string; relayId: string; name: string; type: "daily" | "weekly"; handoffDay: | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday"; handoffTime: string; startAt: string; externalKey: string | null; createdAt: string; updatedAt: string; deletedAt: string | null; }; ``` ### AddScheduleMemberInput ```typescript type AddScheduleMemberInput = | { userId: string } | { email: string }; ``` ### ScheduleMember ```typescript type ScheduleMember = { id?: string; scheduleId: string; userId: string; createdAt?: string; effectiveAt?: string; removedAt?: string | null; }; ``` ### GetAssignmentsParams ```typescript type GetAssignmentsParams = | { type?: "assignments"; count?: number } | { type: "until"; date: string | Date } | { type: "days"; count: number }; ``` **Note:** When using `type: "until"`, the `date` parameter must be either: - A JavaScript `Date` object (automatically converted to UTC ISO 8601 format) - An ISO 8601 timestamp string with Z suffix (e.g., `"2025-12-31T23:59:59.999Z"`) The SDK validates that string dates include the Z suffix to ensure UTC timezone consistency. Dates without timezone indicators will be rejected with a clear error message. ### ScheduleAssignment ```typescript type ScheduleAssignment = { userId: string; startDate: string; // ISO 8601 timestamp endDate: string; // ISO 8601 timestamp assignmentNumber: number; }; ``` ### BeepsUser ```typescript type BeepsUser = { userId: string; email: string; assignmentNumber: number | null; // Generated assignment index, null when schedule has no members scheduleId: string; isOverride?: boolean; // True if user is on-call via schedule override overrideId?: string; // ID of the schedule override (if applicable) }; ``` ### CreateScheduleOverrideInput ```typescript type CreateScheduleOverrideInput = { userId: string; startAt: string | Date; endAt: string | Date; reason?: string; }; ``` ### UpdateScheduleOverrideInput ```typescript type UpdateScheduleOverrideInput = { startAt?: string | Date; endAt?: string | Date; reason?: string; }; ``` ### ListScheduleOverridesParams ```typescript type ListScheduleOverridesParams = { startAt?: string | Date; endAt?: string | Date; }; ``` ### ScheduleOverride ```typescript type ScheduleOverride = { id: string; organizationId: string; scheduleId: string; userId: string; startAt: string; endAt: string; reason: string | null; createdByUserId: string; createdAt: string; updatedAt: string; canceledAt: string | null; deletedAt: string | null; }; ``` ## HTTP Endpoints ### Create Schedule ``` POST /v0/schedule ``` **Request Body:** ```json { "name": "Weekly On-Call", "relayId": "rly_abc123", "type": "weekly", "handoffDay": "monday", "handoffTime": "09:00", "externalKey": "prod-oncall" } ``` If `startAt` is omitted, the server sets it to the current time. ### List Schedules ``` GET /v0/schedule ``` ### Add Schedule Member ``` POST /v0/schedule/:scheduleId/members ``` Adds a member to the rotation. They will join at the next rotation boundary. Pass either `userId` or `email`. **Request Body:** ```json { "email": "alice@example.com" } ``` Or by user ID: ```json { "userId": "usr_alice" } ``` ### List Schedule Members ``` GET /v0/schedule/:scheduleId/members ``` ### Remove Schedule Member ``` DELETE /v0/schedule/:scheduleId/members/:userId ``` Removes a member from the rotation. They will complete their current shift and leave at the next rotation boundary. Cannot remove a member if it would leave the schedule with no active members. ### Get Assignments ``` GET /v0/schedule/:scheduleId/assignments GET /v0/schedule/:scheduleId/assignments?type=assignments&count=5 GET /v0/schedule/:scheduleId/assignments?type=days&count=14 GET /v0/schedule/:scheduleId/assignments?type=until&date=2025-12-31T23:59:59.999Z ``` **Note:** The `date` parameter for `type=until` must be a valid ISO 8601 timestamp with Z suffix (UTC timezone). Dates without the Z suffix will be rejected with a validation error. ### Get Current On-Call User ``` GET /v0/schedule/:scheduleId/on-call ``` ### Create Override ``` POST /v0/schedule/:scheduleId/overrides ``` **Request Body:** ```json { "userId": "usr_bob", "startAt": "2025-03-15T09:00:00.000Z", "endAt": "2025-03-22T09:00:00.000Z", "reason": "Vacation coverage" } ``` **Note:** `startAt` and `endAt` must be valid ISO 8601 timestamps with Z suffix (UTC timezone). ### List Overrides ``` GET /v0/schedule/:scheduleId/overrides GET /v0/schedule/:scheduleId/overrides?startAt=2025-03-01T00:00:00.000Z&endAt=2025-03-31T23:59:59.999Z ``` ### Update Override ``` PATCH /v0/schedule/:scheduleId/overrides/:overrideId ``` **Request Body:** ```json { "endAt": "2025-03-25T09:00:00.000Z", "reason": "Extended coverage" } ``` ### Cancel Override ``` DELETE /v0/schedule/:scheduleId/overrides/:overrideId ``` ## Authentication All API requests require authentication using your access token: ```bash curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ https://api.beeps.dev/v0/schedule ``` --- # Contact Methods Overview > Source: https://beeps.dev/docs/contact-methods/overview/ Contact methods define how users receive notifications. Each user can have multiple email and SMS contact methods. When a relay rule notifies an on-call user, it uses their configured contact methods. ## Automatic Email Contact Method When you sign up for beeps, your signup email is automatically added as a verified contact method. This means you'll start receiving notifications immediately without any extra setup. ## Managing Contact Methods Contact methods are managed through the beeps web UI at **Settings > Profile**. From there you can: - Add additional email addresses - Add SMS phone numbers (E.164 format, e.g. `+14155551234`) - Remove contact methods you no longer need - Resend verification for unverified SMS numbers ## Transport Types ### Email Email contact methods are automatically verified when added. ### SMS SMS contact methods require verification. After adding a phone number, you'll receive a text message and must reply **YES** to confirm. ## Verification Status Contact methods have a `verified` status: - **Verified**: Confirmed to be working and deliverable - **Unverified**: Not yet confirmed Unverified contact methods receive notifications, but delivery is not guaranteed. Verify methods for reliable delivery. ## Listing Contact Methods via SDK You can list and delete contact methods programmatically: ```typescript const methods = await client.contactMethod.list({ userId: "usr_alice", }); console.log(`Alice has ${methods.length} contact methods`); const emailMethods = methods.filter((m) => m.transport === "email"); const smsMethods = methods.filter((m) => m.transport === "sms"); console.log(`Emails: ${emailMethods.length}, SMS: ${smsMethods.length}`); ``` ### Cleaning Up Old Contact Methods Remove outdated contact information: ```typescript const methods = await client.contactMethod.list({ userId: "usr_alice", }); const oldEmail = methods.find( (m) => m.value === "old-email@example.com" ); if (oldEmail) { await client.contactMethod.delete(oldEmail.id, { userId: "usr_alice", }); } ``` ## Best Practices See [Best Practices - Contact Methods](/docs/best-practices/#contact-methods) for configuration guidelines. --- # Managing Contact Methods > Source: https://beeps.dev/docs/contact-methods/manage/ import { Tabs, TabItem } from '@astrojs/starlight/components'; Contact methods are created and managed through the beeps web UI at **Settings > Profile**. Your signup email is automatically added as a verified contact method. The SDK and API provide read and delete access for programmatic management. ## Listing Contact Methods ```typescript const methods = await client.contactMethod.list({ userId: "usr_alice", }); console.log(`Found ${methods.length} contact methods`); methods.forEach((method) => { console.log(`- ${method.transport}: ${method.value}`); console.log(` Verified: ${method.verified}`); }); ``` ### Filter by Transport Type ```typescript const methods = await client.contactMethod.list({ userId: "usr_alice", }); const emailMethods = methods.filter((m) => m.transport === "email"); const smsMethods = methods.filter((m) => m.transport === "sms"); console.log(`Emails: ${emailMethods.length}`); console.log(`SMS: ${smsMethods.length}`); ``` ```bash curl -X GET "https://api.beeps.dev/v0/contact-methods?userId=usr_alice" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "contactMethods": [ { "id": "cm_abc123", "userId": "usr_alice", "transport": "email", "value": "alice@example.com", "verified": true, "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z", "deletedAt": null }, { "id": "cm_def456", "userId": "usr_alice", "transport": "sms", "value": "+14155551234", "verified": false, "createdAt": "2025-01-15T11:00:00Z", "updatedAt": "2025-01-15T11:00:00Z", "deletedAt": null } ] } ``` ## Deleting a Contact Method ```typescript const result = await client.contactMethod.delete("cm_abc123", { userId: "usr_alice", }); console.log("Deleted successfully:", result.success); ``` ### Safe Method ```typescript const result = await client.contactMethod.deleteSafe("cm_abc123", { userId: "usr_alice", }); if (result.error) { console.error("Failed to delete:", result.error.message); } else { console.log("Deleted:", result.data.success); } ``` ```bash curl -X DELETE "https://api.beeps.dev/v0/contact-methods/cm_abc123?userId=usr_alice" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "success": true } ``` ## Type Definitions ### ContactMethod ```typescript type ContactMethod = { id: string; userId: string; transport: string; value: string; verified: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; }; ``` ### ListContactMethodsParams ```typescript type ListContactMethodsParams = { userId: string; }; ``` ### DeleteContactMethodParams ```typescript type DeleteContactMethodParams = { userId: string; }; ``` ## Troubleshooting **No one receiving notifications?** See [Troubleshooting - No One Is Notified](/docs/troubleshooting/#alerts-appear-but-no-one-is-notified). --- # Contact Methods API Reference > Source: https://beeps.dev/docs/contact-methods/api/ Complete API reference for the Contact Method resource in the beeps SDK. ## Contact Method Methods ### `client.contactMethod.list(params)` List all contact methods for a user. **Parameters:** - `params: ListContactMethodsParams` **Returns:** `Promise` **Example:** ```typescript const methods = await client.contactMethod.list({ userId: "usr_alice", }); ``` ### `client.contactMethod.delete(id, params)` Delete a contact method. **Parameters:** - `id: string` - Contact method ID - `params: DeleteContactMethodParams` **Returns:** `Promise<{ success: boolean }>` **Example:** ```typescript await client.contactMethod.delete("cm_abc123", { userId: "usr_alice", }); ``` ## Safe Methods All methods have corresponding `*Safe` variants that return `Result` instead of throwing: ### `client.contactMethod.listSafe(params)` **Returns:** `Promise>` ### `client.contactMethod.deleteSafe(id, params)` **Returns:** `Promise>` ## Types ### ContactMethodTransport ```typescript type ContactMethodTransport = "email" | "sms"; ``` ### ContactMethod ```typescript type ContactMethod = { id: string; userId: string; transport: string; value: string; verified: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; }; ``` ### ListContactMethodsParams ```typescript type ListContactMethodsParams = { userId: string; }; ``` ### DeleteContactMethodParams ```typescript type DeleteContactMethodParams = { userId: string; }; ``` ## HTTP Endpoints ### List Contact Methods ``` GET /v0/contact-methods?userId=usr_alice ``` **Query Parameters:** - `userId` (required): User ID to list contact methods for ### Delete Contact Method ``` DELETE /v0/contact-methods/:id?userId=usr_alice ``` **Query Parameters:** - `userId` (required): User ID (for authorization) ## Authentication All API requests require authentication using your access token: ```bash curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ https://api.beeps.dev/v0/contact-methods?userId=usr_alice ``` --- # Alerts Overview > Source: https://beeps.dev/docs/alerts/overview/ Alerts are incidents received via webhooks from your monitoring systems. They have a severity level (critical, high, medium, low, info), flow through relays, and can be responded to, assigned, and resolved. ## Alert Lifecycle 1. **Created**: Alert is received via webhook or integration 2. **Routed**: Alert flows through relay rules 3. **Notified**: On-call users are notified via their contact methods 4. **Responded**: One or more responders signal they're on it 5. **Assigned**: Alert is assigned to a specific user (optional) 6. **Resolved**: Alert is marked as resolved Multiple responders can join the same alert. Each responder tracks their own status (`on_it`, `done`, `dropped`) independently. ## Alert Severity Levels Alerts have five severity levels: - **Critical**: Immediate attention required, service down - **High**: Important issue requiring prompt attention - **Medium**: Non-urgent issue that should be addressed - **Low**: Minor issue with minimal impact - **Info**: Informational notification, no action required ```typescript const alerts = await client.alert.list(); const criticalAlerts = alerts.filter((a) => a.severity === "critical"); console.log(`${criticalAlerts.length} critical alerts`); ``` ## Alert States ### Active Alerts Alerts that haven't been resolved: ```typescript const activeAlerts = await client.alert.listActive(); console.log(`${activeAlerts.length} alerts need attention`); ``` ### Resolved Alerts Alerts that have been resolved: ```typescript const resolvedAlerts = await client.alert.listResolved(); console.log(`${resolvedAlerts.length} alerts resolved`); ``` ## Common Operations ### Responding to an Alert Signal that you're working on an alert: ```typescript const responder = await client.alert.onIt("alr_abc123", { userId: "usr_alice", }); console.log(`${responder.userId} is on it (joined ${responder.joinedAt})`); ``` ### Assigning an Alert Assign an alert to a specific user: ```typescript const assigned = await client.alert.assign("alr_abc123", "usr_bob"); console.log(`Assigned to ${assigned.assignedToUserId}`); ``` ### Resolving an Alert Mark an alert as resolved: ```typescript const resolved = await client.alert.resolve("alr_abc123"); console.log(`Resolved at ${resolved.resolvedAt}`); ``` ## Alert Metadata Alerts can include arbitrary metadata for context: ```typescript const alert = await client.alert.get("alr_abc123"); console.log("Alert metadata:", alert.metadata); // Example: { server: "web-01", region: "us-west", errorRate: 15.3 } ``` ## Common Use Cases ### Monitoring Active Alerts ```typescript const active = await client.alert.listActive(); console.log(`Active Alerts: ${active.length}\n`); active.forEach((alert) => { console.log(`[${alert.severity.toUpperCase()}] ${alert.title}`); console.log(` Source: ${alert.source}`); if (alert.assignedToUserId) { console.log(` Assigned to: ${alert.assignedToUserId}`); } console.log(); }); ``` ### On-Call Dashboard ```typescript const active = await client.alert.listActive(); const byUser = new Map(); active.forEach((alert) => { if (alert.assignedToUserId) { const count = byUser.get(alert.assignedToUserId) || 0; byUser.set(alert.assignedToUserId, count + 1); } }); console.log("Alerts by User:"); byUser.forEach((count, userId) => { console.log(`${userId}: ${count} alerts`); }); ``` ### Alert Workflow ```typescript const alertId = "alr_abc123"; const userId = "usr_alice"; const alert = await client.alert.get(alertId); console.log(`Working on: ${alert.title}`); const responder = await client.alert.onIt(alertId, { userId }); console.log("On it"); await client.alert.assign(alertId, userId); console.log("Assigned to self"); console.log("Investigating..."); await client.alert.updateResponderStatus(alertId, responder.id, { status: "done", }); console.log("Responder done"); await client.alert.resolve(alertId); console.log("Resolved"); ``` --- # Managing Alerts > Source: https://beeps.dev/docs/alerts/manage/ import { Tabs, TabItem } from '@astrojs/starlight/components'; Manage alerts throughout their lifecycle. ## Listing All Alerts ```typescript import { BeepsClient } from "@beepsdev/sdk"; const client = new BeepsClient({ accessToken: process.env.BEEPS_ACCESS_TOKEN, }); const alerts = await client.alert.list(); console.log(`Found ${alerts.length} total alerts`); alerts.forEach((alert) => { console.log(`[${alert.severity}] ${alert.title}`); }); ``` ```bash curl -X GET https://api.beeps.dev/v0/alerts \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "alerts": [ { "id": "alr_abc123", "organizationId": "org_xyz789", "webhookId": "wh_def456", "title": "High CPU usage on web-01", "message": "CPU usage has exceeded 90% for 5 minutes", "severity": "high", "source": "monitoring", "externalId": "mon-12345", "metadata": { "server": "web-01", "cpu": 92.5 }, "assignedToUserId": null, "resolvedAt": null, "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z" } ] } ``` ```bash beeps alert list beeps alert list --json ``` **Tool:** `beeps_list_alerts` ```json { "status": "all" } ``` ## Listing Active Alerts ```typescript const activeAlerts = await client.alert.listActive(); console.log(`${activeAlerts.length} active alerts need attention`); ``` ### Safe Method ```typescript const result = await client.alert.listActiveSafe(); if (result.error) { console.error("Failed to list alerts:", result.error.message); } else { console.log(`Active alerts: ${result.data.length}`); } ``` ```bash curl -X GET https://api.beeps.dev/v0/alerts/active \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash beeps alert list --active beeps alert list --active --json ``` **Tool:** `beeps_list_alerts` ```json { "status": "active" } ``` Or use the dedicated tool: **Tool:** `beeps_list_active_alerts` ```json {} ``` ## Listing Resolved Alerts ```typescript const resolvedAlerts = await client.alert.listResolved(); console.log(`${resolvedAlerts.length} alerts have been resolved`); ``` ```bash curl -X GET https://api.beeps.dev/v0/alerts/resolved \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash beeps alert list --resolved ``` **Tool:** `beeps_list_alerts` ```json { "status": "resolved" } ``` ## Getting a Specific Alert ```typescript const alert = await client.alert.get("alr_abc123"); console.log(`Title: ${alert.title}`); console.log(`Severity: ${alert.severity}`); console.log(`Status: ${alert.resolvedAt ? "Resolved" : "Active"}`); ``` ```bash curl -X GET https://api.beeps.dev/v0/alerts/alr_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash beeps alert get --alert-id alr_abc123 beeps alert get --alert-id alr_abc123 --json ``` **Tool:** `beeps_get_alert` ```json { "alertId": "alr_abc123" } ``` ## Responding to an Alert Signal that you're working on an alert. This creates a responder record. ```typescript const responder = await client.alert.onIt("alr_abc123", { userId: "usr_alice", }); console.log(`${responder.userId} is on it (status: ${responder.status})`); console.log(`Joined at ${responder.joinedAt}`); ``` If `userId` is not provided, the authenticated user is used: ```typescript const responder = await client.alert.onIt("alr_abc123"); ``` ### Safe Method ```typescript const result = await client.alert.onItSafe("alr_abc123", { userId: "usr_alice", }); if (result.error) { console.error("Failed to respond:", result.error.message); } else { console.log(`Responder joined: ${result.data.id}`); } ``` ```bash curl -X POST https://api.beeps.dev/v0/alerts/alr_abc123/on-it \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "userId": "usr_alice" }' ``` **Response:** ```json { "responder": { "id": "rsp_def456", "alertId": "alr_abc123", "organizationId": "org_xyz789", "responderType": "user", "userId": "usr_alice", "status": "on_it", "joinedAt": "2025-01-15T10:35:00Z", "completedAt": null } } ``` ```bash beeps alert on-it --alert-id alr_abc123 beeps alert on-it --alert-id alr_abc123 --user-id usr_alice ``` **Tool:** `beeps_on_it` ```json { "alertId": "alr_abc123" } ``` With a specific user: ```json { "alertId": "alr_abc123", "userId": "usr_alice" } ``` ## Listing Responders ```typescript const responders = await client.alert.listResponders("alr_abc123"); responders.forEach((r) => { console.log(`${r.userId || r.integrationId} — ${r.status}`); if (r.prUrl) { console.log(` PR: ${r.prUrl}`); } }); ``` ### Safe Method ```typescript const result = await client.alert.listRespondersSafe("alr_abc123"); if (result.error) { console.error("Failed to list responders:", result.error.message); } else { console.log(`${result.data.length} responders`); } ``` ```bash curl -X GET https://api.beeps.dev/v0/alerts/alr_abc123/responders \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "responders": [ { "id": "rsp_def456", "alertId": "alr_abc123", "organizationId": "org_xyz789", "responderType": "user", "userId": "usr_alice", "status": "on_it", "joinedAt": "2025-01-15T10:35:00Z", "completedAt": null } ] } ``` ```bash beeps alert responders --alert-id alr_abc123 beeps alert responders --alert-id alr_abc123 --json ``` **Tool:** `beeps_list_alert_responders` ```json { "alertId": "alr_abc123" } ``` ## Updating Responder Status Mark a responder as `done` or `dropped`. You can optionally attach a PR URL when completing work. ```typescript const updated = await client.alert.updateResponderStatus( "alr_abc123", "rsp_def456", { status: "done", prUrl: "https://github.com/org/repo/pull/42", }, ); console.log(`Responder status: ${updated.status}`); console.log(`Completed at: ${updated.completedAt}`); ``` ### Safe Method ```typescript const result = await client.alert.updateResponderStatusSafe( "alr_abc123", "rsp_def456", { status: "done" }, ); if (result.error) { console.error("Failed to update:", result.error.message); } else { console.log("Responder marked as done"); } ``` ```bash curl -X PATCH https://api.beeps.dev/v0/alerts/alr_abc123/responders/rsp_def456 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "status": "done", "prUrl": "https://github.com/org/repo/pull/42" }' ``` **Response:** ```json { "responder": { "id": "rsp_def456", "status": "done", "prUrl": "https://github.com/org/repo/pull/42", "completedAt": "2025-01-15T11:00:00Z" } } ``` **Tool:** `beeps_update_responder_status` ```json { "alertId": "alr_abc123", "responderId": "rsp_def456", "status": "done", "prUrl": "https://github.com/org/repo/pull/42" } ``` ## Assigning an Alert ```typescript const assigned = await client.alert.assign("alr_abc123", "usr_bob"); console.log(`Alert assigned to ${assigned.assignedToUserId}`); ``` ### Safe Method ```typescript const result = await client.alert.assignSafe("alr_abc123", "usr_bob"); if (result.error) { console.error("Failed to assign:", result.error.message); } else { console.log(`Assigned to ${result.data.assignedToUserId}`); } ``` ```bash curl -X POST https://api.beeps.dev/v0/alerts/alr_abc123/assign \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "userId": "usr_bob" }' ``` **Response:** ```json { "alert": { "id": "alr_abc123", "assignedToUserId": "usr_bob", "updatedAt": "2025-01-15T10:40:00Z" } } ``` ```bash beeps alert assign --alert-id alr_abc123 --user-id usr_bob ``` **Tool:** `beeps_assign_alert` ```json { "alertId": "alr_abc123", "userId": "usr_bob" } ``` ## Resolving an Alert ```typescript const resolved = await client.alert.resolve("alr_abc123"); console.log(`Alert resolved at ${resolved.resolvedAt}`); ``` ### Safe Method ```typescript const result = await client.alert.resolveSafe("alr_abc123"); if (result.error) { console.error("Failed to resolve:", result.error.message); } else { console.log("Alert resolved"); } ``` ```bash curl -X POST https://api.beeps.dev/v0/alerts/alr_abc123/resolve \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "alert": { "id": "alr_abc123", "resolvedAt": "2025-01-15T10:45:00Z", "updatedAt": "2025-01-15T10:45:00Z" } } ``` ```bash beeps alert resolve --alert-id alr_abc123 ``` **Tool:** `beeps_resolve_alert` ```json { "alertId": "alr_abc123" } ``` ## Type Definitions ### Alert ```typescript type Alert = { id: string; organizationId: string; webhookId: string; title: string; message: string | null; severity: AlertSeverity; source: string; externalId: string | null; metadata: Record | null; assignedToUserId: string | null; resolvedAt: string | null; resolvedBy: ResolutionSource | null; resolvedByUserId: string | null; resolvedByProvider: string | null; createdAt: string; updatedAt: string; }; ``` ### AlertSeverity ```typescript type AlertSeverity = "critical" | "high" | "medium" | "low" | "info"; ``` ### ResolutionSource ```typescript type ResolutionSource = "user" | "monitoring_system"; ``` ### AlertResponder ```typescript type AlertResponder = { id: string; alertId: string; organizationId: string; responderType: ResponderType; userId: string | null; integrationId: string | null; agentSessionId: string | null; status: ResponderStatus; prUrl: string | null; joinedAt: string; completedAt: string | null; }; ``` ### ResponderType ```typescript type ResponderType = "user" | "agent"; ``` ### ResponderStatus ```typescript type ResponderStatus = "on_it" | "done" | "dropped"; ``` ### OnItInput ```typescript type OnItInput = { userId?: string; }; ``` ### UpdateResponderStatusInput ```typescript type UpdateResponderStatusInput = { status: "done" | "dropped"; prUrl?: string; }; ``` ## Common Patterns ### Complete Alert Workflow ```typescript const alertId = "alr_abc123"; const userId = "usr_alice"; const alert = await client.alert.get(alertId); console.log(`New alert: ${alert.title}`); const responder = await client.alert.onIt(alertId, { userId }); console.log(`On it: ${responder.status}`); await client.alert.assign(alertId, userId); console.log("Assigned to self"); console.log("Working on the issue..."); await client.alert.updateResponderStatus(alertId, responder.id, { status: "done", prUrl: "https://github.com/org/repo/pull/42", }); console.log("Responder done"); await client.alert.resolve(alertId); console.log("Resolved"); ``` ### Filter Alerts by Severity ```typescript const active = await client.alert.listActive(); const critical = active.filter((a) => a.severity === "critical"); const high = active.filter((a) => a.severity === "high"); console.log(`Critical: ${critical.length}`); console.log(`High: ${high.length}`); ``` ### Alert Dashboard ```typescript const active = await client.alert.listActive(); const stats = { total: active.length, assigned: active.filter((a) => a.assignedToUserId).length, critical: active.filter((a) => a.severity === "critical").length, }; console.log("Alert Dashboard:"); console.log(`Total Active: ${stats.total}`); console.log(`Assigned: ${stats.assigned}`); console.log(`Critical: ${stats.critical}`); ``` ### My Assigned Alerts ```typescript const userId = "usr_alice"; const active = await client.alert.listActive(); const myAlerts = active.filter((a) => a.assignedToUserId === userId); console.log(`You have ${myAlerts.length} assigned alerts:`); myAlerts.forEach((alert) => { console.log(`- [${alert.severity}] ${alert.title}`); }); ``` ## Troubleshooting **Alerts not appearing?** See [Troubleshooting - Alerts Not Appearing](/docs/troubleshooting/#alerts-are-not-appearing). --- # Alert API Reference > Source: https://beeps.dev/docs/alerts/api/ Complete API reference for the Alert resource in the beeps SDK. ## Alert Methods ### `client.alert.list()` List all alerts in your organization. **Returns:** `Promise` **Example:** ```typescript const alerts = await client.alert.list(); ``` ### `client.alert.listActive()` List all active (unresolved) alerts. **Returns:** `Promise` **Example:** ```typescript const activeAlerts = await client.alert.listActive(); ``` ### `client.alert.listResolved()` List all resolved alerts. **Returns:** `Promise` **Example:** ```typescript const resolvedAlerts = await client.alert.listResolved(); ``` ### `client.alert.get(alertId)` Get a specific alert by ID. **Parameters:** - `alertId: string` **Returns:** `Promise` **Example:** ```typescript const alert = await client.alert.get("alr_abc123"); ``` ### `client.alert.onIt(alertId, input?)` Signal that a user is responding to an alert. Creates a responder record. If `userId` is not provided, the authenticated user is used. **Parameters:** - `alertId: string` - `input?: OnItInput` **Returns:** `Promise` **Example:** ```typescript const responder = await client.alert.onIt("alr_abc123", { userId: "usr_alice", }); // Or as the authenticated user const responder = await client.alert.onIt("alr_abc123"); ``` ### `client.alert.listResponders(alertId)` List all responders for an alert. **Parameters:** - `alertId: string` **Returns:** `Promise` **Example:** ```typescript const responders = await client.alert.listResponders("alr_abc123"); ``` ### `client.alert.listAgents(alertId)` List agent jobs associated with an alert. **Parameters:** - `alertId: string` **Returns:** `Promise` **Example:** ```typescript const jobs = await client.alert.listAgents("alr_abc123"); ``` ### `client.alert.getFixContext(alertId)` Get a foreground fix context bundle for an alert, including responders, agent jobs, and suggested next actions. **Parameters:** - `alertId: string` **Returns:** `Promise` **Example:** ```typescript const fixContext = await client.alert.getFixContext("alr_abc123"); console.log(fixContext.nextActions); ``` ### `client.alert.updateResponderStatus(alertId, responderId, input)` Update a responder's status to `done` or `dropped`. Optionally attach a PR URL. **Parameters:** - `alertId: string` - `responderId: string` - `input: UpdateResponderStatusInput` **Returns:** `Promise` **Example:** ```typescript const updated = await client.alert.updateResponderStatus( "alr_abc123", "rsp_def456", { status: "done", prUrl: "https://github.com/org/repo/pull/42" }, ); ``` ### `client.alert.resolve(alertId)` Mark an alert as resolved. **Parameters:** - `alertId: string` **Returns:** `Promise` **Example:** ```typescript const alert = await client.alert.resolve("alr_abc123"); ``` ### `client.alert.assign(alertId, userId)` Assign an alert to a user. **Parameters:** - `alertId: string` - `userId: string` **Returns:** `Promise` **Example:** ```typescript const alert = await client.alert.assign("alr_abc123", "usr_bob"); ``` ## Safe Methods All methods have corresponding `*Safe` variants that return `Result` instead of throwing: - `client.alert.listSafe()` - `client.alert.listActiveSafe()` - `client.alert.listResolvedSafe()` - `client.alert.getSafe(alertId)` - `client.alert.onItSafe(alertId, input?)` - `client.alert.listRespondersSafe(alertId)` - `client.alert.listAgentsSafe(alertId)` - `client.alert.getFixContextSafe(alertId)` - `client.alert.updateResponderStatusSafe(alertId, responderId, input)` - `client.alert.resolveSafe(alertId)` - `client.alert.assignSafe(alertId, userId)` **Example:** ```typescript const result = await client.alert.onItSafe("alr_abc123", { userId: "usr_alice", }); if (result.error) { console.error(result.error.message); } else { console.log(`Responder joined: ${result.data.id}`); } ``` ## Types ### AlertSeverity ```typescript type AlertSeverity = "critical" | "high" | "medium" | "low" | "info"; ``` ### Alert ```typescript type Alert = { id: string; organizationId: string; webhookId: string; title: string; message: string | null; severity: AlertSeverity; source: string; externalId: string | null; metadata: Record | null; assignedToUserId: string | null; resolvedAt: string | null; resolvedBy: ResolutionSource | null; resolvedByUserId: string | null; resolvedByProvider: string | null; createdAt: string; updatedAt: string; }; ``` ### ResolutionSource ```typescript type ResolutionSource = "user" | "monitoring_system"; ``` ### AlertResponder ```typescript type AlertResponder = { id: string; alertId: string; organizationId: string; responderType: ResponderType; userId: string | null; integrationId: string | null; agentSessionId: string | null; status: ResponderStatus; prUrl: string | null; joinedAt: string; completedAt: string | null; }; ``` ### ResponderType ```typescript type ResponderType = "user" | "agent"; ``` ### ResponderStatus ```typescript type ResponderStatus = "on_it" | "done" | "dropped"; ``` ### OnItInput ```typescript type OnItInput = { userId?: string; }; ``` ### UpdateResponderStatusInput ```typescript type UpdateResponderStatusInput = { status: "done" | "dropped"; prUrl?: string; }; ``` ## HTTP Endpoints ### List All Alerts ``` GET /v0/alerts ``` ### List Active Alerts ``` GET /v0/alerts/active ``` ### List Resolved Alerts ``` GET /v0/alerts/resolved ``` ### Get Alert ``` GET /v0/alerts/:alertId ``` ### Respond to Alert (On It) ``` POST /v0/alerts/:alertId/on-it ``` **Request Body:** ```json { "userId": "usr_alice" } ``` ### List Responders ``` GET /v0/alerts/:alertId/responders ``` ### List Agent Jobs for Alert ``` GET /v0/alerts/:alertId/agents ``` ### Get Fix Context ``` GET /v0/alerts/:alertId/fix-context ``` ### Update Responder Status ``` PATCH /v0/alerts/:alertId/responders/:responderId ``` **Request Body:** ```json { "status": "done", "prUrl": "https://github.com/org/repo/pull/42" } ``` ### Resolve Alert ``` POST /v0/alerts/:alertId/resolve ``` ### Assign Alert ``` POST /v0/alerts/:alertId/assign ``` **Request Body:** ```json { "userId": "usr_bob" } ``` ## Authentication All API requests require authentication using your access token: ```bash curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ https://api.beeps.dev/v0/alerts ``` --- # Alert Storms Overview > Source: https://beeps.dev/docs/alert-storms/overview/ An alert storm happens when a single root cause (e.g., a database going down) triggers many different alerts — API timeouts, connection refused errors, query failures, and so on. Without storm detection, each unique alert title would dispatch its own agent, leading to multiple agents fixing symptoms instead of the root cause. ## How Storm Detection Works Storm detection is purely count-based. No AI is involved in the detection step — it simply counts alerts per relay within a time window. ### Normal Flow (No Storm) When alerts arrive at a normal rate: 1. Alert arrives via webhook 2. Relay rules execute 3. Agent rule dispatches a coding agent 4. Agent investigates and creates a PR ### Storm Flow When a burst of alerts arrives: 1. First 2 alerts dispatch agents immediately (configurable via `maxImmediateDispatches`) 2. Once the alert count exceeds the threshold within the time window, a storm is detected 3. Subsequent alerts are held — no additional agents are dispatched 4. After a debounced delay, an AI triage step analyzes all storm alerts 5. The triage identifies the root cause alert 6. A single agent is dispatched for the root cause with full storm context ## Storm Lifecycle | Status | Description | |--------|-------------| | `collecting` | Storm detected, alerts are being collected | | `triaging` | AI is analyzing the alerts to find the root cause | | `dispatched` | Root cause identified, agent dispatched | | `resolved` | Storm resolved (future enhancement) | ## Relationship to Alert Deduplication Storm detection is a layer above the existing same-title dedup: - **Same-title dedup** prevents duplicate agents for alerts with identical (normalized) titles - **Storm detection** prevents excessive agents when many _different_ alert titles fire from a shared root cause Both checks run in sequence. Storm detection runs first — if it holds an alert, the same-title dedup is skipped entirely. ## Configuration Storm behavior is configured per agent rule. See [Tuning Storm Detection](/docs/alert-storms/tuning/) for configuration options. ```typescript const rule = { ruleType: "agent", config: { agentType: "devin", integrationId: "int_abc123", stormThreshold: 5, stormWindowSeconds: 60, maxImmediateDispatches: 2, }, }; ``` ## Fail-Open Design Storm detection is designed to never block alerts: - If the storm detection database query fails, the alert proceeds to normal dispatch - If the AI triage fails, the earliest high-severity alert is selected as root cause - Existing alert routing, notifications, and acknowledgment are unaffected by storm detection --- # Tuning Storm Detection > Source: https://beeps.dev/docs/alert-storms/tuning/ Storm detection is configured per agent rule, so each relay can have independent thresholds tuned to its alert volume and criticality. ## Configuration Options ### `stormThreshold` The number of alerts within the time window that triggers a storm. - **Type:** `number` - **Default:** `5` - **Range:** `2` to `50` ```typescript { stormThreshold: 5 } ``` ### `stormWindowSeconds` The time window (in seconds) used to count alerts for storm detection. - **Type:** `number` - **Default:** `60` - **Range:** `10` to `300` ```typescript { stormWindowSeconds: 60 } ``` ### `maxImmediateDispatches` The number of agents dispatched immediately before storm hold kicks in. These first agents start working right away while the storm is still being evaluated. - **Type:** `number` - **Default:** `2` - **Range:** `0` to `5` ```typescript { maxImmediateDispatches: 2 } ``` Set to `0` to hold all dispatches during a storm. Set to a higher number if you want more agents working in parallel before triage. ## Examples ### High-Traffic Relay For a relay that receives many alerts normally, increase the threshold to avoid false storm detection: ```typescript { agentType: "devin", integrationId: "int_abc123", stormThreshold: 15, stormWindowSeconds: 120, maxImmediateDispatches: 3, } ``` ### Critical Relay For a relay where every alert matters and storms should be detected quickly: ```typescript { agentType: "cursor", integrationId: "int_def456", repository: "https://github.com/org/critical-service", stormThreshold: 3, stormWindowSeconds: 30, maxImmediateDispatches: 1, } ``` ## Debounced Triage Window When a storm is detected, the triage job doesn't fire immediately. Instead, it uses a debounced delay to wait for more alerts to arrive: - Each new alert pushes the triage job forward by **15 seconds** - The maximum delay is capped at **90 seconds** from when the storm was first detected - This ensures late-arriving alerts (which may include the actual root cause) are included in triage For example, if a storm is detected at `T=0`: - Alert at `T=5s` pushes triage to `T=20s` - Alert at `T=10s` pushes triage to `T=25s` - Alert at `T=20s` pushes triage to `T=35s` - No more alerts arrive — triage fires at `T=35s` If alerts keep arriving, triage fires at most at `T=90s` regardless. ## AI Triage After the debounce window, AI analyzes all collected storm alerts. The triage considers: 1. **Timing** — earlier alerts are more likely to be the root cause 2. **Infrastructure level** — lower-level alerts (database, network) rank higher than application-level alerts 3. **Severity** — higher severity alerts with specific error details are more informative 4. **Error specificity** — alerts with stack traces, connection errors, or specific error codes are preferred 5. **Causal relationships** — patterns where one failure causes others (e.g., DB down causing API timeouts) The identified root cause alert is then dispatched to a coding agent with the full storm context, so the agent understands it needs to fix the underlying issue rather than a symptom. --- # Integrations Overview > Source: https://beeps.dev/docs/observability-integrations/overview/ Point your monitoring tools at a relay webhook URL and beeps handles the rest. ## Getting your webhook URL Each relay has a webhook URL in the format `https://hooks.beeps.dev/YOUR_WEBHOOK_ID`. Grab it from your relay in the dashboard and paste it into your monitoring provider's webhook settings. ## Supported providers - [Sentry](/docs/observability-integrations/sentry/) - [Datadog](/docs/observability-integrations/datadog/) - [Axiom](/docs/observability-integrations/axiom/) - [Custom webhooks](/docs/observability-integrations/custom/) — anything that can POST JSON ## How it works 1. Add your relay webhook URL to your monitoring provider 2. Configure alert rules in the provider (what triggers a webhook) 3. When a webhook fires, beeps creates an alert and runs your relay rules ## Testing Fire a test alert from your monitoring tool and check the dashboard. If nothing shows up, see [Troubleshooting](/docs/troubleshooting/#alerts-are-not-appearing). --- # Sentry > Source: https://beeps.dev/docs/observability-integrations/sentry/ Configure Sentry to send error alerts to beeps via webhooks. ## Prerequisites - Active Sentry account and project - beeps relay webhook URL ## Setup Steps 1. In Sentry, navigate to **Settings** → **Integrations** 2. Search for and select **Webhooks** 3. Click **Add to Project** or **Configure** 4. Enter your beeps webhook URL: `https://hooks.beeps.dev/YOUR_WEBHOOK_ID` 5. Select the events you want to trigger alerts: - Error events - Issue state changes - New releases 6. Save the webhook configuration ## Alert Rules Create alert rules to control when notifications are sent: 1. Go to **Alerts** → **Create Alert** 2. Choose **Issues** or **Errors** 3. Set your conditions (e.g., "When an event is seen more than 100 times in 1 hour") 4. Under **Actions**, select **Send a notification via an integration** 5. Choose your beeps webhook 6. Save the rule ## What happens next Once Sentry fires the webhook, beeps creates an alert and runs it through your relay's rules. What happens from there depends on how you've set up your [relay rules](/docs/relays/rules/) — notify on-call, invoke an agent, post to Slack, or any combination. ## Next Steps - [Configure Relay Rules](/docs/relays/rules/) to route alerts to humans and AI agents - [Create Schedules](/docs/schedules/overview/) to define on-call rotations - [Add Contact Methods](/docs/contact-methods/overview/) for email and SMS notifications --- # Datadog > Source: https://beeps.dev/docs/observability-integrations/datadog/ Configure Datadog monitors to send alerts to beeps via webhooks. ## Prerequisites - Active Datadog account - beeps relay webhook URL ## Setup Steps ### Create Webhook Integration 1. In Datadog, navigate to **Integrations** → **Webhooks** 2. Click **New** to create a webhook 3. Name your webhook (e.g., "beeps Production") 4. Enter URL: `https://hooks.beeps.dev/YOUR_WEBHOOK_ID` 5. Optionally customize the payload template (see below) 6. Save the webhook ### Configure Monitors Add the webhook to your monitors: 1. Create or edit a monitor 2. In the **Notify your team** section, add `@webhook-beeps-dev-production` (using your webhook name) 3. Customize the message with monitor details 4. Save the monitor ## Alert Renotification Configure monitors to re-notify if not resolved: 1. In monitor settings, enable **Renotify if this monitor stays in alert state** 2. Set interval (e.g., every 60 minutes) 3. Include in message: `This is a reminder that the alert is still active` ## What happens next When a Datadog monitor triggers, beeps normalizes the payload into an alert and runs your relay rules. Renotification webhooks from Datadog create follow-up alerts if the original is still active. ## Next Steps - [Configure Relay Rules](/docs/relays/rules/) to route alerts to humans and AI agents - [Create Schedules](/docs/schedules/overview/) to define on-call rotations - [Add Contact Methods](/docs/contact-methods/overview/) for email and SMS notifications --- # Axiom > Source: https://beeps.dev/docs/observability-integrations/axiom/ Configure Axiom monitors to send alerts to beeps via webhooks. ## Prerequisites - Active Axiom account - beeps relay webhook URL ## Setup Steps 1. In Axiom, navigate to **Monitors** 2. Create a new monitor or edit an existing one 3. Configure your query and alert conditions 4. In **Notifications**, select **Webhook** 5. Enter URL: `https://hooks.beeps.dev/YOUR_WEBHOOK_ID` 6. Choose notification frequency and threshold 7. Save the monitor ## Webhook Payload Axiom sends webhooks with this structure: ```json { "monitor": { "id": "mon_abc123", "name": "High 5xx Error Rate", "description": "Alert when 5xx errors exceed threshold", "query": "['http-logs'] | where status >= 500" }, "alert": { "state": "triggered", "value": 25, "threshold": 10, "operator": ">" }, "timestamp": "2024-10-27T10:30:00Z", "queryUrl": "https://app.axiom.co/...", "dataset": "http-logs" } ``` ## Notification Settings ### Frequency Options - **Every time**: Alert on every evaluation that meets threshold - **Once**: Alert only once when condition is first met - **Every X minutes**: Rate limit notifications ### Alert Resolution Configure when to send "resolved" notifications: ```typescript { "sendResolved": true, "resolutionThreshold": { "operator": "<=", "value": 5 } } ``` ## What happens next When an Axiom monitor fires, beeps creates an alert from the payload and runs your relay rules. If you've configured `sendResolved: true`, Axiom will also send a resolution webhook that beeps can use to auto-resolve the alert. ## Next Steps - [Configure Relay Rules](/docs/relays/rules/) to route alerts to humans and AI agents - [Create Schedules](/docs/schedules/overview/) to define on-call rotations - [Add Contact Methods](/docs/contact-methods/overview/) for email and SMS notifications --- # Custom Webhooks > Source: https://beeps.dev/docs/observability-integrations/custom/ Send alerts to beeps from any system using HTTP POST requests. ## Webhook Format beeps relays accept webhook POST requests at: ``` https://hooks.beeps.dev/YOUR_WEBHOOK_ID ``` ## Basic Example Send a simple alert: ```bash curl -X POST https://hooks.beeps.dev/YOUR_WEBHOOK_ID \ -H "Content-Type: application/json" \ -d '{ "title": "Database Connection Failed", "message": "Unable to connect to production database", "severity": "critical" }' ``` ## Payload Structure ### Required Fields - `title` (string): Short alert title (max 255 characters) ### Optional Fields - `message` (string): Detailed alert description - `severity` (string): Alert severity level - `low` / `info` - `medium` / `warning` - `high` / `error` - `critical` / `fatal` - `source` (string): System generating the alert - `timestamp` (string): ISO 8601 timestamp (defaults to current time) - `metadata` (object): Additional key-value pairs ### Complete Example ```json { "title": "High API Error Rate", "message": "Production API error rate exceeded 5% threshold. Current rate: 8.2%", "severity": "critical", "source": "monitoring-system", "timestamp": "2024-10-27T10:30:00Z", "metadata": { "environment": "production", "service": "api", "region": "us-east-1", "error_rate": 8.2, "threshold": 5.0, "dashboard_url": "https://dashboard.example.com/api", "runbook_url": "https://wiki.example.com/runbooks/high-error-rate" } } ``` ## Language Examples ### Python ```python import requests from datetime import datetime def send_alert(title, message, severity="high"): webhook_url = "https://hooks.beeps.dev/YOUR_WEBHOOK_ID" payload = { "title": title, "message": message, "severity": severity, "source": "python-app", "timestamp": datetime.utcnow().isoformat() + "Z", "metadata": { "host": "app-server-01", "pid": 12345 } } response = requests.post(webhook_url, json=payload) response.raise_for_status() return response.json() send_alert( "Database Connection Failed", "Could not connect to PostgreSQL after 3 retries", severity="critical" ) ``` ### Node.js ```javascript async function sendAlert(title, message, severity = "high") { const webhookUrl = "https://hooks.beeps.dev/YOUR_WEBHOOK_ID"; const payload = { title, message, severity, source: "nodejs-app", timestamp: new Date().toISOString(), metadata: { host: process.env.HOSTNAME, pid: process.pid, }, }; const response = await fetch(webhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`Failed to send alert: ${response.statusText}`); } return response.json(); } await sendAlert( "High Memory Usage", "Application memory usage exceeded 90%", "warning", ); ``` ### Go ```go package main import ( "bytes" "encoding/json" "net/http" "time" ) type Alert struct { Title string `json:"title"` Message string `json:"message"` Severity string `json:"severity"` Source string `json:"source"` Timestamp string `json:"timestamp"` Metadata map[string]interface{} `json:"metadata"` } func sendAlert(title, message, severity string) error { webhookURL := "https://hooks.beeps.dev/YOUR_WEBHOOK_ID" alert := Alert{ Title: title, Message: message, Severity: severity, Source: "go-app", Timestamp: time.Now().UTC().Format(time.RFC3339), Metadata: map[string]interface{}{ "host": "app-server-01", "version": "1.2.3", }, } payload, err := json.Marshal(alert) if err != nil { return err } resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(payload)) if err != nil { return err } defer resp.Body.Close() return nil } func main() { sendAlert( "Service Unavailable", "Health check failed for payment service", "critical", ) } ``` ### Ruby ```ruby require 'net/http' require 'json' require 'uri' def send_alert(title, message, severity: 'high') webhook_url = URI('https://hooks.beeps.dev/YOUR_WEBHOOK_ID') payload = { title: title, message: message, severity: severity, source: 'ruby-app', timestamp: Time.now.utc.iso8601, metadata: { host: `hostname`.strip, ruby_version: RUBY_VERSION } } http = Net::HTTP.new(webhook_url.host, webhook_url.port) http.use_ssl = true request = Net::HTTP::Post.new(webhook_url) request['Content-Type'] = 'application/json' request.body = payload.to_json response = http.request(request) raise "Failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess) JSON.parse(response.body) end send_alert( 'Cache Miss Rate High', 'Redis cache miss rate exceeded 50%', severity: 'warning' ) ``` ### Bash ```bash #!/bin/bash WEBHOOK_URL="https://hooks.beeps.dev/YOUR_WEBHOOK_ID" send_alert() { local title="$1" local message="$2" local severity="${3:-high}" curl -X POST "$WEBHOOK_URL" \ -H "Content-Type: application/json" \ -d @- < { return new Promise((resolve, reject) => { const options = { host: domain, port: 443, method: "GET", rejectUnauthorized: false, agent: false, }; const req = https.request(options, (res) => { const cert = res.socket.getPeerCertificate(); if (!cert || Object.keys(cert).length === 0) { reject(new Error("No certificate found")); return; } const validTo = new Date(cert.valid_to); const now = new Date(); const daysRemaining = Math.floor( (validTo.getTime() - now.getTime()) / (1000 * 60 * 60 * 24) ); resolve({ domain, validTo, daysRemaining, issuer: cert.issuer.O || "Unknown", subject: cert.subject.CN || domain, }); }); req.on("error", reject); req.end(); }); } async function sendAlert(cert: CertificateInfo) { const severity = cert.daysRemaining <= 7 ? "critical" : "warning"; const payload = { title: `SSL Certificate Expiring Soon: ${cert.domain}`, message: `SSL certificate for ${cert.domain} expires in ${cert.daysRemaining} days (${cert.validTo.toISOString()})`, severity, source: "ssl-monitor", timestamp: new Date().toISOString(), metadata: { domain: cert.domain, days_remaining: cert.daysRemaining, expiration_date: cert.validTo.toISOString(), issuer: cert.issuer, subject: cert.subject, }, }; try { const response = await fetch(WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!response.ok) { console.error(`Failed to send alert: ${response.statusText}`); } else { console.log(`Alert sent for ${cert.domain}`); } } catch (error) { console.error(`Error sending alert:`, error); } } async function checkAllCertificates() { console.log(`Checking ${DOMAINS.length} certificates...`); for (const domain of DOMAINS) { try { const cert = await checkCertificate(domain); console.log( `${domain}: ${cert.daysRemaining} days remaining (expires ${cert.validTo.toLocaleDateString()})` ); if (cert.daysRemaining <= WARNING_DAYS) { await sendAlert(cert); } } catch (error) { console.error(`Error checking ${domain}:`, error); await fetch(WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: `SSL Certificate Check Failed: ${domain}`, message: `Unable to retrieve certificate: ${error.message}`, severity: "high", source: "ssl-monitor", timestamp: new Date().toISOString(), metadata: { domain, error: error.message, }, }), }); } } } checkAllCertificates(); setInterval(checkAllCertificates, 24 * 60 * 60 * 1000); ``` **Usage:** ```bash # Install dependencies npm install # Run the monitor npx tsx ssl-monitor.ts # Or run as a cron job (daily at 9 AM) 0 9 * * * cd /path/to/monitor && npx tsx ssl-monitor.ts ``` ## Response Format Successful webhook submission returns: ```json { "success": true, "alert_id": "alr_abc123", "received_at": "2024-10-27T10:30:00Z" } ``` ## Error Handling ### HTTP Status Codes - `200 OK`: Alert received successfully - `400 Bad Request`: Invalid payload format - `401 Unauthorized`: Invalid webhook ID - `429 Too Many Requests`: Rate limit exceeded - `500 Internal Server Error`: Server error ### Example Error Response ```json { "success": false, "error": "Invalid payload: 'title' is required", "code": "INVALID_PAYLOAD" } ``` ## Best Practices 1. **Include Context**: Add relevant metadata for triage 2. **Use Severity**: Properly categorize alert urgency 3. **Add Timestamps**: Include when issue occurred 4. **Link to Resources**: Add dashboard/runbook URLs 5. **Handle Errors**: Implement retry logic with backoff 6. **Rate Limit**: Batch alerts when possible 7. **Validate Payloads**: Check data before sending 8. **Use HTTPS**: Always use secure connections ## Rate Limits - **Default**: 100 requests per minute per webhook - **Burst**: Up to 10 requests per second - Exceeding limits returns `429 Too Many Requests` ## What happens next Once beeps receives a POST, it creates an alert and runs your [relay rules](/docs/relays/rules/). The response includes the `alert_id` so you can track it programmatically. ## Next Steps - [Configure Relay Rules](/docs/relays/rules/) to route alerts to humans and AI agents - [Create Schedules](/docs/schedules/overview/) to define on-call rotations - [Add Contact Methods](/docs/contact-methods/overview/) for email and SMS notifications --- # Agent Integrations Overview > Source: https://beeps.dev/docs/agent-integrations/overview/ An agent integration stores the credentials beeps needs to invoke an AI agent: an API key plus any provider-specific settings. Relay rules of `ruleType: "agent"` reference an integration; when a matching alert fires, beeps invokes the agent, polls until it finishes, and writes the result (usually a PR URL) onto the alert responder. ## How it works 1. An alert fires on one of your relays. 2. A relay rule of `ruleType: "agent"` matches and points at an integration. 3. Beeps invokes the agent: calling its API, opening a self-hosted session, or triggering a GitHub workflow. 4. Beeps polls the agent until it reaches a terminal state. 5. The alert responder is updated: `done` (with `prUrl` when the agent opened one) on success, or `dropped` on failure. Storm-detection holds, alert deduplication, polling intervals, and max-attempts apply to every provider. They're configured on the relay rule, not the integration. See [Relay Rules](/docs/relays/rules/) for the full rule shape. ## Supported providers | Provider | How it runs | Output | |---|---|---| | [Claude](/docs/agent-integrations/claude/) | GitHub Actions | PR URL | | [OpenAI Codex](/docs/agent-integrations/codex/) | GitHub Actions | PR URL | | [Cursor](/docs/agent-integrations/cursor/) | Cloud API | PR URL | | [OpenCode](/docs/agent-integrations/opencode/) | Self-hosted HTTP | File diff | | [Devin](/docs/agent-integrations/devin/) | Cloud API | PR URL | | [AWS DevOps Agent](/docs/agent-integrations/aws-devops/) | Agent Space (HMAC webhook + AWS SDK) | Investigation report | | [Zup](/docs/agent-integrations/zup/) | Self-hosted HTTP | Investigation report | ## Where to next - Pick a provider above for setup instructions. - [Manage integrations](/docs/agent-integrations/manage/): CRUD via SDK, API, or CLI. - [Integration API reference](/docs/agent-integrations/api/): full endpoint and type reference. - [Best Practices — Agent Integrations](/docs/best-practices/#agent-integrations): configuration guidelines. ## MCP Clients To connect beeps tools in Claude Code or Codex, see [MCP Setup for Claude Code and Codex](/docs/using-beeps/mcp-server/). --- # Managing Integrations > Source: https://beeps.dev/docs/agent-integrations/manage/ import { Tabs, TabItem } from '@astrojs/starlight/components'; Create, update, and manage integrations for AI agents. ## Creating an Integration ```typescript import { BeepsClient } from "@beepsdev/sdk"; const client = new BeepsClient({ accessToken: process.env.BEEPS_ACCESS_TOKEN, }); const integration = await client.integration.create({ name: "Production Agent", provider: "devin", apiKey: process.env.AGENT_API_KEY, // metadata shape is provider-specific — see the per-provider page }); console.log(`Created integration: ${integration.id}`); ``` For provider-specific `metadata` and rule-config fields, see the page for that provider under [Agent Integrations](/docs/agent-integrations/overview/). ### Safe Method (No Exceptions) ```typescript const result = await client.integration.createSafe({ name: "Production Agent", provider: "devin", apiKey: process.env.AGENT_API_KEY, }); if (result.error) { console.error("Failed to create integration:", result.error.message); } else { console.log(`Created: ${result.data.id}`); } ``` ```bash curl -X POST https://api.beeps.dev/integrations \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Devin Production", "provider": "devin", "apiKey": "devin_api_key_here", "metadata": { "environment": "production" } }' ``` **Response:** ```json { "integration": { "id": "int_abc123", "organizationId": "org_xyz789", "name": "Devin Production", "provider": "devin", "apiKey": "***", "metadata": { "environment": "production" }, "createdBy": "usr_alice", "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z", "deletedAt": null } } ``` ```bash beeps integration create \ --provider devin \ --name "Devin Production" \ --api-key-env DEVIN_API_KEY ``` Add `--json` for machine-readable output: ```bash beeps integration create \ --provider devin \ --name "Devin Production" \ --api-key-env DEVIN_API_KEY \ --json ``` ## Listing Integrations ```typescript const integrations = await client.integration.list(); console.log(`Found ${integrations.length} integrations:`); integrations.forEach((integration) => { console.log(`- ${integration.name} (${integration.provider})`); }); ``` ### Filter by Provider ```typescript const integrations = await client.integration.list(); const claudeIntegrations = integrations.filter((i) => i.provider === "claude"); console.log(`Claude integrations: ${claudeIntegrations.length}`); ``` ```bash curl -X GET https://api.beeps.dev/integrations \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "integrations": [ { "id": "int_abc123", "organizationId": "org_xyz789", "name": "Devin Production", "provider": "devin", "apiKey": "***", "metadata": { "environment": "production" }, "createdBy": "usr_alice", "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z", "deletedAt": null } ] } ``` ```bash beeps integration list ``` Add `--json` for machine-readable output: ```bash beeps integration list --json ``` **Tool:** `beeps_list_integrations` ```json {} ``` ## Getting a Specific Integration ```typescript const integration = await client.integration.get("int_abc123"); console.log(`Name: ${integration.name}`); console.log(`Provider: ${integration.provider}`); console.log(`Metadata:`, integration.metadata); ``` ```bash curl -X GET https://api.beeps.dev/integrations/int_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash beeps integration get --integration-id int_abc123 ``` Add `--json` for machine-readable output: ```bash beeps integration get --integration-id int_abc123 --json ``` ## Updating an Integration ```typescript const updated = await client.integration.update("int_abc123", { name: "Production Agent (Updated)", apiKey: process.env.NEW_API_KEY, // metadata shape is provider-specific — see the per-provider page }); console.log("Integration updated"); ``` ### Update Name Only ```typescript const updated = await client.integration.update("int_abc123", { name: "New Integration Name", }); ``` ### Rotate API Key ```typescript const updated = await client.integration.update("int_abc123", { apiKey: process.env.NEW_API_KEY, }); console.log("API key rotated"); ``` ```bash curl -X PUT https://api.beeps.dev/integrations/int_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Devin Production (Updated)", "apiKey": "new_api_key_here" }' ``` ```bash beeps integration update \ --integration-id int_abc123 \ --name "Devin Production (Updated)" \ --api-key-env NEW_DEVIN_API_KEY ``` Add `--json` for machine-readable output: ```bash beeps integration update \ --integration-id int_abc123 \ --name "Devin Production (Updated)" \ --api-key-env NEW_DEVIN_API_KEY \ --json ``` ## Deleting an Integration ```typescript await client.integration.delete("int_abc123"); console.log("Integration deleted successfully"); ``` ### Safe Method ```typescript const result = await client.integration.deleteSafe("int_abc123"); if (result.error) { console.error("Failed to delete:", result.error.message); } else { console.log("Deleted:", result.data.success); } ``` ```bash curl -X DELETE https://api.beeps.dev/integrations/int_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "success": true } ``` ```bash beeps integration delete --integration-id int_abc123 ``` Add `--json` for machine-readable output: ```bash beeps integration delete --integration-id int_abc123 --json ``` ## Type Definitions ### IntegrationProvider ```typescript type IntegrationProvider = | "devin" | "cursor" | "zup" | "aws_devops" | "opencode" | "codex" | "claude"; ``` ### CreateIntegrationInput ```typescript type CreateIntegrationInput = { name: string; // Integration name provider: IntegrationProvider; // Service provider apiKey: string; // API key (stored securely) metadata?: Record; // Provider-specific config }; ``` ### UpdateIntegrationInput ```typescript type UpdateIntegrationInput = { name?: string; // Update name apiKey?: string; // Rotate API key metadata?: Record; // Update metadata }; ``` ### Integration ```typescript type Integration = { id: string; organizationId: string; name: string; provider: IntegrationProvider; apiKey: string; // Always returns "***" for security metadata: Record | null; createdBy: string; // User who created it createdAt: string; updatedAt: string; deletedAt: string | null; }; ``` ## Common Patterns ### Complete Setup with Relay Rule ```typescript const integration = await client.integration.create({ name: "Devin Auto-Triage", provider: "devin", apiKey: process.env.DEVIN_API_KEY, }); const relay = await client.relay.create({ name: "Production Alerts", }); await client.relay.rules.create(relay.id, { name: "AI triage first", ruleType: "agent", order: 1, config: { agentType: "devin", integrationId: integration.id, pollInterval: 30000, }, }); await client.relay.rules.create(relay.id, { name: "Escalate to humans", ruleType: "schedule_notify", order: 2, config: { scheduleId: "sch_oncall", }, }); ``` ### Key Rotation ```typescript const integrations = await client.integration.list(); for (const integration of integrations) { if (integration.provider === "devin") { await client.integration.update(integration.id, { apiKey: process.env.NEW_DEVIN_KEY, }); console.log(`Rotated key for ${integration.name}`); } } ``` ### Environment-Specific Integrations ```typescript const environments = ["development", "staging", "production"]; for (const env of environments) { await client.integration.create({ name: `Devin - ${env}`, provider: "devin", apiKey: process.env[`DEVIN_${env.toUpperCase()}_KEY`], metadata: { environment: env, }, }); } ``` ## Troubleshooting **AI agent not responding?** See [Troubleshooting - AI Agent Never Responds](/docs/troubleshooting/#ai-agent-never-responds). --- # Integration API Reference > Source: https://beeps.dev/docs/agent-integrations/api/ Complete API reference for the Integration resource in the beeps SDK. ## Integration Methods ### `client.integration.create(input)` Create a new integration. **Parameters:** - `input: CreateIntegrationInput` **Returns:** `Promise` **Example:** ```typescript const integration = await client.integration.create({ name: "Devin Production", provider: "devin", apiKey: process.env.DEVIN_API_KEY, metadata: { environment: "production", }, }); ``` ### `client.integration.list()` List all integrations in your organization. **Returns:** `Promise` **Example:** ```typescript const integrations = await client.integration.list(); ``` ### `client.integration.get(integrationId)` Get a specific integration by ID. **Parameters:** - `integrationId: string` **Returns:** `Promise` **Example:** ```typescript const integration = await client.integration.get("int_abc123"); ``` ### `client.integration.update(integrationId, input)` Update an integration. **Parameters:** - `integrationId: string` - `input: UpdateIntegrationInput` **Returns:** `Promise` **Example:** ```typescript const updated = await client.integration.update("int_abc123", { name: "Updated Name", apiKey: process.env.NEW_API_KEY, }); ``` ### `client.integration.delete(integrationId)` Delete an integration. **Parameters:** - `integrationId: string` **Returns:** `Promise<{ success: boolean }>` **Example:** ```typescript await client.integration.delete("int_abc123"); ``` ## Safe Methods All methods have corresponding `*Safe` variants that return `Result` instead of throwing: - `client.integration.listSafe()` - `client.integration.createSafe(input)` - `client.integration.getSafe(integrationId)` - `client.integration.updateSafe(integrationId, input)` - `client.integration.deleteSafe(integrationId)` **Example:** ```typescript const result = await client.integration.createSafe({ name: "Devin", provider: "devin", apiKey: process.env.DEVIN_API_KEY, }); if (result.error) { console.error(result.error.message); } else { console.log(result.data.id); } ``` ## Types ### IntegrationProvider ```typescript type IntegrationProvider = | "devin" | "cursor" | "zup" | "aws_devops" | "opencode" | "codex" | "claude"; ``` ### CreateIntegrationInput ```typescript type CreateIntegrationInput = { name: string; provider: IntegrationProvider; apiKey: string; metadata?: Record; }; ``` ### UpdateIntegrationInput ```typescript type UpdateIntegrationInput = { name?: string; apiKey?: string; metadata?: Record; }; ``` ### Integration ```typescript type Integration = { id: string; organizationId: string; name: string; provider: IntegrationProvider; apiKey: string; // Always returns "***" for security metadata: Record | null; createdBy: string; createdAt: string; updatedAt: string; deletedAt: string | null; }; ``` ## HTTP Endpoints ### Create Integration ``` POST /v0/integrations ``` **Request Body:** ```json { "name": "Devin Production", "provider": "devin", "apiKey": "devin_api_key_here", "metadata": { "environment": "production" } } ``` ### List Integrations ``` GET /v0/integrations ``` ### Get Integration ``` GET /v0/integrations/:integrationId ``` ### Update Integration ``` PUT /v0/integrations/:integrationId ``` **Request Body:** ```json { "name": "Updated Name", "apiKey": "new_api_key_here", "metadata": { "version": "v2" } } ``` ### Delete Integration ``` DELETE /v0/integrations/:integrationId ``` ## Authentication All API requests require authentication using your access token: ```bash curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ https://api.beeps.dev/v0/integrations ``` --- # Claude > Source: https://beeps.dev/docs/agent-integrations/claude/ The [Claude Agent SDK](https://code.claude.com/docs/en/agent-sdk/overview) is a local Node.js / Python library, with no remote API to call. Beeps integrates with Claude by triggering a `workflow_dispatch` GitHub Actions run in your repository; the workflow runs Claude (via `anthropics/claude-code-action` or the SDK directly), opens a PR with the changes, and beeps polls the workflow run and resolves the PR by branch. ## Prerequisites - A GitHub repository where Claude can run and open PRs. - A workflow file in that repo (template below). - An `ANTHROPIC_API_KEY` stored as a GitHub Actions secret in the repo. - A fine-grained GitHub PAT (or GitHub App installation token) with these permissions on the target repo: - `actions: write` (to trigger the workflow) - `actions: read` (to poll the workflow run) - `pull-requests: read` (to resolve the PR URL) ## Provider-side setup ### 1. Add the workflow file Drop this into `.github/workflows/claude-beeps.yml`. The `run-name` line is required so beeps can locate the run it triggered: ```yaml name: Claude (Beeps) run-name: "Beeps claude run ${{ inputs.dispatchId }}" on: workflow_dispatch: inputs: prompt: required: true type: string branch: required: true type: string dispatchId: required: true type: string alertId: required: false type: string jobs: claude: runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - uses: actions/checkout@v4 - uses: anthropics/claude-code-action@v1 with: anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} prompt: ${{ inputs.prompt }} - name: Open PR with changes uses: peter-evans/create-pull-request@v6 with: branch: ${{ inputs.branch }} title: "Claude: ${{ inputs.alertId }}" body: | Triggered by beeps alert `${{ inputs.alertId }}`. ${{ inputs.prompt }} commit-message: "fix: claude response for ${{ inputs.alertId }}" ``` ### 2. Create the GitHub PAT Create a fine-grained PAT scoped to the repo with the permissions listed in Prerequisites. Save it; you'll paste it into beeps as `apiKey`. ## Beeps-side setup Create the integration with the PAT and repo coordinates: ```typescript const claudeIntegration = await client.integration.create({ name: "Claude - acme/monorepo", provider: "claude", apiKey: process.env.GITHUB_PAT, metadata: { githubOwner: "acme", githubRepo: "monorepo", workflowFileName: "claude-beeps.yml", defaultBranch: "main", branchPrefix: "claude-beeps", }, }); ``` Reference it from a relay rule. Workflow runs typically take a few minutes, so push `pollInterval` higher than the cloud-API agents: ```typescript await client.relay.rules.create(relayId, { name: "Claude Auto-Fix", ruleType: "agent", config: { agentType: "claude", integrationId: claudeIntegration.id, pollInterval: 60000, maxPollAttempts: 60, }, }); ``` ## Configuration reference ### Integration metadata | Field | Type | Required | Default | Description | |---|---|---|---|---| | `githubOwner` | string | yes | — | GitHub org or user that owns the repo | | `githubRepo` | string | yes | — | Repo name | | `workflowFileName` | string | yes | — | Workflow filename, e.g. `claude-beeps.yml` | | `defaultBranch` | string | no | `main` | Branch to dispatch the workflow against | | `branchPrefix` | string | no | `claude-beeps` | Prefix for the branch name beeps generates per dispatch | ### Rule config The rule needs `agentType: "claude"` and `integrationId`. The shared polling and storm-control fields are documented in [Relay Rules](/docs/relays/rules/). ## Troubleshooting Beeps reports "workflow run not found". Your workflow file is missing the `run-name: "Beeps claude run ${{ inputs.dispatchId }}"` line. Without it, beeps falls back to "most recent workflow_dispatch run" matching, which is flaky under concurrency. The PR URL never appears on the alert responder. Beeps resolves the PR by querying `GET /repos/{owner}/{repo}/pulls?head={owner}:{branch}`, so your workflow needs to open the PR using the `inputs.branch` value beeps passed in (not a branch the workflow chose itself). --- # OpenAI Codex > Source: https://beeps.dev/docs/agent-integrations/codex/ The [Codex SDK](https://developers.openai.com/codex/sdk) is a local Node.js library that shells out to the `codex` binary, with no remote API to call. Beeps integrates with Codex by triggering a `workflow_dispatch` GitHub Actions run in your repository; the workflow uses `openai/codex-action` to run Codex, opens a PR, and beeps polls the workflow run and resolves the PR by branch. ## Prerequisites - A GitHub repository where Codex can run and open PRs. - A workflow file in that repo (template below). - An `OPENAI_API_KEY` stored as a GitHub Actions secret in the repo. (Codex Web / Codex Cloud requires ChatGPT login and is not supported here; only the API-key path is.) - A fine-grained GitHub PAT with these permissions on the target repo: - `actions: write` (to trigger the workflow) - `actions: read` (to poll the workflow run) - `pull-requests: read` (to resolve the PR URL) ## Provider-side setup ### 1. Add the workflow file Drop this into `.github/workflows/codex-beeps.yml`. The `run-name` line is required so beeps can locate the run it triggered: ```yaml name: Codex (Beeps) run-name: "Beeps codex run ${{ inputs.dispatchId }}" on: workflow_dispatch: inputs: prompt: required: true type: string branch: required: true type: string dispatchId: required: true type: string alertId: required: false type: string jobs: codex: runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - uses: actions/checkout@v4 - uses: openai/codex-action@v1 with: openai-api-key: ${{ secrets.OPENAI_API_KEY }} prompt: ${{ inputs.prompt }} - name: Open PR with changes uses: peter-evans/create-pull-request@v6 with: branch: ${{ inputs.branch }} title: "Codex: ${{ inputs.alertId }}" body: | Triggered by beeps alert `${{ inputs.alertId }}`. ${{ inputs.prompt }} commit-message: "fix: codex response for ${{ inputs.alertId }}" ``` ### 2. Create the GitHub PAT Create a fine-grained PAT scoped to the repo with the permissions listed in Prerequisites. Save it; you'll paste it into beeps as `apiKey`. ## Beeps-side setup Create the integration with the PAT and repo coordinates: ```typescript const codexIntegration = await client.integration.create({ name: "Codex - acme/monorepo", provider: "codex", apiKey: process.env.GITHUB_PAT, metadata: { githubOwner: "acme", githubRepo: "monorepo", workflowFileName: "codex-beeps.yml", defaultBranch: "main", branchPrefix: "codex-beeps", }, }); ``` Reference it from a relay rule. Workflow runs typically take a few minutes, so push `pollInterval` higher than the cloud-API agents: ```typescript await client.relay.rules.create(relayId, { name: "Codex Auto-Fix", ruleType: "agent", config: { agentType: "codex", integrationId: codexIntegration.id, pollInterval: 60000, maxPollAttempts: 60, }, }); ``` ## Configuration reference ### Integration metadata | Field | Type | Required | Default | Description | |---|---|---|---|---| | `githubOwner` | string | yes | — | GitHub org or user that owns the repo | | `githubRepo` | string | yes | — | Repo name | | `workflowFileName` | string | yes | — | Workflow filename, e.g. `codex-beeps.yml` | | `defaultBranch` | string | no | `main` | Branch to dispatch the workflow against | | `branchPrefix` | string | no | `codex-beeps` | Prefix for the branch name beeps generates per dispatch | ### Rule config The rule needs `agentType: "codex"` and `integrationId`. The shared polling and storm-control fields are documented in [Relay Rules](/docs/relays/rules/). ## Troubleshooting Beeps reports "workflow run not found". Your workflow file is missing the `run-name: "Beeps codex run ${{ inputs.dispatchId }}"` line. Without it, beeps falls back to "most recent workflow_dispatch run" matching, which is flaky under concurrency. The PR URL never appears on the alert responder. Beeps resolves the PR by querying `GET /repos/{owner}/{repo}/pulls?head={owner}:{branch}`, so your workflow needs to open the PR using the `inputs.branch` value beeps passed in. --- # Cursor > Source: https://beeps.dev/docs/agent-integrations/cursor/ import { Aside } from "@astrojs/starlight/components"; [Cursor's cloud agents](https://cursor.com/dashboard?tab=cloud-agents) are a hosted API: Cursor clones your repo, runs the agent, and opens a PR. Beeps creates an agent run when an alert fires, polls Cursor for status, and writes the PR URL onto the alert responder. ## Prerequisites - A Cursor account with cloud agents enabled. - **Privacy Mode enabled** in account settings (see callout above). - A GitHub repository Cursor can clone. The `repository` URL goes on the relay rule, not the integration. ## Provider-side setup 1. Sign in to [cursor.com/dashboard](https://cursor.com/dashboard?tab=cloud-agents). 2. Enable **Privacy Mode** in account settings. 3. Generate an API key from the same dashboard. 4. Save the API key; you'll paste it into beeps as `apiKey`. ## Beeps-side setup Create the integration: ```typescript const cursorIntegration = await client.integration.create({ name: "Cursor Agent", provider: "cursor", apiKey: process.env.CURSOR_API_KEY, }); ``` Reference it from a relay rule. Cursor requires a `repository` URL on the rule. That's the repo Cursor will clone and open the PR against: ```typescript await client.relay.rules.create(relayId, { name: "Cursor Auto-Fix", ruleType: "agent", config: { agentType: "cursor", integrationId: cursorIntegration.id, repository: "https://github.com/org/repo", autoCreatePr: true, pollInterval: 30000, maxPollAttempts: 120, }, }); ``` ## Configuration reference ### Integration metadata Cursor doesn't use `metadata`. Only `apiKey` is required. ### Rule config | Field | Type | Required | Default | Description | |---|---|---|---|---| | `agentType` | `"cursor"` | yes | — | Selects the Cursor agent | | `integrationId` | string | yes | — | The Cursor integration's id | | `repository` | string (URL) | yes | — | GitHub repo URL Cursor will clone | | `autoCreatePr` | boolean | no | `true` | Whether the agent should open a PR | | `branchName` | string | no | — | Override the branch name | | `autoBranch` | boolean | no | `true` | Let Cursor pick the branch name | The shared polling and storm-control fields are documented in [Relay Rules](/docs/relays/rules/). ## API version Beeps targets the Cursor Cloud Agents **v1 API** (`https://api.cursor.com/v1/agents`). The legacy `v0` endpoint had a different request and response shape; customers still pinned to `v0` via an explicit `config.endpoint` override should plan to migrate, since `v0` is no longer documented. ## Troubleshooting ### All requests return 403 Forbidden Privacy Mode is not enabled on your Cursor account. Toggle it on in settings and retry. ### Repository required error on dispatch The `repository` URL must be on the rule config, not the integration metadata. ### `Cursor API error: response missing agent.id` You're on a `config.endpoint` that returns a v0-shaped response. Either remove the `endpoint` override (recommended) or update it to a v1 path. --- # OpenCode > Source: https://beeps.dev/docs/agent-integrations/opencode/ [OpenCode](https://opencode.ai/) is a self-hosted AI coding agent. You run the OpenCode server on your own infrastructure; beeps connects to it over HTTP, creates a session per alert, and sends a prompt. OpenCode produces a file diff (it does not open a PR by itself). ## Prerequisites - A running OpenCode server reachable from beeps. See the [OpenCode server docs](https://opencode.ai/docs/server/) for installation. - HTTP Basic auth credentials for the server. - The server's URL. ## Provider-side setup 1. Install and run OpenCode following [opencode.ai/docs/server](https://opencode.ai/docs/server/). 2. Configure HTTP Basic auth on your server. The password becomes `apiKey` in beeps; the username goes in metadata (default `opencode`). 3. Note the server URL. You'll pass it on the relay rule as `endpoint`. ## Beeps-side setup Create the integration with the basic-auth password as `apiKey`: ```typescript const openCodeIntegration = await client.integration.create({ name: "OpenCode - internal", provider: "opencode", apiKey: process.env.OPENCODE_SERVER_PASSWORD, metadata: { username: "opencode", }, }); ``` Reference it from a relay rule. OpenCode needs an `endpoint` pointing at your server. The optional `openCodeModel` lets you pick which model the session runs: ```typescript await client.relay.rules.create(relayId, { name: "OpenCode investigation", ruleType: "agent", config: { agentType: "opencode", integrationId: openCodeIntegration.id, endpoint: "https://opencode.internal.corp:4096", openCodeModel: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514", }, pollInterval: 15000, maxPollAttempts: 200, }, }); ``` ## Configuration reference ### Integration metadata | Field | Type | Required | Default | Description | |---|---|---|---|---| | `username` | string | no | `opencode` | HTTP Basic auth username | ### Rule config | Field | Type | Required | Default | Description | |---|---|---|---|---| | `agentType` | `"opencode"` | yes | — | Selects the OpenCode agent | | `integrationId` | string | yes | — | The OpenCode integration's id | | `endpoint` | string (URL) | yes | — | Base URL of the OpenCode server | | `openCodeModel.providerID` | string | no | server default | Provider id, e.g. `anthropic` | | `openCodeModel.modelID` | string | no | server default | Model id, e.g. `claude-sonnet-4-20250514` | The shared polling and storm-control fields are documented in [Relay Rules](/docs/relays/rules/). ## Troubleshooting 401 Unauthorized on session creation. Beeps sends `Authorization: Basic `. Confirm both the username (in metadata) and password (`apiKey`) match the server's basic-auth config. Session completes but no PR is created. OpenCode produces a file diff, not a PR. Check the session's diff endpoint on your server, or wire your own commit/PR step downstream. --- # Devin > Source: https://beeps.dev/docs/agent-integrations/devin/ [Devin](https://devin.ai/) is a hosted AI software engineering agent. Beeps creates a Devin session when an alert fires, sends a prompt built from the alert, polls Devin until the session finishes, and surfaces the PR URL on the alert responder. ## Prerequisites - A Devin account with API access. - A Devin **service-user token** (`cog_*` prefix). ## Provider-side setup 1. In the Devin web app, navigate to `https://app.devin.ai/org//settings/org-service-users`. Replace `` with your Devin org. 2. Create a service user and generate its token. It will look like `cog_...`. 3. Save the token; you'll paste it into beeps as `apiKey`. Beeps verifies the token against Devin's API at integration-creation time, so a bad token is caught immediately rather than failing on the first alert. ## Beeps-side setup Create the integration: ```typescript const devinIntegration = await client.integration.create({ name: "Devin Production", provider: "devin", apiKey: process.env.DEVIN_SERVICE_TOKEN, // cog_* }); ``` Beeps captures the Devin org id from the token and stores it on the integration metadata. You don't need to provide it yourself. Reference the integration from a relay rule: ```typescript await client.relay.rules.create(relayId, { name: "Devin Auto-Triage", ruleType: "agent", config: { agentType: "devin", integrationId: devinIntegration.id, pollInterval: 30000, maxPollAttempts: 120, }, }); ``` ## Configuration reference ### Rule config | Field | Type | Required | Default | Description | |---|---|---|---|---| | `agentType` | `"devin"` | yes | — | Selects the Devin agent | | `integrationId` | string | yes | — | The Devin integration's id | | `endpoint` | string (URL) | no | composed from the integration's org id | Override only for Devin Enterprise / self-hosted; provide a full session-create URL including the org path | The shared polling and storm-control fields are documented in [Relay Rules](/docs/relays/rules/). ## Troubleshooting ### `invalid_devin_token` at integration creation The token was rejected by Devin. Confirm it starts with `cog_`, hasn't been revoked, and was generated from `https://app.devin.ai/org//settings/org-service-users`. ### Session stuck "waiting for user" Devin sometimes pauses for human input or approval mid-session. Beeps marks the job `completed` with a `blocked` result so the alert isn't left dangling. Open the Devin session URL (logged when beeps creates the session) to unblock it. ### Session ended with "quota_exceeded" When your Devin account hits a credit, quota, or billing limit, the session terminates and beeps surfaces it as **`quota_exceeded`** on the alert responder. Possible underlying reasons: - `out_of_credits` — purchased credit pool is empty. - `out_of_quota` / `no_quota_allocation` — plan-level quota exhausted. - `usage_limit_exceeded` / `org_usage_limit_exceeded` — per-user or per-org cap hit. - `total_session_limit_exceeded` — concurrent session cap hit. - `payment_declined` — billing failure on file. Top up or adjust the limit at https://app.devin.ai/settings/usage, then retry the failed alert (or wait for the next one). Beeps doesn't auto-retry quota failures. --- # AWS DevOps Agent > Source: https://beeps.dev/docs/agent-integrations/aws-devops/ [AWS DevOps Agent](https://docs.aws.amazon.com/devopsagent/) is AWS's incident-investigation agent, hosted in an Agent Space. The integration is two-sided: beeps POSTs the alert to your Agent Space's webhook URL to start an investigation, then polls task status through the AWS SDK using IAM credentials. The output is an investigation report, not a PR. ## Prerequisites - An AWS account with [DevOps Agent](https://docs.aws.amazon.com/devopsagent/) enabled and at least one Agent Space configured. - The Agent Space's webhook URL. - The webhook authentication secret. Either an HMAC signing secret (`hmac_v1`) or a bearer token (`bearer_v2`), depending on how your Agent Space is configured. - AWS IAM credentials with the `aidevops:*` permissions listed in the [DevOps Agent IAM docs](https://docs.aws.amazon.com/devopsagent/latest/userguide/aws-devops-agent-security-devops-agent-iam-permissions.html). - Optionally: a cross-account `roleArn` for STS AssumeRole. ## Provider-side setup ### 1. Configure the Agent Space Follow the [AWS DevOps Agent setup guide](https://docs.aws.amazon.com/devopsagent/latest/userguide/getting-started.html) to create an Agent Space. Note: - The Agent Space ID (e.g. `space-abc123`). - The Agent Space webhook URL (e.g. `https://webhook.devopsagent.us-east-1.amazonaws.com/invoke/abc123`). - Whether the webhook uses HMAC v1 or bearer v2 auth. ### 2. Get the auth secret - **HMAC v1**: copy the signing secret from the Agent Space configuration. Beeps will sign each request with HMAC-SHA256. - **Bearer v2**: copy the bearer token from the Agent Space configuration. Beeps will send it as `Authorization: Bearer `. ### 3. Provision IAM credentials Create an IAM user (or, preferred for cross-account, a role) with the `aidevops:*` permissions referenced in the [DevOps Agent IAM docs](https://docs.aws.amazon.com/devopsagent/latest/userguide/aws-devops-agent-security-devops-agent-iam-permissions.html). Beeps uses the access key + secret to call the DevOps Agent SDK (`ListBacklogTasksCommand`, `ListJournalRecordsCommand`) when polling. If you're going cross-account, create a role in the target account with those permissions and a trust policy allowing the IAM user in your beeps integration account to assume it. Provide the role ARN as `roleArn` in metadata; beeps will assume it via STS for each poll. ## Beeps-side setup The webhook secret goes in `apiKey`. Everything else (AWS creds, region, agent space, auth version, optional role) goes in `metadata`: ```typescript const awsDevOpsIntegration = await client.integration.create({ name: "AWS DevOps Agent - prod", provider: "aws_devops", apiKey: process.env.AWS_DEVOPS_WEBHOOK_SECRET, metadata: { awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID, awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, awsRegion: "us-east-1", agentSpaceId: "space-abc123", authVersion: "hmac_v1", // or "bearer_v2" // roleArn: "arn:aws:iam::123456789012:role/beeps-devops-agent", }, }); ``` Reference it from a relay rule. Point the rule at the Agent Space webhook URL via `endpoint`, and pass the Agent Space ID and region so beeps knows where to poll: ```typescript await client.relay.rules.create(relayId, { name: "AWS DevOps investigation", ruleType: "agent", config: { agentType: "aws_devops", integrationId: awsDevOpsIntegration.id, endpoint: "https://webhook.devopsagent.us-east-1.amazonaws.com/invoke/abc123", awsAgentSpaceId: "space-abc123", awsRegion: "us-east-1", awsAuthVersion: "hmac_v1", awsPriority: "HIGH", pollInterval: 60000, maxPollAttempts: 60, }, }); ``` ## Configuration reference ### Integration metadata | Field | Type | Required | Default | Description | |---|---|---|---|---| | `awsAccessKeyId` | string | yes | — | IAM access key id | | `awsSecretAccessKey` | string | yes | — | IAM secret access key | | `awsRegion` | string | yes | — | AWS region of the Agent Space, e.g. `us-east-1` | | `agentSpaceId` | string | yes | — | Agent Space identifier | | `authVersion` | `"hmac_v1"` \| `"bearer_v2"` | yes | — | Webhook signature scheme | | `roleArn` | string | no | — | IAM role to assume via STS for polling (cross-account) | The `apiKey` field on the integration holds the webhook secret (the HMAC signing secret or bearer token, depending on `authVersion`). ### Rule config | Field | Type | Required | Default | Description | |---|---|---|---|---| | `agentType` | `"aws_devops"` | yes | — | Selects the AWS DevOps agent | | `integrationId` | string | yes | — | The integration's id | | `endpoint` | string (URL) | yes | — | Agent Space webhook URL | | `awsAgentSpaceId` | string | yes | — | Repeated here so polling can locate it without re-reading the integration | | `awsRegion` | string | yes | — | Repeated here for the same reason | | `awsAuthVersion` | `"hmac_v1"` \| `"bearer_v2"` | yes | — | Must match the integration's `authVersion` | | `awsPriority` | `"CRITICAL"` \| `"HIGH"` \| `"MEDIUM"` \| `"LOW"` | no | — | Priority sent on the webhook payload | | `awsService` | string | no | — | Service tag added to the investigation | The shared polling and storm-control fields are documented in [Relay Rules](/docs/relays/rules/). ## Troubleshooting Webhook returns 401/403. Check that `authVersion` on both the integration metadata and the rule config matches what the Agent Space expects. HMAC and bearer use different request signing; they're not interchangeable. Polling fails with `AccessDenied`. The IAM credentials need `aidevops:*` (see the IAM permissions doc above). For cross-account, also confirm the trust policy on the assumed role allows your IAM user. `roleArn` set but polling still uses the base credentials. Beeps assumes the role via STS only if `roleArn` is present in metadata. Confirm the field is set on the integration (not the rule). --- # Zup > Source: https://beeps.dev/docs/agent-integrations/zup/ [Zup](https://zup.dev) is a self-hosted incident-investigation agent. You run Zup on your own infrastructure; beeps creates a run when an alert fires and polls until the investigation finishes. The output is an investigation report (findings, situation assessment, recommendations), not a PR. ## Prerequisites - A running Zup server reachable from beeps. See the [Zup docs](https://zup.dev/docs) for installation and configuration. - A bearer token for authenticating to the Zup server. - The Zup server's URL. ## Provider-side setup 1. Install and run Zup following [zup.dev/docs](https://zup.dev/docs). 2. Configure an API token on the Zup server. Save it; you'll paste it into beeps as `apiKey`. 3. Note the server URL. You'll pass it on the relay rule as `endpoint`. Default during local development is `http://localhost:3000/api/v0/runs`. ## Beeps-side setup Create the integration with the bearer token as `apiKey`: ```typescript const zupIntegration = await client.integration.create({ name: "Zup - internal", provider: "zup", apiKey: process.env.ZUP_API_TOKEN, }); ``` Reference it from a relay rule. Zup needs an `endpoint` pointing at your server: ```typescript await client.relay.rules.create(relayId, { name: "Zup investigation", ruleType: "agent", config: { agentType: "zup", integrationId: zupIntegration.id, endpoint: "https://zup.internal.corp/api/v0/runs", pollInterval: 30000, maxPollAttempts: 120, }, }); ``` ## Configuration reference ### Integration metadata Zup doesn't use `metadata`. Only `apiKey` is required. ### Rule config | Field | Type | Required | Default | Description | |---|---|---|---|---| | `agentType` | `"zup"` | yes | — | Selects the Zup agent | | `integrationId` | string | yes | — | The Zup integration's id | | `endpoint` | string (URL) | no | `http://localhost:3000/api/v0/runs` | Base URL of your Zup server's runs endpoint | The shared polling and storm-control fields are documented in [Relay Rules](/docs/relays/rules/). ## Troubleshooting 401 Unauthorized when starting a run. Beeps sends `Authorization: Bearer `. Confirm the token in `apiKey` matches what the Zup server is configured to accept. Run completes but no findings show up. Check the run's status in your Zup server. Zup returns `status: "completed"` only when the investigation actually finishes; partial or failed runs return different statuses and beeps marks the alert responder `dropped` accordingly. --- # Members API Reference > Source: https://beeps.dev/docs/members/api/ import { Tabs, TabItem } from '@astrojs/starlight/components'; List organization members to resolve emails to user IDs when managing schedules. ## List Members ### `client.member.list()` List all members in your organization. **Returns:** `Promise` **Example:** ```typescript const members = await client.member.list(); ``` ### `client.member.listSafe()` List members without throwing exceptions. **Returns:** `Promise>` **Example:** ```typescript const result = await client.member.listSafe(); if (result.error) { console.error("Error:", result.error.message); } else { console.log(`Members: ${result.data.length}`); } ``` ### HTTP Endpoint ``` GET /v0/members ``` All API requests require authentication using your access token: ```bash curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ https://api.beeps.dev/v0/members ``` ```bash beeps member list ``` Add `--json` for machine-readable output: ```bash beeps member list --json ``` **Tool:** `beeps_list_members` ```json {} ``` ## Types ### OrganizationMember ```typescript type OrganizationMember = { userId: string; organizationId: string; role: string; email: string; }; ``` --- # Troubleshooting > Source: https://beeps.dev/docs/troubleshooting/ Solutions for common beeps issues. ## Alerts are not appearing If you're sending alerts but they're not showing up: 1. Check the webhook URL — make sure you're POSTing to the right relay webhook 2. Check the payload — `title` is required, `severity` is recommended 3. Check the response — a `200` with an `alert_id` means beeps received it. A `401` means the webhook key is wrong. 4. For Sentry/Datadog/Axiom, check their delivery logs to see if the webhook is actually firing Quick test from your terminal: ```bash curl -X POST https://hooks.beeps.dev/YOUR_WEBHOOK_ID \ -H "Content-Type: application/json" \ -d '{"title": "Test Alert", "severity": "high"}' ``` ## Alerts appear but no one is notified Check these in order: 1. **Does the on-call user have contact methods?** Open the dashboard and check. Users without email or SMS configured won't get anything. 2. **Is someone actually on-call?** If your schedule has no members, `schedule_notify` rules have nothing to do. 3. **Are the relay rules enabled?** Rules are created with `enabled: true` by default, but check that someone didn't disable them. 4. **Is it the right rule type?** A `webhook` rule posts to a URL — it won't send email/SMS. You need a `schedule_notify` rule for that. ## No one is on-call The schedule probably has no members. Add at least one user to the rotation. If there are members but no one shows as on-call, check these next: 1. `startAt` may be in the future. A schedule is inactive until its start time. 2. `handoffTime` and `handoffDay` may not be what you expect. All times are UTC. ## AI agent never responds 1. **Check the integration exists** — the agent rule references an `integrationId`. Make sure that integration hasn't been deleted. 2. **Check the access token** — test the Devin/Cursor access token directly against their API to confirm it's valid and has the right permissions. 3. **Check the rule is enabled** — disabled rules are skipped. 4. **Check the rule's group and order** — if the agent rule is in the same group as a `schedule_notify` rule with a lower order, and a human responds first, the relay may stop before reaching the agent rule. ## Relay rules not firing 1. **Is the relay receiving alerts?** Check the dashboard for alerts on this relay. If there are none, the webhook isn't being called. 2. **Are the rules enabled?** Disabled rules are skipped entirely. 3. **Check the rule config** — each rule type has required fields: - `schedule_notify` needs a `scheduleId` - `webhook` needs an `endpoint` and `method` - `agent` needs an `agentType` and `integrationId` Invalid rule config now returns field-level validation issues, including unknown fields and typo suggestions when available. 4. **Check groups and order** — rules in the same group run sequentially by `order`. If an earlier rule in the same group causes a response, later rules may not fire. ## Getting help If none of the above helps, reach out with: - Your organization ID - The relay ID and rule IDs involved - An example alert payload - The error message or unexpected behavior you're seeing - The `X-Request-ID` response header if you have it --- # Best Practices > Source: https://beeps.dev/docs/best-practices/ ## Idempotent Configuration Always set `externalKey` on relays, schedules, and relay rules. This makes operations idempotent—you can safely re-run setup scripts without creating duplicates. ```typescript // First run: creates relay const relay = await client.relay.create({ name: "Production Alerts", externalKey: "prod-alerts-v1", }); // Second run: updates existing relay (no duplicate) const sameRelay = await client.relay.create({ name: "Production Alerts (Updated)", externalKey: "prod-alerts-v1", // Same key = update }); ``` Use a consistent naming convention for external keys: ```typescript // Recommended: namespace::resource::identifier "myco::relay::production" "myco::schedule::primary-weekly" "myco::rule::notify-oncall" ``` ## Parallel vs Sequential Execution The `group` field on relay rules controls execution flow: - **Same group** = sequential (order 1, then 2, then 3) - **Different groups** = parallel (run simultaneously) ```typescript // Parallel: AI and humans notified at the same time { group: "agents", order: 1, ruleType: "agent" } { group: "humans", order: 1, ruleType: "schedule_notify" } // Sequential: AI tries first, then escalates to humans { group: "default", order: 1, ruleType: "agent" } { group: "default", order: 2, ruleType: "schedule_notify" } ``` **Use parallel when:** You want the fastest possible response time. **Use sequential when:** You want AI to attempt resolution before interrupting humans. ## Relays - **Descriptive names**: Use names that indicate purpose ("Production Alerts", not "Relay 1") - **Separate by service**: Create different relays for different services or environments - **Test rules disabled**: Create rules with `enabled: false`, test, then enable ## Schedules - **UTC times**: All times are in UTC. Account for your team's time zones when setting `handoffTime` - **Default start**: Omit `startAt` when you want a schedule to start immediately. Set it explicitly for planned cutovers. - **Member order matters**: Members rotate in the order they're added - **Weekly over daily**: Weekly rotations are generally better for work-life balance - **Always have coverage**: Ensure your schedule has at least one member at all times ## Contact Methods - **Multiple channels**: Add both email and SMS for each on-call engineer - **E.164 format**: Use international format for phone numbers (+14155551234) - **Verify methods**: Verified contact methods have higher deliverability - **Keep updated**: Review contact information when team members change roles ## Agent Integrations - **Environment separation**: Use separate integrations for dev/staging/prod - **Rotate keys regularly**: Update access tokens periodically for security - **Descriptive metadata**: Store environment and purpose in metadata ## Testing 1. Create relays and rules with `enabled: false` 2. Send test alerts to verify webhook connectivity 3. Use `client.relay.simulate()` to preview routing 4. Use `client.relay.lint()` to validate coverage 5. Enable rules once verified --- # Terminal > Source: https://beeps.dev/docs/using-beeps/terminal/ import { Tabs, TabItem } from "@astrojs/starlight/components"; beeps-terminal is a keyboard-driven TUI dashboard that gives you a live view of active alerts, alert details, and your on-call roster — all from the terminal. ## Installation ```bash npm install -g beeps-terminal ``` Then run: ```bash beeps-terminal ``` ```bash npx beeps-terminal ``` ## Configuration beeps-terminal needs one environment variable: | Variable | Required | Description | |----------|----------|-------------| | `BEEPS_ACCESS_TOKEN` | Yes | Your access token from organization settings (looks like `bat_xxxxxxxx`) | ```bash export BEEPS_ACCESS_TOKEN="bat_your_access_token_here" beeps-terminal ``` ## Layout The dashboard has a 3-panel layout that adapts to your terminal size: ``` ┌──────────────────────┬──────────────────────┐ │ │ │ │ Active Alerts │ Alert Detail / │ │ │ Recent Deploys │ │ (scrollable list │ │ │ of all active ├──────────────────────┤ │ alerts) │ │ │ │ On-Call Roster │ │ │ │ └──────────────────────┴──────────────────────┘ ``` - **Left pane** — scrollable list of active alerts with status, severity, source, and assignment info. Updates automatically. - **Top-right pane** — shows recent deploy activity by default. Press `enter` on an alert to expand its full detail (ID, status, severity, timestamps, metadata). - **Bottom-right pane** — on-call roster showing who's on-call for each schedule, their active alert count, rotation end time, and who's up next. ## Keybindings | Key | Action | |-----|--------| | `↑` / `↓` | Navigate alerts | | `enter` | Expand alert detail | | `escape` | Collapse detail | | `a` | Acknowledge alert | | `r` | Resolve alert | | `e` | Escalate alert | | `q` / `ctrl+c` | Quit | --- # MCP Server > Source: https://beeps.dev/docs/using-beeps/mcp-server/ import { Tabs, TabItem } from "@astrojs/starlight/components"; The beeps MCP server lets you triage, fix, and resolve alerts without leaving your editor. ## Setup ```bash claude mcp add --transport http --scope user beeps https://mcp.beeps.dev/mcp ``` Verify: ```bash claude mcp list claude mcp get beeps ``` #### Optional: Pre-Registered OAuth Client If your deployment disables open dynamic client registration, add a client id/secret: ```bash claude mcp add --transport http --scope user --client-id --client-secret beeps https://mcp.beeps.dev/mcp ``` ```bash codex mcp add beeps --url https://mcp.beeps.dev/mcp ``` Run OAuth login: ```bash codex mcp login beeps --scopes "openid,profile,email,offline_access,beeps.tools.read,beeps.tools.write" ``` Verify: ```bash codex mcp list codex mcp get beeps ``` ## Tools ### Triage **`beeps_list_active_alerts`** — List active unresolved alerts with optional severity filter. ```text Show me all active alerts ``` ```text Are there any critical alerts right now? ``` **`beeps_list_alert_responders`** — List alert responders and their status. ```text Who's responding to alert al_abc123? ``` **`beeps_get_fix_status`** — Get current fix progress by alert jobs or a specific job. ```text What's the fix status on alert al_abc123? ``` ### Act **`beeps_on_it`** — Mark an alert as being worked by a responder. ```text I'm on it for alert al_abc123 ``` **`beeps_fix_here`** — Load complete alert fix context for foreground remediation. ```text Load the fix context for alert al_abc123 and help me resolve it ``` ### Resolve **`beeps_update_responder_status`** — Mark responder as done or dropped and optionally attach a PR URL. ```text Mark my response to al_abc123 as done with PR https://github.com/org/repo/pull/42 ``` **`beeps_resolve_alert`** — Resolve an alert. ```text Resolve alert al_abc123 ``` ### Scheduling **`beeps_get_on_call`** — Get the currently on-call user for a schedule. ```text Who's on call for the platform schedule? ``` **`beeps_set_override`** — Set a temporary schedule override. ```text Override the platform schedule to put me on call from now until 6pm ``` **`beeps_update_override`** — Update the time window or reason of an override. ```text Extend the current override until Monday ``` **`beeps_cancel_override`** — Cancel an override to restore normal rotation. ```text Cancel the override on the platform schedule ``` **`beeps_get_schedule_assignments`** — Get upcoming schedule assignments. ```text Show the next 5 on-call assignments for the platform schedule ``` ### Configuration **`beeps_list_relays`** — List all relays. ```text What relays do we have? ``` **`beeps_list_webhooks`** — List webhooks for a relay. ```text What webhook URLs does the production relay have? ``` **`beeps_create_relay`** — Create a new relay. ```text Create a relay called "staging alerts" ``` **`beeps_list_schedules`** — List all schedules. ```text Show me all schedules ``` **`beeps_create_schedule`** — Create a schedule. ```text Create a weekly schedule on the production relay, handoff Monday at 9am ``` **`beeps_list_schedule_members`** — List members of a schedule. ```text Who's in the primary on-call rotation? ``` **`beeps_add_schedule_member`** — Add a member to a rotation. ```text Add alice@example.com to the primary schedule ``` **`beeps_remove_schedule_member`** — Remove a member from a rotation. ```text Remove Bob from the primary schedule ``` **`beeps_list_relay_rules`** — List rules for a relay. ```text What rules are configured on the production relay? ``` **`beeps_create_relay_rule`** — Create a relay rule. ```text Add a schedule_notify rule to the production relay for the primary schedule ``` **`beeps_delete_relay_rule`** — Delete a relay rule. ```text Delete the webhook rule on the staging relay ``` **`beeps_assign_alert`** — Assign an alert to a user. ```text Assign alert alr_abc123 to Alice ``` **`beeps_list_integrations`** — List integrations. ```text What integrations are configured? ``` **`beeps_list_members`** — List organization members. ```text Who's in the org? ``` **`beeps_get_alert`** — Get full details of a specific alert. ```text Show me the details of alert alr_abc123 ``` ## Rate limits Per-actor, sliding 1-minute window: | Tier | Limit | |---|---| | Read tools (e.g. `beeps_list_alerts`) | 120 / minute | | Write tools (e.g. `beeps_on_it`, `beeps_resolve_alert`) | 20 / minute | | Per-IP fallback (unauth, mixed) | 200 / minute | When you exceed a limit the server returns `429 Too Many Requests` with a `Retry-After` header (seconds until the window resets). The body is `{ "error": "rate_limit_exceeded" }`. ## Troubleshooting ### `429 rate_limit_exceeded` You hit a per-minute cap (see [Rate limits](#rate-limits)). Wait the number of seconds in `Retry-After` and retry. If you're consistently hitting the limit, batch your reads with `beeps_list_alerts` rather than per-alert `beeps_get_alert` calls. ### `403 missing required scope` Re-run `codex mcp login beeps` with both `beeps.tools.read` and `beeps.tools.write`. For Claude Code, remove/re-add the server and complete OAuth consent again. ### `Organization context required` If the user belongs to multiple organizations, set an active org in the web app first. For Claude Code, you can also set a fixed header when adding the server: ```bash claude mcp add --transport http --scope user -H "X-Organization-Id: " beeps https://mcp.beeps.dev/mcp ``` ### `explicit write confirmation required` Write tools require confirmation metadata by default. Ask the agent to include MCP `_meta`: ```json { "beeps/confirmWrite": true } ``` For self-hosted environments, this policy can be disabled with `BEEPS_REQUIRE_WRITE_CONFIRMATION=false`. --- # Config as Code > Source: https://beeps.dev/docs/config-as-code/ import { Tabs, TabItem } from "@astrojs/starlight/components"; Once you've gone through the [Quickstart](/docs/quickstart/) and have a working setup, manage it as code. A single `beeps.config.ts` file is the source of truth, checked into your repo, and deployed via CI. ## Full SDK config This is the complete `beeps.config.ts` that wires up the quickstart end-to-end. Because every entity sets `externalKey`, the file is idempotent: safe to re-run without creating duplicates. Treat it like a database migration. ```typescript import { BeepsClient } from "@beepsdev/sdk"; async function configure() { const client = new BeepsClient({ accessToken: process.env.BEEPS_ACCESS_TOKEN, }); // 1. Create relay const relay = await client.relay.create({ name: "production relay", description: "routes critical production incidents", externalKey: "production::relay", }); // 2. Create schedule const schedule = await client.schedule.create({ name: "Primary On-Call", relayId: relay.id, type: "weekly", handoffDay: "monday", handoffTime: "09:00", // startAt omitted -> schedule starts immediately externalKey: "primary::schedule", }); // 3. Add team members by email await client.schedule.addMember(schedule.id, { email: "alice@example.com" }); await client.schedule.addMember(schedule.id, { email: "bob@example.com" }); // 4. Contact methods are managed in Settings → Profile in the dashboard. // 5. Create AI integration const integration = await client.integration.create({ name: "Cursor Production Agent", provider: "cursor", apiKey: process.env.CURSOR_API_KEY, externalKey: "agents::cursor::integration", }); // 6. Create relay rules (parallel: agents + humans) await client.relay.rules.create(relay.id, { name: "AI Agent Auto-Triage", externalKey: "agents::cursor", ruleType: "agent", group: "agents", order: 1, config: { agentType: "cursor", integrationId: integration.id, repository: "https://github.com/your-org/your-repo", autoCreatePr: true, }, enabled: true, }); await client.relay.rules.create(relay.id, { name: "Notify On-Call Engineer", externalKey: "humans::primary::schedule-notify", ruleType: "schedule_notify", group: "humans", order: 1, config: { scheduleId: schedule.id }, enabled: true, }); // 7. Verify const rules = await client.relay.rules.list(relay.id); console.log("\nRelay Rules:"); rules.forEach((rule) => { console.log(` [${rule.group}] ${rule.name}`); console.log(` Type: ${rule.ruleType}`); console.log(` Enabled: ${rule.enabled}`); }); } configure().catch(console.error); ``` Run it: ```bash export BEEPS_ACCESS_TOKEN="bat_your_token_here" export CURSOR_API_KEY="cursor_api_key_here" npx tsx beeps.config.ts ``` ## Deploy via CI Treat `beeps.config.ts` like infrastructure. Every change goes through PR review. ### GitHub Actions ```yaml name: Deploy beeps config on: push: branches: [main] paths: [beeps.config.ts] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - run: npm install @beepsdev/sdk - run: npx tsx beeps.config.ts env: BEEPS_ACCESS_TOKEN: ${{ secrets.BEEPS_ACCESS_TOKEN }} CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} ``` For a CLI-driven plan/apply flow that previews diffs before applying (à la Terraform), see [CLI Config-as-Code](/docs/cli/config/). ## Understanding parallel execution ``` Alert Received | Relay: "production relay" | |-- [agents] AI Agent Auto-Triage --> Cursor starts working | |-- [humans] Notify On-Call Engineer --> Email/SMS sent ``` Rules with the same `group` run sequentially in `order` order. Rules in different groups run in parallel. ## Common customizations ### Sequential instead of parallel To run AI first, then escalate to humans only if the AI doesn't resolve in time, put both rules in the same group: ```typescript await client.relay.rules.create(relay.id, { name: "Try AI first", ruleType: "agent", group: "default", order: 1, config: { agentType: "cursor", integrationId: integration.id, repository: "https://github.com/your-org/your-repo", }, }); await client.relay.rules.create(relay.id, { name: "Escalate to humans", ruleType: "schedule_notify", group: "default", order: 2, config: { scheduleId: schedule.id }, }); ``` ### Add a secondary schedule ```typescript await client.relay.rules.create(relay.id, { name: "Escalate to backup", ruleType: "schedule_notify", group: "humans", order: 2, config: { scheduleId: backupSchedule.id }, }); ``` ### Webhook fan-out ```typescript await client.relay.rules.create(relay.id, { name: "Post to Slack ops channel", ruleType: "webhook", group: "notifications", order: 1, config: { endpoint: "https://hooks.slack.com/services/...", method: "POST", headers: { "Content-Type": "application/json" }, }, }); ``` See [Relay Rules](/docs/relays/rules/) for the full rule type reference. --- # CLI > Source: https://beeps.dev/docs/using-beeps/cli/ import { Tabs, TabItem } from "@astrojs/starlight/components"; The beeps CLI lets you manage relays, schedules, alerts, and integrations directly from the command line — or define your entire on-call setup as code. ## Installation ```bash npm install -g @beepsdev/cli ``` ```bash npx @beepsdev/cli --help ``` ## Authentication Set your access token as an environment variable: ```bash export BEEPS_ACCESS_TOKEN="bat_your_access_token_here" ``` Or use a JSON config file (must be `chmod 600`, owned by the current user, and not a symlink): ```json { "accessToken": "bat_your_access_token_here", "baseUrl": "https://api.beeps.dev/v0" } ``` ```bash beeps relay list --config ./beeps.cli.json # or set once via environment variable export BEEPS_CLI_CONFIG="./beeps.cli.json" ``` Resolution order (highest priority first): config file > `BEEPS_ACCESS_TOKEN` environment variable. Passing an access token on the command line is not supported — argv is visible to other local users via `ps` and `/proc//cmdline`. ## Global Options These flags are available on every command: | Flag | Description | |------|-------------| | `--base-url ` | Custom API endpoint | | `--config ` | Path to a JSON config file | | `--timeout ` | Request timeout in milliseconds (default: 10000) | | `--retries ` | Number of retry attempts (default: 2) | | `--json` | Output machine-readable JSON | | `--help` | Show usage info | ## Quick Commands ### See who's on call ```bash beeps schedule on-call --schedule-id sch_abc123 ``` ### List active alerts ```bash beeps alert list --active ``` ### Respond to an alert ```bash beeps alert on-it --alert-id alt_abc123 ``` ### Resolve an alert ```bash beeps alert resolve --alert-id alt_abc123 ``` ### List relays ```bash beeps relay list ``` ### Simulate a relay ```bash beeps relay simulate --relay-id rly_abc123 --simulate-at 2025-03-01T00:00:00Z ``` ### Define your setup as code ```bash beeps relay export -o beeps.config.ts # export current setup beeps relay plan -f beeps.config.ts # preview changes beeps relay apply -f beeps.config.ts # apply changes ``` ## Command Reference ``` beeps ├── alert │ ├── list [--active] [--resolved] │ ├── get --alert-id │ ├── on-it --alert-id [--user-id ] │ ├── assign --alert-id --user-id │ ├── resolve --alert-id │ ├── responders --alert-id │ ├── agents --alert-id │ └── fix-context --alert-id ├── relay │ ├── list │ ├── create --name [--external-key ] │ ├── lint --relay-id [--coverage-days ] │ ├── simulate --relay-id --simulate-at │ ├── plan -f │ ├── apply -f [--prune] [--dry-run] │ ├── export -o │ └── rule │ ├── list --relay-id │ ├── get --relay-id --rule-id │ ├── create --relay-id --name --rule-type │ └── delete --relay-id --rule-id ├── schedule │ ├── list │ ├── create --name --relay-id --type ... │ ├── on-call --schedule-id │ ├── assignments --schedule-id [--count ] │ ├── add-member --schedule-id (--email | --user-id ) │ ├── remove-member --schedule-id --user-id │ └── override │ ├── create --schedule-id --user-id --start-at --end-at │ ├── list --schedule-id │ ├── update --schedule-id --override-id │ └── cancel --schedule-id --override-id ├── integration │ ├── list │ ├── get --integration-id │ ├── create --provider --name --access-token-env │ ├── update --integration-id │ └── delete --integration-id ├── webhook │ └── list --relay-id ├── agent-job │ ├── list │ ├── get --job-id │ └── status --job-id ├── member │ └── list └── config └── lint -f ``` ## Reference - [Config-as-Code](/docs/cli/config) — define relays, schedules, and rules in TypeScript - [Relay Commands](/docs/cli/relay) — manage relays and rules directly - [Schedule Commands](/docs/cli/schedule) — manage schedules, members, and overrides - [Alert Commands](/docs/cli/alert) — triage, respond to, and resolve alerts - [Integration Commands](/docs/cli/integration) — connect AI agents and notification services All commands support `--json` for machine-readable output and `--help` for usage info.