Skip to content

Sending to Slack

With a Slack profile set up (see Slack Endpoints in the DBA Guide), the same compose()/send() calls you use for email post messages to Slack channels. Two things change:

  • The recipient is a Slack channel id, not an email address — p_to => ARRAY['C0123ABCD']. Find it in Slack under channel details → Channel ID. Only the first recipient is used; cc/bcc have no Slack meaning.
  • Formatting is mrkdwn, not HTML. Write p_body_text in Slack's markdown dialect (*bold*, `code`, ``` fences); p_body_html is ignored whenever p_body_text is present.

There are three tiers of message, chosen automatically by what you supply.

Tier 1 — plain text

Body only. Produces a simple Slack message, nothing else:

SELECT pgrelay_notifier.send('slack_bot', ARRAY['C0123ABCD'],
    NULL, 'Deploy of build 1234 finished cleanly.');

Tier 2 — auto-rendered Block Kit

Supply a subject and/or render hints in p_payload, and the message is laid out with Block Kit: a header, a severity-colored sidebar, aligned fields, body sections, a link button, and a footer.

SELECT pgrelay_notifier.send(
    p_profile   => 'slack_bot',
    p_to        => ARRAY['C0123ABCD'],
    p_subject   => 'PG Healthcheck — CRITICAL',
    p_body_text => E'*Details*\nTwo TOAST tables have no owning relation.\n\n'
                   E'*How to fix*\nFollow the catalog''s drop procedure.',
    p_payload   => '{
        "severity":    "CRITICAL",
        "fields":      [{"label": "Check",    "value": "`PGHF07-005`"},
                        {"label": "Database", "value": "`relay_dev`"}],
        "url":         "https://example.com/runbook",
        "button_text": "Open fix procedure",
        "footer":      "pg-health-framework  •  alert HC-2215",
        "text":        "CRITICAL: orphaned TOAST tables on relay_dev"
    }'::jsonb);

How each input renders:

Input Becomes
p_subject A header block, prefixed with the severity's emoji (🚨 / ⚠️ / ✅ / ℹ️) when a severity is set
payload.severity The attachment sidebar color — CRITICAL #d61111, WARNING #e8a317, OK/INFO #2eb67d (case-insensitive) — plus a *Status:* field, always first. No severity → no sidebar; the blocks sit at the top level of the message.
payload.fields Aligned two-column fields, in order, after Status: [{"label": ..., "value": ...}, ...], at most 9 (Slack caps a section at 10 fields and Status may take one slot). Values are mrkdwn.
p_body_text One mrkdwn section per blank-line paragraph, after the fields (with a divider between). More than 20 paragraphs collapse into a single section.
payload.url An actions block with a link button — payload.button_text labels it (default Open link), styled danger when the severity is CRITICAL.
payload.footer A small context line (mrkdwn) at the bottom.
payload.text The one-line notification fallback Slack shows in toasts, mobile previews, and unread lists. Omitted, it derives from the subject, else a one-line prefix of the body.

Every rendered message also sets "unfurl_links": false so pasted URLs don't sprout previews; override it via the payload if you want unfurling.

Tier 3 — raw Block Kit passthrough

For full control, build the chat.postMessage payload yourself: a p_payload containing blocks and/or attachments is treated as Slack-native and delivered verbatim, merged over the basics (channel from the recipient, the text fallback, unfurl_links: false — your keys win):

SELECT pgrelay_notifier.send(
    p_profile => 'slack_bot',
    p_to      => ARRAY['C0123ABCD'],
    p_payload => '{
        "text": "Deploy finished — build 1234",
        "blocks": [
          {"type": "section",
           "text": {"type": "mrkdwn", "text": ":rocket: *Deploy finished* — build 1234"}}
        ]
    }'::jsonb);

A top-level text fallback must exist — supply payload.text, or a subject/body to derive one from; a raw payload with none of those is rejected at compose time. Render hints can't be mixed into a raw payload: you either describe the message (tier 2) or you build it (tier 3).

One caveat to "your keys win": if the DBA's webhook profile carries a body_merge overlay (rare for Slack), those keys are applied last, on the Processor at send time, and always win over yours — they're profile-owned by design.

Extra chat.postMessage arguments

A handful of Slack arguments ride along on any tier and merge into the delivered payload: text, thread_ts (reply in a thread), unfurl_links, unfurl_media, mrkdwn, reply_broadcast, link_names, parse, metadata, username, icon_emoji, icon_url.

-- A plain-text threaded reply:
SELECT pgrelay_notifier.send('slack_bot', ARRAY['C0123ABCD'], NULL, 'On it.',
    p_payload => '{"thread_ts": "1755850000.123456"}'::jsonb);

(A message's own thread timestamp comes back as its provider_ref — the Slack ts — via delivery status, so a follow-up can thread under an earlier send.)

What gets checked, and when

p_payload is validated when you compose, in your transaction, with a message naming every problem — a typo'd key, an unknown severity, a non-http(s) url, malformed fields, a channel key (never allowed — the channel always comes from the recipient), a raw payload with no text fallback. Nothing malformed ever reaches the queue.

What is not pre-checked: Slack's own content limits (3000 characters per section, 50 blocks, 150-character headers — headers are truncated for you) and the validity of raw blocks. Those fail at Slack, and the notification lands as failed with Slack's error (invalid_blocks, msg_too_long, ...) in its status_detail.

Two email features have no Slack equivalent and are quietly skipped: file attachments (Slack's chat.postMessage cannot carry them) and p_body_html.