Template

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:

{
  &quot;parent&quot;: { &quot;database_id&quot;: &quot;[RUN_LOG_DB_ID]&quot; },
  &quot;properties&quot;: {
    &quot;workflow_name&quot;: { &quot;title&quot;: [{ &quot;text&quot;: { &quot;content&quot;: &quot;[WORKFLOW_NAME]&quot; } }] },
    &quot;run_id&quot;: { &quot;rich_text&quot;: [{ &quot;text&quot;: { &quot;content&quot;: &quot;[RUN_ID]&quot; } }] },
    &quot;client&quot;: { &quot;rich_text&quot;: [{ &quot;text&quot;: { &quot;content&quot;: &quot;[CLIENT_NAME]&quot; } }] },
    &quot;started_at&quot;: { &quot;date&quot;: { &quot;start&quot;: &quot;[STARTED_AT_ISO]&quot; } },
    &quot;duration_ms&quot;: { &quot;number&quot;: [DURATION_MS] },
    &quot;status&quot;: { &quot;select&quot;: { &quot;name&quot;: &quot;[STATUS]&quot; } },
    &quot;error_code&quot;: { &quot;rich_text&quot;: [{ &quot;text&quot;: { &quot;content&quot;: &quot;[ERROR_CODE]&quot; } }] },
    &quot;payload_link&quot;: { &quot;url&quot;: &quot;[PAYLOAD_URL]&quot; },
    &quot;rerun_url&quot;: { &quot;url&quot;: &quot;[RERUN_URL]&quot; },
    &quot;env&quot;: { &quot;select&quot;: { &quot;name&quot;: &quot;[ENV]&quot; } },
    &quot;service&quot;: { &quot;rich_text&quot;: [{ &quot;text&quot;: { &quot;content&quot;: &quot;[SERVICE]&quot; } }] },
    &quot;cost_cents&quot;: { &quot;number&quot;: [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:

{
  &quot;display_information&quot;: { &quot;name&quot;: &quot;[WORKSPACE]-RunAlerts&quot; },
  &quot;features&quot;: {
    &quot;bot_user&quot;: { &quot;display_name&quot;: &quot;Run Alerts&quot;, &quot;always_online&quot;: false }
  },
  &quot;oauth_config&quot;: {
    &quot;scopes&quot;: {
      &quot;bot&quot;: [&quot;chat:write&quot;]
    }
  },
  &quot;settings&quot;: {
    &quot;event_subscriptions&quot;: { &quot;bot_events&quot;: [], &quot;request_url&quot;: &quot;&quot; },
    &quot;interactivity&quot;: { &quot;is_enabled&quot;: false },
    &quot;socket_mode_enabled&quot;: false
  }
}

Install steps:

  1. Create the app from manifest in your workspace.
  2. Install to workspace → copy the Bot User OAuth Token as [SLACK_BOT_TOKEN].
  3. 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-After and 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):

{
  &quot;channel&quot;: &quot;[ALERTS_CHANNEL_ID]&quot;,
  &quot;text&quot;: &quot;[WORKFLOW_NAME] • [STATUS] • [DURATION_MS_READABLE] • [CLIENT_NAME] — run [RUN_ID]&quot;,
  &quot;blocks&quot;: [
    {
      &quot;type&quot;: &quot;section&quot;,
      &quot;text&quot;: {
        &quot;type&quot;: &quot;mrkdwn&quot;,
        &quot;text&quot;: &quot;*[WORKFLOW_NAME]* • *[STATUS]* • [DURATION_MS_READABLE] • [CLIENT_NAME] — run `[RUN_ID]`&quot;
      }
    },
    {
      &quot;type&quot;: &quot;context&quot;,
      &quot;elements&quot;: [
        { &quot;type&quot;: &quot;mrkdwn&quot;, &quot;text&quot;: &quot;env: [ENV] • service: [SERVICE]&quot; },
        { &quot;type&quot;: &quot;mrkdwn&quot;, &quot;text&quot;: &quot;&lt; [RERUN_URL] |rerun &gt; • &lt; [PAYLOAD_URL] |payload &gt;&quot; }
      ]
    }
  ]
}

Response gives ts — save as [PARENT_TS].

Threaded details (via Incoming Webhook to the same channel):

{
  &quot;text&quot;: &quot;[WORKFLOW_NAME] details&quot;,
  &quot;thread_ts&quot;: &quot;[PARENT_TS]&quot;,
  &quot;blocks&quot;: [
    { &quot;type&quot;: &quot;header&quot;, &quot;text&quot;: { &quot;type&quot;: &quot;plain_text&quot;, &quot;text&quot;: &quot;[WORKFLOW_NAME] — details&quot;, &quot;emoji&quot;: true } },
    {
      &quot;type&quot;: &quot;section&quot;,
      &quot;fields&quot;: [
        {&quot;type&quot;: &quot;mrkdwn&quot;, &quot;text&quot;: &quot;*Run ID*\n`[RUN_ID]&quot;},
        {&quot;type&quot;: &quot;mrkdwn&quot;, &quot;text&quot;: &quot;*Client*\n[CLIENT_NAME]&quot;},
        {&quot;type&quot;: &quot;mrkdwn&quot;, &quot;text&quot;: &quot;*Status*\n[STATUS]&quot;},
        {&quot;type&quot;: &quot;mrkdwn&quot;, &quot;text&quot;: &quot;*Duration*\n[DURATION_MS_READABLE]&quot;}
      ]
    },
    {&quot;type&quot;: &quot;section&quot;, &quot;text&quot;: {&quot;type&quot;: &quot;mrkdwn&quot;, &quot;text&quot;: &quot;*Error*\n`[ERROR_CODE]`&quot;}},
    {&quot;type&quot;: &quot;section&quot;, &quot;text&quot;: {&quot;type&quot;: &quot;mrkdwn&quot;, &quot;text&quot;: &quot;*Links*\n• &lt;[RERUN_URL]|Rerun&gt;  •  &lt;[PAYLOAD_URL]|Payload&gt;  •  &lt;[NOTION_PAGE_URL]|Run Log&gt;&quot;}}
  ]
}

Notes:

  • Incoming webhook responses don’t include ts. Use the parent ts from chat.postMessage as thread_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., [&quot;22:00-06:00&quot;] local
  sample_rate_ok: 0.0             # 0.0 = never alert on OK
links:
  rerun_url_prefix: &quot;[RERUN_URL_PREFIX]&quot;
  payload_url_prefix: &quot;[S3_URL_PREFIX]&quot;

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:

  1. HTTP Make a request → Notion (create page)
  2. Tools → Sleep (only on 429 with header seconds)
  3. HTTP Make a request → Slack chat.postMessage (parent)
  4. 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
  • Body: use the JSON from Section 2 with mapped variables.
  • Error handling: If status = 429, route to a Sleep module with {{headers[&quot;Retry-After&quot;] * 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 ts from 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:

  1. Trigger: [YOUR TRIGGER]
  2. Webhooks by Zapier — Custom Request (Notion create page)
  3. Code by Zapier (Python) — safe_post for Slack parent with 429 handling
  4. 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 = &quot;https://slack.com/api/chat.postMessage&quot;
    headers = {&quot;Authorization&quot;: f&quot;Bearer {token}&quot;, &quot;Content-Type&quot;: &quot;application/json&quot;}
    r = requests.post(url, headers=headers, data=json.dumps(payload))
    if r.status_code == 429:
        wait = int(r.headers.get(&quot;Retry-After&quot;, &quot;1&quot;))
        time.sleep(wait)
        r = requests.post(url, headers=headers, data=json.dumps(payload))
    return r.json()

parent = {
  &quot;channel&quot;: &quot;[ALERTS_CHANNEL_ID]&quot;,
  &quot;text&quot;: &quot;[WORKFLOW_NAME] • [STATUS] • [DURATION_MS_READABLE] • [CLIENT_NAME] — run [RUN_ID]&quot;
}
res = safe_post(input_data[&#39;SLACK_BOT_TOKEN&#39;], parent)
return {&quot;parent_ts&quot;: res.get(&quot;ts&quot;)}

Step 4 — Webhook to thread:

  • URL: [SLACK_WEBHOOK_URL]
  • JSON body: Section 4 thread JSON using thread_ts from step 3 ({{steps.code.parent_ts}}).

Section 8 — n8n snippet

Workflow:

  1. HTTP Request — Notion create page
  2. IF response.status = 429 → Wait node for {{$json[&quot;headers&quot;][&quot;Retry-After&quot;] * 1000}} ms → repeat
  3. HTTP Request — Slack chat.postMessage (store ts)
  4. 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?.[&#39;retry-after&#39;] || 1);
  return [{ json: { action: &#39;sleep&#39;, ms: wait * 1000 } }];
}
return items;

Notes:

  • Set Slack requests to sequential execution to respect ~1 msg/sec/channel.
  • Capture ts from Slack response JSON: {{$json[&quot;ts&quot;]}}.

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 &amp;&amp; retries &gt; 0) {
    const wait = Number(res.headers.get(&#39;retry-after&#39;) || 1);
    await new Promise(r =&gt; 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(&#39;Retry-After&#39;, &#39;1&#39;))
        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):

{
  &quot;children&quot;: [
    {
      &quot;object&quot;: &quot;block&quot;,
      &quot;type&quot;: &quot;toggle&quot;,
      &quot;toggle&quot;: {
        &quot;rich_text&quot;: [{&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: {&quot;content&quot;: &quot;Details for [RUN_ID]&quot;}}],
        &quot;children&quot;: [
          {&quot;object&quot;: &quot;block&quot;, &quot;type&quot;: &quot;paragraph&quot;, &quot;paragraph&quot;: {&quot;rich_text&quot;: [{&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: {&quot;content&quot;: &quot;Error: [ERROR_CODE]&quot;}}]}},
          {&quot;object&quot;: &quot;block&quot;, &quot;type&quot;: &quot;bulleted_list_item&quot;, &quot;bulleted_list_item&quot;: {&quot;rich_text&quot;: [{&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: {&quot;content&quot;: &quot;env: [ENV]&quot;}}]}}
        ]
      }
    }
  ]
}

Tip: If you need more than 100 child blocks, make multiple append calls and respect 429s.