Schedule Configured + PagerDuty Collector
Enforce Schedule Configured using data collected by PagerDuty Collector. Automatically check operational readiness standards on every PR.
How PagerDuty Collector Powers This Guardrail
The PagerDuty Collector gathers metadata from your incident management systems. This data flows into Lunar's Component JSON, where the Schedule Configured guardrail evaluates it against your standards.
When enabled, this check runs automatically on every PR and in AI coding workflows, providing real-time enforcement with actionable feedback.
Quick Start Configuration
Add both the collector and policy to your lunar-config.yml to enable this guardrail.
# Step 1: Enable the PagerDuty Collector
collectors:
- uses: github://earthly/lunar-lib/collectors/pagerduty@v1.0.5
# with: ...
# Step 2: Enable the On-Call Guardrails
policies:
- uses: github://earthly/lunar-lib/policies/oncall@v1.0.5
include: [schedule-configured]
# with: ...
What PagerDuty Collector Collects
This collector gathers the following data that the Schedule Configured guardrail evaluates.
oncall
Queries the PagerDuty REST API for the component's service on a code hook
— it runs when commits are pushed (PRs and the default branch). Resolves
the service ID in order: the component's pagerduty/service-id meta
annotation (set by a cataloger via
lunar catalog component --meta pagerduty/service-id <id>), then the
explicit service_id input, then — with backstage_discovery: "true" —
the component's own catalog-info.yaml from the checkout. Records where
the ID came from at .oncall.service.discovered_via. Fetches service
details, on-call schedule participants, and escalation policy. Writes
normalized data to .oncall.service, .oncall.schedule, .oncall.escalation,
and .oncall.summary, and the raw API responses under
.oncall.native.pagerduty. When none of those sources maps the component,
writes .oncall.service_lookup with the places it looked instead, and the
from-backstage-collector sub-collector, if included, looks in the live catalog.
oncall-cron
Same PagerDuty query as oncall, but on a daily cron instead of on push —
it re-collects on a schedule so .oncall stays fresh as on-call schedules
and escalation policies rotate between commits (they change on their own
clock, independent of code). Resolves the service ID identically, then
also reads the backstage collector's live lookup from the default
branch's Component JSON when there is one. Writes the same
normalized .oncall.service, .oncall.schedule, .oncall.escalation, and
.oncall.summary (raw responses under .oncall.native.pagerduty).
clone-code gives it the checkout so backstage_discovery: "true" can
read catalog-info.yaml. Include oncall for on-push evaluation,
oncall-cron for always-fresh scheduled refresh, or both.
from-backstage-collector
Finds the component's PagerDuty service in what the backstage collector
read from the live catalog: the component's own entity, else its System,
else that System's Domain. Covers the components oncall can't map from
meta, service_id or, with backstage_discovery: "true", the catalog
file, and leaves those to oncall. Runs once the backstage collector's
lookup (.catalog.native.backstage.refs.entity) lands in the Component
JSON, retrying the read for up to backstage_wait_seconds while it
catches up, then queries PagerDuty and writes .oncall like oncall. Only
the backstage collector calls Backstage; it needs backstage_url set.
Example Data Flow
Here's an example of the data that PagerDuty Collector writes to the Component JSON, which Schedule Configured then evaluates.
{
"oncall": {
"source": {
"tool": "pagerduty",
"integration": "api"
},
"service": {
"id": "PXXXXXX",
"name": "Payment API",
"status": "active",
"discovered_via": "system:default/payment-platform"
},
"schedule": {
"exists": true,
"participants": 4,
"rotation": "weekly"
},
"escalation": {
"exists": true,
"levels": 3,
"policy_name": "Payment API Escalation"
},
"summary": {
"has_oncall": true,
"has_escalation": true,
"min_participants": 4
},
"native": {
"pagerduty": { "...full PagerDuty API response..." }
}
}
}
Configuration Options
PagerDuty Collector Inputs
| Input | Required | Default | Description |
|---|---|---|---|
service_id |
Required | — | PagerDuty service ID to query (e.g. PXXXXXX). Optional if the component has a `pagerduty/service-id` meta annotation set by a cataloger. |
pagerduty_base_url |
Optional |
https://api.pagerduty.com
|
PagerDuty API base URL |
backstage_discovery |
Optional |
false
|
When "true", and component meta and the service_id input don't give a service ID, read it from the annotations in the component's checked-out catalog-info.yaml. The live catalog, including an ID inherited from the component's System or Domain, is the `from-backstage-collector` sub-collector's job and needs no setting here. |
backstage_annotations |
Optional |
pagerduty.com/service-id,pagerduty/service-id
|
Comma-separated annotation keys to read the service ID from, tried in order (first non-empty value wins). Defaults to both conventional spellings — PagerDuty's Backstage integration guide uses `pagerduty.com/service-id`; Lunar's meta uses `pagerduty/service-id`. Used for the checked-out catalog-info.yaml and the live catalog alike. |
backstage_catalog_paths |
Optional |
catalog-info.yaml,catalog-info.yml
|
Comma-separated catalog-info file paths to try in the checked-out repo (first match wins). Only used when backstage_discovery is "true". |
backstage_wait_seconds |
Optional |
1800
|
How long the `from-backstage-collector` sub-collector keeps retrying the read of the backstage collector's lookup before it fails the run. The Component JSON it reads can lag the write that triggered it by several minutes. |
On-Call Guardrails Inputs
| Input | Required | Default | Description |
|---|---|---|---|
min_participants |
Optional |
2
|
Minimum number of on-call rotation participants required |
Ready to Automate Your Standards?
See how Lunar can turn your AGENTS.md, engineering wiki, compliance docs, or postmortem action items into automated guardrails with our 300+ built-in guardrails.