Developer docs
Terraform
Manage teams, rotations, PagerDuty and Opsgenie syncs, escalation policies, alert integrations, services and group on-calls as code, with drift showing up in terraform plan.
The Pagerly provider keeps your on-call configuration in code. Changes go through a pull request, CI applies them, and terraform plan shows anything someone changed by hand in Slack or the dashboard. It works with Terraform and OpenTofu.
Early access. The provider is not on the public registry yet. Email hello@pagerly.io and we will send you the build while the listing is on its way. Everything below is what you get.
What it manages
| Resource | Manages |
|---|---|
pagerly_team |
A team: settings, shift reminder, Slack user group, and either a Pagerly rotation or a sync from PagerDuty, Opsgenie, Datadog, JSM Ops or Google Calendar |
pagerly_escalation_policy |
Up to three layers of who is paged, how long each waits, and how often it repeats |
pagerly_alert_integration |
An inbound alert webhook (Datadog, Grafana, CloudWatch, Prometheus and more), where its alerts go, and its routing rules |
pagerly_service |
A service incidents are raised against, with its owning team and escalation policy |
pagerly_oncall_group |
A group on-call: one name that stands for the on-call of several teams |
pagerly_user |
A person in the on-call directory, and the number Pagerly dials |
Data sources: pagerly_team (look up by name or id) and pagerly_oncall (who is on call right now).
Overrides, shift swaps, incidents and who is on call are runtime state, not configuration. They stay in Slack, the dashboard and the MCP server.
Set up
- An org admin creates an API key in the dashboard under MCP, API & Terraform → API Keys. The key acts as the admin who created it.
- Store it in your CI secrets and expose it as
PAGERLY_API_KEY. - Add the provider:
terraform {
required_providers {
pagerly = {
source = "pagerly/pagerly"
}
}
}
provider "pagerly" {
# reads PAGERLY_API_KEY when api_key is not set
}
People are always given as email addresses, never Slack ids, so the same files keep working after a Slack reinstall.
A team with a rotation
resource "pagerly_team" "payments" {
name = "Payments"
timezone = "Europe/London"
slack_channel = "C0123456789"
rotation = {
users = ["alice@example.com", "bob@example.com"]
frequency = "Weekly" # Hourly | Daily | Weekly | BiWeekly | Monthly
day = "Monday"
time = "09:00"
}
reminder = {
type = "Every Rotation" # Daily | Weekly | Custom | Every Rotation
}
}
A team synced from PagerDuty
Keep the schedule in PagerDuty, Opsgenie, Datadog On-Call, JSM Ops or Google Calendar, and let Pagerly follow it. The source must be connected under Scheduling Integrations first.
resource "pagerly_team" "platform" {
name = "Platform"
slack_channel = "C0123456789"
sync = {
source = "pagerduty" # pagerduty | opsgenie | datadog | jsmops | gcal
schedule_id = "PABC123"
}
}
The schedule id is checked against the source during apply, so a typo fails the run instead of creating a team nobody is paged from. Changing schedule_id updates the team in place. Switching a team between rotation and sync recreates it.
Escalation and alerts
resource "pagerly_escalation_policy" "payments_p1" {
name = "Payments P1"
layer = [
{ team_id = pagerly_team.payments.id, wait_minutes = 5 },
{ people = ["cto@example.com"], wait_minutes = 10 },
]
repeat = 1
}
resource "pagerly_alert_integration" "datadog" {
name = "Datadog production"
source = "datadog"
escalation_id = pagerly_escalation_policy.payments_p1.id
rules = jsonencode([
{
name = "Ignore info"
conditions = [{ field = "severity", op = "is", value = "info" }]
action = "ignore"
},
])
}
output "datadog_webhook_url" {
value = pagerly_alert_integration.datadog.webhook_url
}
An integration sends alerts to exactly one of team_id or escalation_id. Its webhook URL is an output: paste it into the monitoring tool once.
Services and group on-calls
resource "pagerly_service" "checkout" {
name = "Checkout"
team_id = pagerly_team.payments.id
escalation_id = pagerly_escalation_policy.payments_p1.id
}
resource "pagerly_oncall_group" "backend" {
name = "Backend"
team_ids = [pagerly_team.payments.id, pagerly_team.platform.id]
}
Bring existing configuration under Terraform
Every resource can be imported by id; a user by email and a group on-call by name. The dashboard’s Terraform page lists ready-made import blocks for all your teams.
import {
to = pagerly_team.platform
id = "<team id>"
}
terraform plan -generate-config-out=teams.tf
Terraform writes the matching configuration, and the plan shows nothing to change.
Good to know
- Plans are quiet. The API returns exactly the attributes you configure, in one canonical form, so a second
planafterapplyshows no changes. - Writes go one at a time. Most configuration lives on one workspace record, so the provider sends one change at a time and reads in parallel.
- Deleting a team soft-deletes it; it can be restored from the dashboard.
- Permissions are the ones the key’s creator has; see Permissions.
Updated 19 October 2026
Put your on-call where your team already is.
Install in two minutes. Import your existing schedule. Page someone tonight.
