Skip to main content

PagerDuty Alerting

Open PagerDuty incidents directly from SpatialFlow workflows using the Events API v2. This recipe wires any of the live-ops triggers (Device Offline, Device Stuck, Shift Overdue) into your existing PagerDuty escalation policy.

Prerequisites

  • A PagerDuty account with permission to add integrations to a service
  • A SpatialFlow workflow with one of the three live-ops triggers configured
  • An API key or JWT token for the SpatialFlow API (see Authentication)

Step 1 — Get an Events API v2 Integration Key

  1. In PagerDuty, navigate to Services → choose the destination service (or create one)
  2. Open the Integrations tab and click Add an integration
  3. Select Events API V2 and click Add
  4. Expand the new integration row and copy the Integration Key — it's a 32-character hex string that PagerDuty also calls a "routing key"
Treat the integration key as a secret

Anyone with this key can open an incident on your service. Store it in your workspace's Integrations settings rather than hard-coding it.

Step 2 — Add a Webhook Action to Your Workflow

PagerDuty's Events API v2 is a plain HTTP endpoint, so you'll wire it up with a standard Webhook action. In the Workflow Builder, drag a Webhook action onto the canvas after your trigger node and connect them. Configure:

FieldValue
NameOpen PagerDuty incident
URLhttps://events.pagerduty.com/v2/enqueue
MethodPOST
HeadersContent-Type: application/json
Body(copy from the matching tab below — replace YOUR_INTEGRATION_KEY)

Body — Device Offline

{
"routing_key": "YOUR_INTEGRATION_KEY",
"event_action": "trigger",
"dedup_key": "spatialflow-device-offline-{{trigger.device_id}}",
"payload": {
"summary": "Device offline: {{trigger.device_name}} silent for {{trigger.minutes_since_seen}} min",
"source": "{{trigger.device_id}}",
"severity": "warning",
"component": "{{trigger.device_type}}",
"group": "fleet",
"class": "device_last_seen",
"custom_details": {
"device_id": "{{trigger.device_id}}",
"device_name": "{{trigger.device_name}}",
"device_type": "{{trigger.device_type}}",
"last_seen_at": "{{trigger.last_seen_at}}",
"minutes_since_seen": "{{trigger.minutes_since_seen}}",
"threshold_minutes": "{{trigger.threshold_minutes}}",
"latitude": "{{trigger.location.latitude}}",
"longitude": "{{trigger.location.longitude}}",
"workspace_id": "{{trigger.workspace_id}}"
}
}
}

Body — Device Stuck

{
"routing_key": "YOUR_INTEGRATION_KEY",
"event_action": "trigger",
"dedup_key": "spatialflow-device-stuck-{{trigger.device_id}}",
"payload": {
"summary": "Device stuck: {{trigger.device_name}} moved {{trigger.meters_moved_in_window}} m in {{trigger.window_minutes}} min",
"source": "{{trigger.device_id}}",
"severity": "warning",
"component": "{{trigger.device_type}}",
"group": "fleet",
"class": "device_stuck",
"custom_details": {
"device_id": "{{trigger.device_id}}",
"device_name": "{{trigger.device_name}}",
"device_type": "{{trigger.device_type}}",
"meters_moved_in_window": "{{trigger.meters_moved_in_window}}",
"window_minutes": "{{trigger.window_minutes}}",
"threshold_distance_meters": "{{trigger.threshold_distance_meters}}",
"latitude": "{{trigger.location.latitude}}",
"longitude": "{{trigger.location.longitude}}",
"workspace_id": "{{trigger.workspace_id}}"
}
}
}

Body — Shift Overdue

{
"routing_key": "YOUR_INTEGRATION_KEY",
"event_action": "trigger",
"dedup_key": "spatialflow-shift-overdue-{{trigger.device_id}}",
"payload": {
"summary": "Shift overdue: {{trigger.device_name}} has been on shift for {{trigger.hours_elapsed}} hours",
"source": "{{trigger.device_id}}",
"severity": "warning",
"component": "{{trigger.device_type}}",
"group": "shifts",
"class": "shift_overdue",
"custom_details": {
"device_id": "{{trigger.device_id}}",
"device_name": "{{trigger.device_name}}",
"device_type": "{{trigger.device_type}}",
"shift_started_at": "{{trigger.shift_started_at}}",
"hours_elapsed": "{{trigger.hours_elapsed}}",
"threshold_hours": "{{trigger.threshold_hours}}",
"latitude": "{{trigger.location.latitude}}",
"longitude": "{{trigger.location.longitude}}",
"workspace_id": "{{trigger.workspace_id}}"
}
}
}
About dedup_key

The dedup_key ties multiple trigger fires for the same device to the same PagerDuty incident — PD will not open a second incident as long as the first one is still alive. This pairs naturally with repeat_policy: every_interval on the SpatialFlow side: each repeat fire becomes a follow-up event on the open incident rather than a new page.

If you want a brand-new incident per fire, change the dedup_key to include a timestamp (e.g., "spatialflow-device-offline-{{trigger.device_id}}-{{trigger.last_seen_at}}").

Step 3 — Test the Workflow

  1. In the Workflow Builder, click Test
  2. Review the mock trigger payload and run the test
  3. In PagerDuty, open the service's incident list — you should see the new incident within a couple of seconds
  4. If the test passes, Save then Deploy the workflow
Confirming via curl

You can verify the integration key independently of SpatialFlow:

curl -X POST https://events.pagerduty.com/v2/enqueue \
-H 'Content-Type: application/json' \
-d '{
"routing_key": "YOUR_INTEGRATION_KEY",
"event_action": "trigger",
"dedup_key": "manual-test-1",
"payload": {
"summary": "Manual smoke test from curl",
"source": "localhost",
"severity": "info"
}
}'

A 200 OK with {"status":"success", ...} confirms the key is valid.

Resolving Incidents

To automatically close PagerDuty incidents when the device recovers, you'd need a paired resolve event. SpatialFlow does not currently emit a "recovery" trigger when a device starts reporting again or a shift ends — so the typical pattern is:

  • Use repeat_policy: once_until_seen (the default) so the workflow does not re-page during the same anomaly window
  • Have your on-call resolve the incident manually once they've handled it
  • If you need automatic resolution, post-process the SpatialFlow webhook stream on your end and emit your own resolve events against PagerDuty

Best Practices

  • One service per anomaly class — split Device Offline, Device Stuck, and Shift Overdue across different PagerDuty services so each has its own escalation policy and on-call rotation
  • Severity tuningwarning is a sensible default; reserve critical for production-impacting anomalies (e.g., a regulated hours-of-service breach)
  • Use custom_details — PagerDuty's mobile app and incident summary view both surface custom details prominently, so include latitude/longitude and last_seen_at for fast triage
  • Store the integration key in an Integration rather than per-workflow so you can rotate it in one place

Troubleshooting

PagerDuty returns Invalid Routing Key. The integration key is wrong, or the integration has been deleted from the service. Regenerate it in PagerDuty (Step 1).

Incidents open but then immediately auto-resolve. Your service has an aggressive auto-resolve timer. Open the service in PagerDuty → Incident Settings → adjust Auto-resolve incidents.

Multiple devices share a single incident. That's a dedup_key collision — the recipe scopes the key by device_id, but if you have a workflow that fires across multiple devices simultaneously and you used a static dedup key, they'll all merge. Always interpolate {{trigger.device_id}} into the dedup key.

See Also