Webhook Templates
Shape incoming webhook payloads into structured Alphorn notifications with per-webhook template fields — no external proxies needed.
Many tools send webhooks in their own shape — GitHub, Sentry, Prometheus, and others each use different payload formats. Alphorn turns these into structured notifications without any external proxy or transform service.
You have three options, from most to least work on the sender side:
- Configure the sender natively — if the service supports custom webhook body templating (Grafana, Datadog, PagerDuty, Healthchecks.io, Gatus), make it send Alphorn's native format directly. See Integrations.
- Use Alphorn's per-webhook template fields — for services with fixed payload shapes (GitHub, Sentry, Prometheus Alertmanager), fill in the four template fields on the webhook and Alphorn extracts fields from the body.
- Rely on the heuristic fallback — leave the templates empty and Alphorn picks reasonable defaults from common field names.
This page covers options 2 and 3.
Template fields
Each webhook has four optional template fields in Webhooks → Edit:
| Field | Produces | Notes |
|---|---|---|
| Title template | title (≤ 200 chars) | Short headline |
| Message template | message (≤ 4000 chars) | Notification body |
| Tags template | tags[] | Comma-separated; each part trimmed |
| Priority template | priority (1–5) | Must resolve to a number |
Syntax
Templates use {dotted.path} placeholders resolved against the request body:
{title}→body.title{repository.full_name}→body.repository.full_name{data.issue.level}→body.data.issue.level
Missing paths render as empty. There are no conditionals, loops, or helpers — just substitution. That's intentional: templates stay predictable and can't be used to smuggle logic or secrets.
Heuristic fallback
If a template is empty, Alphorn walks the body plus common wrapper keys (issue, pull_request, data, event, object, payload, form_response) and picks the first string-valued field from:
- Title candidates:
title,subject,name,headline - Message candidates:
message,body,msg,text,content,description,summary
Top-level priority (number) and tags (array or comma-separated string) are read directly. If present, a top-level action string is prefixed to the title (opened: Some issue title).
If nothing matches, the message renders as the full JSON body.
GitHub
The event type is only carried in the X-GitHub-Event header, and templates resolve against the body only — so create one Alphorn webhook per GitHub event type and have GitHub deliver only that event to it.
Pull requests
Title: PR {action}: {pull_request.title}
Message: {sender.login} {action} PR #{pull_request.number} in {repository.full_name}
Tags: github,pr,{action},{repository.name}
Priority: 3Issues
Title: Issue {action}: {issue.title}
Message: {sender.login} {action} issue #{issue.number} in {repository.full_name}
Tags: github,issue,{action},{repository.name}
Priority: 2Pushes
Title: Push to {repository.full_name}
Message: {pusher.name} pushed to {ref} — {head_commit.message}
Tags: github,push,{repository.name}
Priority: 2Sentry
Sentry's default webhook plugin sends events under data.issue:
Title: Sentry: {data.issue.title}
Message: Culprit: {data.issue.culprit} — Level: {data.issue.level} — Events: {data.issue.count}
Tags: sentry,{data.issue.level},{data.project_slug}
Priority: 4PagerDuty (incoming)
Fan PagerDuty incidents out to other channels by pointing an extension at an Alphorn webhook:
Title: PagerDuty: {event.data.title}
Message: Service: {event.data.service.name} — Severity: {event.data.severity} — Status: {event.data.status}
Tags: pagerduty,{event.data.severity},{event.data.status}
Priority: 5Prometheus Alertmanager
Alertmanager sends arrays of alerts with commonLabels and commonAnnotations rollups. Use those instead of the per-alert array (templates can't index into arrays):
Title: {commonLabels.alertname}
Message: {commonAnnotations.summary}
Tags: prometheus,{status},{commonLabels.severity}
Priority: 4Tips
- Priority can't branch on values. If you need different priorities per severity (critical = 5, warning = 3), either create separate webhooks — one per severity — or keep priority constant and route severities to different channels via filter rules.
- Narrow event delivery at the source. Most services let you pick which events each webhook URL receives. Combining that with dedicated Alphorn webhooks keeps templates simple.
- The full body is always stored as
payloadand available for filtering rules, so you don't have to cram everything into the templated message.
Using payload for routing
The full JSON body is stored on every message as payload, regardless of templating. Filter on it to route the same webhook to different channels:
| Channel | Filter | Purpose |
|---|---|---|
| PagerDuty | payload.ref == "refs/heads/main" | Only alert on pushes to main |
| Slack (#frontend) | payload.repository.name == "frontend" | Frontend repo activity |
| Slack (#backend) | payload.repository.name == "api" | Backend repo activity |
See Filtering Rules → JSON Payload Paths for the full syntax.
Direct forwarding
For trivial cases, point any webhook at Alphorn without filling in any template. The raw body is accepted and the heuristic picks a reasonable title and message from common field names. Add templates later when you want more polished notifications.