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/bcchave no Slack meaning. - Formatting is mrkdwn, not HTML. Write
p_body_textin Slack's markdown dialect (*bold*,`code`,```fences);p_body_htmlis ignored wheneverp_body_textis 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.