Skip to content

Webhooks#

External systems can trigger a Lens Prism agent by POSTing to a named webhook endpoint. Where the heartbeat polls on a timer and scheduled tasks run on a calendar, a webhook is push: Alertmanager fires an alert, PagerDuty opens an incident, GitHub merges a PR, and the agent reacts immediately.

Each webhook is a subscription: a named route with its own endpoint URL, an optional prompt template, and a delivery target. When an event arrives, the agent reasons over the payload — or, in deliver-only mode, the rendered template is posted directly with no LLM call — and the result is delivered to your connected channels.

Enable the receiver#

Webhooks are off by default. To enable them, open the web UI Webhooks page and click Enable Webhooks. The toggle takes effect immediately — no restart. While the receiver is disabled, every webhook endpoint answers 403.

Create a subscription#

To create a subscription, click New subscription on the Webhooks page and set its fields:

Field Meaning
Name Lowercase slug ([a-z0-9][a-z0-9_-]*); becomes the endpoint path /agents/webhooks/<name>. Spaces are normalized to hyphens.
Description What this webhook does (optional, shown in the list).
Events Comma-separated event-type allowlist; empty accepts all. See event filtering.
Deliver to Where results go: All channels (default — web chat plus Slack), Chat (web chat only), or Slack (Slack only).
Deliver only Skip the agent: the rendered prompt template is posted directly as the message. Zero LLM cost — useful for plain notifications.
Prompt Instructions for the agent when this webhook fires (optional). Supports payload substitution — see prompt templates.

Each row in the subscriptions list shows the endpoint path with a copy button, an Active switch (disabled subscriptions answer 403 without running anything), and a delete action. Changes apply immediately.

The endpoint#

POST /agents/webhooks/<NAME>
Content-Type: application/json

The body must be a JSON object. Responses (event is the resolved event type — see event filtering):

Status Meaning
202 Accepted Agent run dispatched: {"status":"accepted","event":"<type>","runId":"..."}. The response is delivered to your channels when the run finishes.
200 OK Deliver-only message posted: {"status":"delivered","event":"<type>"} — or the event was filtered out by the Events list: {"status":"ignored","event":"<type>"}.
400 Body was not a JSON object.
403 Receiver disabled, or this subscription is disabled.
404 No subscription with that name.
429 This subscription's run queue is full — retry later.

Agent runs are asynchronous by design — webhook senders like Alertmanager time out quickly and retry on non-2xx, so the endpoint doesn't hold the connection open while the agent works. The agent's response arrives in the conversation (prefixed **Webhook: <name>**) on the subscription's delivery target. If the run fails outright, a brief failure notice is delivered there instead, so a triggered event never disappears silently.

Runs are serialized per subscription: concurrent deliveries are accepted (202) and queued, then executed one at a time in arrival order — so rapid distinct events are never dropped, even from senders that don't retry (GitHub never redelivers automatically). The queue is capped at 10 pending; beyond that, deliveries answer 429 as a backstop against pathological floods. Deliver-only requests are not queued or limited — they cost no LLM tokens.

The runtime does not authenticate webhook calls itself — like every other runtime endpoint, auth is terminated by the platform routing in front of the container. Don't expose the runtime port directly to the internet.

Delivery targets and storage#

Each subscription's Deliver to setting controls both where the message is sent and where it's recorded:

  • All channels (default) — broadcast, exactly like a heartbeat alert: the message appears in web chat and is sent to Slack. On Slack that means your DM (personal mode) or the agent's main channel (team mode — the one flagged on the Channels page, else the oldest linked channel), not every linked channel.
  • Chat — web chat only; nothing is sent to Slack.
  • Slack — Slack only. The message is delivered to your DM (personal mode) or the main channel (team mode) and kept out of web chat: it's stored in that Slack destination's own conversation thread, which the web Recents list and CLI resume picker don't show. If no Slack DM or channel is linked, the message falls back to the web thread (and the runtime logs a warning) so a triggered event is never silently lost.

Prompt templates#

An empty Prompt sends the agent the raw payload as pretty-printed JSON with an event and route lead-in — the agent reasons over the full event. Alertmanager and PagerDuty payloads work well this way with no template at all.

A non-empty Prompt is rendered against the payload first:

  • {field.path} — dot-notation access into the payload, for example {commonLabels.alertname} or {pull_request.title}. Unresolvable keys are left as-is.
  • {__raw__} — the entire payload as indented JSON (truncated to about 4000 characters).

Example, for Alertmanager:

An alert group just fired ({status}):

{__raw__}

Triage it: check the affected pods, look for an obvious cause, and summarize
what you found. Only suggest a remediation if you're confident.

In deliver-only mode the rendered template is the message — for example Alert: {commonLabels.alertname} is {status} posts "Alert: HighMemory is firing" with no agent involvement.

Event filtering#

The Events field matches the incoming event's type, resolved in this order:

Source Where the type comes from Example values
GitHub X-GitHub-Event header push, pull_request, issues
GitLab X-GitLab-Event header Merge Request Hook
Generic top-level event_type field in the payload whatever the sender sets
Anything else unknown

A non-empty Events list accepts only matching types; everything else answers 200 {"status":"ignored"} without running the agent.

Note

Leave Events empty for Alertmanager and PagerDuty. Neither sends a GitHub-style event header or a top-level event_type field, so their deliveries resolve to unknown — an Events list like firing,resolved would silently filter out every event. Put that intent in the Prompt instead ("only act on firing alerts; ignore resolved notifications").

Connect common senders#

Alertmanager — add a webhook receiver to alertmanager.yml:

receivers:
  - name: prism
    webhook_configs:
      - url: http://<PRISM-HOST>/agents/webhooks/alertmanager
        send_resolved: true
route:
  receiver: prism

Alertmanager groups alerts and sends one JSON payload per group (with status, commonLabels, and an alerts array) — the raw-payload default prompt handles it well. Alertmanager retries on non-2xx and re-notifies on repeat_interval; per-subscription run serialization keeps that from fanning out into concurrent agent runs, without dropping distinct alert groups.

PagerDuty — create a v3 webhook subscription (Integrations → Developer Tools → Webhooks) pointing at http://<PRISM-HOST>/agents/webhooks/pagerduty. Events arrive as {"event": {"event_type": "incident.triggered", ...}}.

GitHub — add a repository webhook (Settings → Webhooks) with content type application/json pointing at http://<PRISM-HOST>/agents/webhooks/github. Use the Events field (for example pull_request,issues) to pick the event types you care about.

Anything else — any system that can POST JSON works:

curl -X POST http://<PRISM-HOST>/agents/webhooks/my-hook \
  -H 'Content-Type: application/json' \
  -d '{"event_type": "deploy.finished", "service": "api", "version": "1.4.2"}'