Learning Goals
3 minBy the end of this lesson you can:
- Create a Slack incoming webhook and understand what it is.
- Post a message by sending JSON to the webhook URL.
- Format richer messages with Block Kit and colour attachments.
- Keep the webhook URL (a secret!) in the environment.
Warm-Up · What's a Webhook?
5 minA webhook is just a URL that does something when you POST to it. Slack gives you an "incoming webhook" URL tied to one channel; POST a small JSON payload to it and the message appears in that channel. No OAuth dance, no SDK required.
https://hooks.slack.com/services/T00000/B00000/XXXXXXXXXXXX
└─ team ─┘└─ bot ─┘└── secret ──┘Sending a notification = requests.post(webhook_url, json={"text": "..."}). That's the whole API. But note: that URL is a secret — anyone who has it can post to your channel — so it lives in the environment, never in code or git. The same pattern works for Discord, Teams, and most chat tools.
New Concept · Posting to Slack
14 minSetting up the webhook
In Slack: create an app → enable "Incoming Webhooks" → "Add New Webhook to Workspace" → pick a channel. You get a URL. Put it in .env as SLACK_WEBHOOK (git-ignored, Lesson 8).
The simplest message
import os, requests from dotenv import load_dotenv load_dotenv() WEBHOOK = os.environ["SLACK_WEBHOOK"] resp = requests.post(WEBHOOK, json={"text": "Deploy finished ✅"}, timeout=10) resp.raise_for_status() # Slack returns 200 + body "ok"
POST a JSON object with a "text" key. That's it — the message appears in the channel. Slack supports basic markdown (*bold*, `code`, <url|link>) in the text.
A reusable notifier
import os, requests, logging log = logging.getLogger("slack") def notify(text: str) -> bool: url = os.getenv("SLACK_WEBHOOK") if not url: log.warning("SLACK_WEBHOOK not set — skipping notification") return False try: r = requests.post(url, json={"text": text}, timeout=10) r.raise_for_status() return True except requests.RequestException as e: log.error("slack notify failed: %s", e) return False
Note the graceful degradation: if the webhook isn't configured, it logs and returns False rather than crashing — your automation shouldn't die just because notifications are off.
Richer messages with Block Kit
def notify_blocks(title: str, body: str) -> bool: payload = { "blocks": [ {"type": "header", "text": {"type": "plain_text", "text": title}}, {"type": "section", "text": {"type": "mrkdwn", "text": body}}, {"type": "context", "elements": [{"type": "mrkdwn", "text": f"sent by automation • <!date^...>"}]}, ] } return requests.post(os.environ["SLACK_WEBHOOK"], json=payload, timeout=10).ok
Block Kit builds structured messages — headers, sections, dividers, buttons. You assemble a list of block dicts. Slack's online Block Kit Builder lets you design visually and copy the JSON.
Severity colours with attachments
COLOURS = {"info": "#2563EB", "good": "#16A34A", "warning": "#F59E0B", "error": "#DC2626"} def notify_status(text: str, level: str = "info") -> bool: payload = {"attachments": [ {"color": COLOURS.get(level, "#999999"), "text": text} ]} return requests.post(os.environ["SLACK_WEBHOOK"], json=payload, timeout=10).ok
The attachments field with a color draws a coloured bar beside the message — green for success, red for failure, etc. An instant visual signal in a busy channel.
Worked Example · A Job-Status Notifier
12 minGoal: wrap any task so that when it runs, Slack gets a coloured start/success/failure message with timing and the error if it fails — the pattern behind "the build is green/red" bots.
import os, time, logging, functools, traceback import requests from dotenv import load_dotenv load_dotenv() logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") log = logging.getLogger("slack") COLOURS = {"start": "#6B7280", "success": "#16A34A", "error": "#DC2626"} def post(text: str, level: str) -> None: url = os.getenv("SLACK_WEBHOOK") if not url: log.warning("no SLACK_WEBHOOK — would have posted: %s", text) return try: requests.post(url, json={"attachments": [ {"color": COLOURS[level], "text": text}]}, timeout=10) except requests.RequestException as e: log.error("slack post failed: %s", e) def notify_run(job_name: str): """Decorator: announce a function's start, success, or failure on Slack.""" def decorator(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): post(f"▶️ *{job_name}* started", "start") t0 = time.time() try: result = fn(*args, **kwargs) dt = time.time() - t0 post(f"✅ *{job_name}* finished in {dt:.1f}s", "success") return result except Exception as e: post(f"❌ *{job_name}* FAILED: {e}", "error") log.error("job failed:\n%s", traceback.format_exc()) raise return wrapper return decorator @notify_run("Nightly backup") def backup(): time.sleep(1) # pretend to do work # raise RuntimeError("disk full") # uncomment to see the failure path return "ok" backup()
# In Slack (with coloured bars): ▶️ Nightly backup started (grey) ✅ Nightly backup finished in 1.0s (green) # if it raised: ❌ Nightly backup FAILED: disk full (red)
Read the code
A decorator (from Level-5 territory) cleanly wraps any job with notifications — start (grey), success (green) with timing, or failure (red) with the error message, while still re-raising so the caller sees the exception. The webhook is read from the environment and a missing one degrades gracefully. Slap @notify_run("…") on any scheduled task and your team gets live status in a channel — no dashboards required. This is the Slack channel for Lesson 34's multi-channel alerts.
Try It Yourself
13 minMake a free Slack workspace (or use a test channel) and create an incoming webhook for it. Keep the URL in .env.
Post a plain message to your channel. Confirm it appears, then add some markdown (*bold*, a <url|link>).
Write status(text, level) using the attachments+colour approach and post one of each level (info/good/warning/error). Check the coloured bars render.
Hint
for lvl in ("info", "good", "warning", "error"): notify_status(f"this is a {lvl} message", lvl)
Post a Block Kit message with a header and a section containing a small bullet summary (e.g. today's order count and total). Design it in Slack's Block Kit Builder, then send the JSON from Python.
Hint
payload = {"blocks": [ {"type": "header", "text": {"type": "plain_text", "text": "Daily Summary"}}, {"type": "section", "text": {"type": "mrkdwn", "text": "*Orders:* 412\n*Total:* 84,210.50"}}, ]} requests.post(os.environ["SLACK_WEBHOOK"], json=payload, timeout=10)
Mini-Challenge · The Threshold Watcher
8 minWrite a watcher that checks some metric (free disk %, a count from a CSV, an API number) and posts to Slack only when it crosses a threshold — and not again until it recovers, to avoid spamming the channel. This "alert once" logic is essential for real notifications.
Show a sample solution
import json, shutil from pathlib import Path STATE = Path("alert_state.json") def watch_disk(threshold_pct: float = 90.0) -> None: used = shutil.disk_usage("/") pct = used.used / used.total * 100 alerted = json.loads(STATE.read_text())["alerted"] if STATE.exists() else False if pct >= threshold_pct and not alerted: notify_status(f"⚠️ Disk at {pct:.0f}% (threshold {threshold_pct:.0f}%)", "error") STATE.write_text(json.dumps({"alerted": True})) elif pct < threshold_pct and alerted: notify_status(f"✅ Disk recovered: now {pct:.0f}%", "good") STATE.write_text(json.dumps({"alerted": False})) watch_disk(90)
Non-negotiables: alerts on crossing up, recovery message on crossing back, no repeat spam while over threshold.
Recap
3 minA Slack incoming webhook turns notifications into a one-line requests.post(url, json={"text": ...}). The webhook URL is a secret — keep it in the environment, never in code. Build a reusable notify() that degrades gracefully when the webhook is unset. For richer messages, use coloured attachments for severity or blocks (Block Kit) for structured layouts. Wrap jobs with a decorator to broadcast start/success/failure, and use "alert once until recovered" state to avoid channel spam. Same pattern powers Discord, Teams, and more — next lesson, Discord.
Vocabulary Card
- webhook
- A URL that performs an action when you POST data to it.
- incoming webhook
- A Slack URL bound to a channel; posting JSON appears as a message.
- Block Kit
- Slack's system of JSON blocks for structured, formatted messages.
- alert debouncing
- Notifying only on state change, not repeatedly while a condition holds.
Homework
4 minBuild slack.py as a reusable module with notify(), notify_status(text, level), and the @notify_run(name) decorator. Then apply the decorator to one earlier automation in this level (e.g. the report generator or a backup function) so it announces its outcome to a test channel. Confirm both the success and failure messages render with the right colours.
Sample · applying the decorator
from slack import notify_run @notify_run("Daily report") def generate_report(csv_path): rows = load(csv_path) stats = analyse(rows) out = make_folder() write_excel(rows, stats, out) write_cover(stats, csv_path, out) return out # Slack channel shows: # ▶️ Daily report started (grey) # ✅ Daily report finished in 0.8s (green) # and on error: # ❌ Daily report FAILED: <msg> (red)
Non-negotiables: reusable module, webhook from env, decorator applied to a real job, success+failure both verified.