Universal Run Log + Slack Alerts (Notion DB + Slack Manifest + Snippets)
Copy‑ready assets to centralize your automation run history in Notion and post rate‑safe Slack alerts on thresholds, not every event. Includes the Notion DB schema, Slack app manifest, Block Kit payloads, Make/Zapier/n8n snippets, and Retry‑After helpers.
Use this template pack to ship a single run log across all workflows and rate‑safe Slack alerts in under an hour. Fill the [BRACKETS], paste the snippets into your tools, and stick to the constraints called out in each section so you don’t trip 429s or payload caps.
Section 1 — Run Log database (Notion) with copy‑ready schema
Create a Notion database named "Run Log" with these exact properties. Keep writes lean: small scalars, no giant arrays, links to payloads instead of blobs.
Required properties (Notion types):
- workflow_name — Title
- run_id — Rich text (unique per run; use as idempotency key)
- client — Rich text
- started_at — Date (UTC ISO string)
- duration_ms — Number
- status — Select (values: ok, warn, fail)
- error_code — Rich text (optional)
- payload_link — URL (to JSON body, e.g., S3)
- rerun_url — URL (to your re-run endpoint)
Optional properties:
- env — Select (values: prod, staging, dev)
- service — Rich text (system/component)
- cost_cents — Number
CSV quick-create (import this as a CSV to generate columns fast):
workflow_name,run_id,client,started_at,duration_ms,status,error_code,payload_link,rerun_url,env,service,cost_cents
API property schema skeleton (use if creating the DB via API):
{
"parent": { "type": "page_id", "page_id": "[PARENT_PAGE_ID]" },
"title": [{ "type": "text", "text": { "content": "Run Log" } }],
"properties": {
"workflow_name": { "title": {} },
"run_id": { "rich_text": {} },
"client": { "rich_text": {} },
"started_at": { "date": {} },
"duration_ms": { "number": { "format": "number" } },
"status": { "select": { "options": [
{"name": "ok", "color": "green"},
{"name": "warn", "color": "yellow"},
{"name": "fail", "color": "red"}
]}},
"error_code": { "rich_text": {} },
"payload_link": { "url": {} },
"rerun_url": { "url": {} },
"env": { "select": { "options": [
{"name": "prod", "color": "red"},
{"name": "staging", "color": "yellow"},
{"name": "dev", "color": "blue"}
]}},
"service": { "rich_text": {} },
"cost_cents": { "number": { "format": "number" } }
}
}
Guardrails:
- Keep each write < 500 KB and arrays < 100 items. If you need human‑readable details, append blocks later in batches ≤ 100 with at most two nested levels per request.
- Average ≤ 3 requests/second per integration; on 429, honor Retry‑After seconds before retrying.
Section 2 — Notion write payload (copy/paste)
Post a compact page to your Run Log database. This is the only write your run path must do.
Endpoint: POST https://api.notion.com/v1/pages
Headers:
- Authorization:
Bearer [NOTION_API_TOKEN] - Notion-Version:
[NOTION_API_VERSION](e.g., 2022-06-28 or newer) - Content-Type:
application/json
Body:
{
"parent": { "database_id": "[RUN_LOG_DB_ID]" },
"properties": {
"workflow_name": { "title": [{ "text": { "content": "[WORKFLOW_NAME]" } }] },
"run_id": { "rich_text": [{ "text": { "content": "[RUN_ID]" } }] },
"client": { "rich_text": [{ "text": { "content": "[CLIENT_NAME]" } }] },
"started_at": { "date": { "start": "[STARTED_AT_ISO]" } },
"duration_ms": { "number": [DURATION_MS] },
"status": { "select": { "name": "[STATUS]" } },
"error_code": { "rich_text": [{ "text": { "content": "[ERROR_CODE]" } }] },
"payload_link": { "url": "[PAYLOAD_URL]" },
"rerun_url": { "url": "[RERUN_URL]" },
"env": { "select": { "name": "[ENV]" } },
"service": { "rich_text": [{ "text": { "content": "[SERVICE]" } }] },
"cost_cents": { "number": [COST_CENTS] }
}
}
Notes:
- Use
[RUN_ID]as an idempotency key in your own code to prevent dups. - Store large payloads at
[PAYLOAD_URL](e.g.,https://[S3_BUCKET]/runs/[RUN_ID].json). - If you must add narrative details, append blocks to the created page in small batches; don’t bloat the initial write.
Section 3 — Slack app manifest (minimal, safe defaults)
This manifest gives you a bot with chat:write and keeps the app minimal. After installing, enable Incoming Webhooks in the Slack UI to generate [SLACK_WEBHOOK_URL].
manifest.json:
{
"display_information": { "name": "[WORKSPACE]-RunAlerts" },
"features": {
"bot_user": { "display_name": "Run Alerts", "always_online": false }
},
"oauth_config": {
"scopes": {
"bot": ["chat:write"]
}
},
"settings": {
"event_subscriptions": { "bot_events": [], "request_url": "" },
"interactivity": { "is_enabled": false },
"socket_mode_enabled": false
}
}
Install steps:
- Create the app from manifest in your workspace.
- Install to workspace → copy the Bot User OAuth Token as
[SLACK_BOT_TOKEN]. - In App Features, toggle "Incoming Webhooks" on → copy a channel‑scoped webhook URL as
[SLACK_WEBHOOK_URL].
Why both? Use chat.postMessage once to post the parent (captures ts). Then use the webhook for a threaded details reply using thread_ts to keep channel noise low.
Rate‑safety:
- Post at most ~1 message/second per channel. If you get HTTP 429, read
Retry-Afterand wait that many seconds before retrying once. - Read methods have separate limits. Avoid bulk reads; store context you’ll need at write time.
Section 4 — Slack message templates (parent + thread)
Parent (channel one‑liner via chat.postMessage):
{
"channel": "[ALERTS_CHANNEL_ID]",
"text": "[WORKFLOW_NAME] • [STATUS] • [DURATION_MS_READABLE] • [CLIENT_NAME] — run [RUN_ID]",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*[WORKFLOW_NAME]* • *[STATUS]* • [DURATION_MS_READABLE] • [CLIENT_NAME] — run `[RUN_ID]`"
}
},
{
"type": "context",
"elements": [
{ "type": "mrkdwn", "text": "env: [ENV] • service: [SERVICE]" },
{ "type": "mrkdwn", "text": "< [RERUN_URL] |rerun > • < [PAYLOAD_URL] |payload >" }
]
}
]
}
Response gives ts — save as [PARENT_TS].
Threaded details (via Incoming Webhook to the same channel):
{
"text": "[WORKFLOW_NAME] details",
"thread_ts": "[PARENT_TS]",
"blocks": [
{ "type": "header", "text": { "type": "plain_text", "text": "[WORKFLOW_NAME] — details", "emoji": true } },
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": "*Run ID*\n`[RUN_ID]"},
{"type": "mrkdwn", "text": "*Client*\n[CLIENT_NAME]"},
{"type": "mrkdwn", "text": "*Status*\n[STATUS]"},
{"type": "mrkdwn", "text": "*Duration*\n[DURATION_MS_READABLE]"}
]
},
{"type": "section", "text": {"type": "mrkdwn", "text": "*Error*\n`[ERROR_CODE]`"}},
{"type": "section", "text": {"type": "mrkdwn", "text": "*Links*\n• <[RERUN_URL]|Rerun> • <[PAYLOAD_URL]|Payload> • <[NOTION_PAGE_URL]|Run Log>"}}
]
}
Notes:
- Incoming webhook responses don’t include
ts. Use the parenttsfromchat.postMessageasthread_ts. - Keep the parent tiny. Put verbose context in the thread.
Section 5 — Threshold policy template (alert only when it matters)
Define when to alert instead of posting every event. Paste this policy into your ops docs or a config file.
policy.yaml:
workflows:
- name: [WORKFLOW_NAME]
channel_id: [ALERTS_CHANNEL_ID]
thresholds:
on_fail: true # always alert on fail
on_warn: true # optional warn channel
p95_duration_ms: [P95_MS] # alert if last [LOOKBACK_RUNS] p95 exceeds this
lookback_runs: [LOOKBACK_RUNS]
consecutive_failures: [FAIL_THRESHOLD] # alert if this many in a row
suppress:
during_hours: [] # e.g., ["22:00-06:00"] local
sample_rate_ok: 0.0 # 0.0 = never alert on OK
links:
rerun_url_prefix: "[RERUN_URL_PREFIX]"
payload_url_prefix: "[S3_URL_PREFIX]"
Implementation tip:
- Compute p95 over the last
[LOOKBACK_RUNS]stored in Notion (or your cache). Only post the parent if thresholds trip. Otherwise, write to Notion silently.
Section 6 — Make (Integromat) snippet
Scenario outline:
- HTTP Make a request → Notion (create page)
- Tools → Sleep (only on 429 with header seconds)
- HTTP Make a request → Slack
chat.postMessage(parent) - HTTP Make a request → Slack Incoming Webhook (thread)
Module 1 — Notion write (HTTP):
- Method: POST
- URL:
https://api.notion.com/v1/pages - Headers:
- Authorization:
Bearer [NOTION_API_TOKEN] - Notion-Version:
[NOTION_API_VERSION] - Content-Type:
application/json
- Authorization:
- Body: use the JSON from Section 2 with mapped variables.
- Error handling: If status = 429, route to a Sleep module with
{{headers["Retry-After"] * 1000}}then retry once.
Module 2 — Slack parent (HTTP):
- Method: POST
- URL:
https://slack.com/api/chat.postMessage - Headers: Authorization:
Bearer [SLACK_BOT_TOKEN], Content-Type:application/json - Body: Section 4 parent JSON. Capture
tsfrom response →[PARENT_TS].
Module 3 — Slack thread via webhook (HTTP):
- Method: POST
- URL:
[SLACK_WEBHOOK_URL] - Body: Section 4 thread JSON with
thread_ts: [PARENT_TS].
Make note:
- Space requests by ≥ 1 second when posting multiple messages to the same channel. Use a Sleep module to enforce if needed.
Section 7 — Zapier snippet
Zap outline:
- Trigger: [YOUR TRIGGER]
- Webhooks by Zapier — Custom Request (Notion create page)
- Code by Zapier (Python) — safe_post for Slack parent with 429 handling
- Webhooks by Zapier — POST to Incoming Webhook (thread)
Step 2 — Notion create page:
- Method: POST, URL:
https://api.notion.com/v1/pages - Headers:
Authorization: Bearer [NOTION_API_TOKEN],Notion-Version: [NOTION_API_VERSION],Content-Type: application/json - Data: Section 2 JSON.
Step 3 — Code by Zapier (Python):
import requests, time, json
def safe_post(token, payload):
url = "https://slack.com/api/chat.postMessage"
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
r = requests.post(url, headers=headers, data=json.dumps(payload))
if r.status_code == 429:
wait = int(r.headers.get("Retry-After", "1"))
time.sleep(wait)
r = requests.post(url, headers=headers, data=json.dumps(payload))
return r.json()
parent = {
"channel": "[ALERTS_CHANNEL_ID]",
"text": "[WORKFLOW_NAME] • [STATUS] • [DURATION_MS_READABLE] • [CLIENT_NAME] — run [RUN_ID]"
}
res = safe_post(input_data['SLACK_BOT_TOKEN'], parent)
return {"parent_ts": res.get("ts")}
Step 4 — Webhook to thread:
- URL:
[SLACK_WEBHOOK_URL] - JSON body: Section 4 thread JSON using
thread_tsfrom step 3 ({{steps.code.parent_ts}}).
Section 8 — n8n snippet
Workflow:
- HTTP Request — Notion create page
- IF response.status = 429 → Wait node for
{{$json["headers"]["Retry-After"] * 1000}}ms → repeat - HTTP Request — Slack
chat.postMessage(storets) - HTTP Request — Incoming Webhook with
thread_ts
Function node (between 1 and 3) for 429 handling example:
// Inputs: items[0].json has previous HTTP response
const resp = items[0].json;
const status = resp.statusCode || resp.status;
if (status === 429) {
const wait = Number(resp.headers?.['retry-after'] || 1);
return [{ json: { action: 'sleep', ms: wait * 1000 } }];
}
return items;
Notes:
- Set Slack requests to sequential execution to respect ~1 msg/sec/channel.
- Capture
tsfrom Slack response JSON:{{$json["ts"]}}.
Section 9 — Retry‑After backoff helpers (Node + Python)
Use these helpers anywhere you make Slack/Notion calls.
Node (fetch):
async function safeFetch(url, opts = {}, retries = 1) {
const res = await fetch(url, opts);
if (res.status === 429 && retries > 0) {
const wait = Number(res.headers.get('retry-after') || 1);
await new Promise(r => setTimeout(r, wait * 1000));
return safeFetch(url, opts, retries - 1);
}
return res;
}
Python (requests):
import time, requests
def safe_request(method, url, **kwargs):
r = requests.request(method, url, **kwargs)
if r.status_code == 429:
wait = int(r.headers.get('Retry-After', '1'))
time.sleep(wait)
r = requests.request(method, url, **kwargs)
return r
Scope backoff per method/workspace (Slack) and per integration (Notion). Don’t pause unrelated work.
Section 10 — Optional: append human‑readable Notion details (blocks)
Only add blocks for human readers. Keep each append under 100 children and at most two nested levels per request.
Endpoint: PATCH https://api.notion.com/v1/blocks/[PAGE_OR_BLOCK_ID]/children
Body example (append a collapsible with a few bullets):
{
"children": [
{
"object": "block",
"type": "toggle",
"toggle": {
"rich_text": [{"type": "text", "text": {"content": "Details for [RUN_ID]"}}],
"children": [
{"object": "block", "type": "paragraph", "paragraph": {"rich_text": [{"type": "text", "text": {"content": "Error: [ERROR_CODE]"}}]}},
{"object": "block", "type": "bulleted_list_item", "bulleted_list_item": {"rich_text": [{"type": "text", "text": {"content": "env: [ENV]"}}]}}
]
}
}
]
}
Tip: If you need more than 100 child blocks, make multiple append calls and respect 429s.