Pipeline template

Forge → PagerDuty / Opsgenie

Page the on-call engineer the moment a machine anomaly fires, with the machine ID and the reading already attached. The fastest template here: no shim, no OAuth, two API calls.

Setup: ~2 minutes Direct: no shim, no token rotation Auto-resolve: supported Target: Events API v2 / Alert API v2

Before and after

Manual

  1. A machine starts drifting at 02:00.
  2. Nobody is watching the historian overnight.
  3. The line stops before anyone knows there was a signal.
  4. The morning shift starts the investigation cold.

With Forge

  1. Forge evaluates the condition on every normalize event.
  2. The on-call phone rings at 02:00 with the machine ID.
  3. The alert carries the field, the value, and the threshold.
  4. When the reading recovers, the incident resolves itself.

Severity mapping

One trigger per severity band. Forge interpolates values but does not evaluate expressions, so severity is set on the tool rather than computed from the reading.

ConditionPagerDuty severityOpsgenie priorityMeaning
Threshold hard-breachedcriticalP1Stop the line now
Breach forecast within 4herrorP2Intervene this shift
Breach forecast within 24hwarningP3Schedule maintenance
Anomaly detected, no thresholdinfoP4Worth a look

PagerDuty

  1. Get a routing key

    In PagerDuty: Service → Integrations → Add integration → Events API v2. Copy the Integration Key. That is your routing key, and it is the only credential you need. The Events API takes the key in the body, so no auth header is involved.

  2. Register PagerDuty as a Forge tool

    curl -sS -X POST https://forge.foundrynet.io/v1/tools \
      -H "Authorization: Bearer $FORGE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "PagerDuty critical",
        "description": "Page on-call for a hard threshold breach",
        "url": "https://events.pagerduty.com/v2/enqueue",
        "method": "POST",
        "auth_type": "none",
        "payload_template": {
          "routing_key": "YOUR_INTEGRATION_KEY",
          "event_action": "trigger",
          "dedup_key": "forge-{{machine_id}}-{{field}}",
          "payload": {
            "summary": "{{trigger_name}}: {{field}} is {{value}} on {{machine_id}} (limit {{threshold}})",
            "source": "{{machine_id}}",
            "severity": "critical",
            "component": "{{field}}",
            "group": "{{site}}",
            "class": "machine-threshold",
            "custom_details": {
              "machine_id": "{{machine_id}}",
              "oem": "{{oem}}",
              "model": "{{model}}",
              "site": "{{site}}",
              "canonical_field": "{{field}}",
              "actual_value": "{{value}}",
              "threshold": "{{threshold}}",
              "detected_at": "{{timestamp}}",
              "forge_trigger_id": "{{trigger_id}}"
            }
          }
        }
      }'
    dedup_key is built from the machine and field, so repeat fires on the same problem group into one incident instead of paging the same person forty times. It is also the handle used to auto-resolve below.
  3. Create the automation

    curl -sS -X POST https://forge.foundrynet.io/v1/triggers \
      -H "Authorization: Bearer $FORGE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Bearing temp critical",
        "machine_id": "YOUR_MACHINE_ID",
        "condition": { "field": "sensor_readings.bearing_temp", "op": "gt", "value": 95 },
        "actions": [ { "tool_id": "TOOL_ID_FROM_STEP_2" } ],
        "enabled": true
      }'

    That is the whole integration. The next normalize event that reports a bearing temperature above 95 pages your on-call.

  4. Resolve it automatically

    Register a second tool identical to the first but with "event_action": "resolve" and the same dedup_key shape, then a trigger with the inverse condition.

    curl -sS -X POST https://forge.foundrynet.io/v1/triggers \
      -H "Authorization: Bearer $FORGE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Bearing temp recovered",
        "machine_id": "YOUR_MACHINE_ID",
        "condition": { "field": "sensor_readings.bearing_temp", "op": "lte", "value": 90 },
        "actions": [ { "tool_id": "RESOLVE_TOOL_ID" } ],
        "enabled": true
      }'
    Resolve at a lower value than you alert at, 90 against 95 here. A single threshold used for both directions will flap and re-page every time the reading wobbles across the line.

Opsgenie

Same shape, different destination. Opsgenie authenticates with a GenieKey header rather than a bearer token, which Forge sends as a plain header.

curl -sS -X POST https://forge.foundrynet.io/v1/tools \
  -H "Authorization: Bearer $FORGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Opsgenie P1",
    "description": "Raise a P1 alert for a hard threshold breach",
    "url": "https://api.opsgenie.com/v2/alerts",
    "method": "POST",
    "auth_type": "none",
    "headers": { "Authorization": "GenieKey YOUR_OPSGENIE_API_KEY" },
    "payload_template": {
      "message": "{{trigger_name}}: {{field}} is {{value}} on {{machine_id}}",
      "alias": "forge-{{machine_id}}-{{field}}",
      "description": "Machine {{machine_id}} ({{oem}} {{model}}) at {{site}} reported {{field}} = {{value}} against a limit of {{threshold}} at {{timestamp}}.",
      "priority": "P1",
      "source": "Forge",
      "entity": "{{machine_id}}",
      "tags": ["forge", "machine-threshold"],
      "details": {
        "machine_id": "{{machine_id}}",
        "canonical_field": "{{field}}",
        "actual_value": "{{value}}",
        "threshold": "{{threshold}}",
        "site": "{{site}}",
        "forge_trigger_id": "{{trigger_id}}"
      }
    }
  }'
Use the EU endpoint https://api.eu.opsgenie.com/v2/alerts if your Opsgenie account is in the European region. A region mismatch returns 401 and is easy to misread as a bad key.

alias is Opsgenie's deduplication handle, equivalent to PagerDuty's dedup_key. Close an alert by posting to /v2/alerts/{alias}/close?identifierType=alias from a second tool.

What PagerDuty receives

POST https://events.pagerduty.com/v2/enqueue
Content-Type: application/json
X-FoundryNet-Trigger-Id: 7b1e4c92-3d05-4a61-9f28-c4a70d1b8e33

{
  "routing_key": "R0XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "event_action": "trigger",
  "dedup_key": "forge-mint_7f3a91c4-sensor_readings.bearing_temp",
  "payload": {
    "summary": "Bearing temp critical: sensor_readings.bearing_temp is 97.2 on mint_7f3a91c4 (limit 95)",
    "source": "mint_7f3a91c4",
    "severity": "critical",
    "component": "sensor_readings.bearing_temp",
    "group": "Plant 3 / Cell B",
    "class": "machine-threshold",
    "custom_details": {
      "machine_id": "mint_7f3a91c4",
      "oem": "Haas",
      "model": "VF-2",
      "canonical_field": "sensor_readings.bearing_temp",
      "actual_value": "97.2",
      "threshold": "95",
      "detected_at": "2026-07-28T18:04:11.882714+00:00",
      "forge_trigger_id": "7b1e4c92-3d05-4a61-9f28-c4a70d1b8e33"
    }
  }
}
Values inside custom_details arrive as strings. PagerDuty treats custom details as free-form, so this is display-only and nothing rejects it. severity is a fixed enum string set on the tool, which is what the Events API expects.

Paging on a forecast

To page before the threshold is crossed rather than after, use a predicted_breach condition. That path posts a fixed body to a plain webhook_url and does not run actions[], so it needs a small forwarding endpoint rather than the direct wiring above.

{
  "name": "Bearing temp will breach within 4h",
  "machine_id": "YOUR_MACHINE_ID",
  "condition": {
    "type": "predicted_breach",
    "field": "sensor_readings.bearing_temp",
    "threshold": 95,
    "direction": "above",
    "breach_within_hours": 4,
    "confidence_threshold": 0.75
  },
  "webhook_url": "https://your-shim.example.com/forge/predicted-breach"
}

The ServiceNow template documents the forecast payload in full. Forwarding it to PagerDuty is a few lines, since the Events API needs no auth header:

ttb = body["forecast"]["time_to_breach_hours"]
severity = "error" if ttb <= 4 else "warning"
requests.post("https://events.pagerduty.com/v2/enqueue", json={
    "routing_key": ROUTING_KEY,
    "event_action": "trigger",
    "dedup_key": f"forge-{body['machine_id']}-{body['field']}-forecast",
    "payload": {
        "summary": f"{body['field']} forecast to breach {body['threshold']} in {ttb}h on {body['machine_id']}",
        "source": body["machine_id"],
        "severity": severity,
        "custom_details": body["forecast"],
    },
})

Next