Automation

How do you track job postings and buying signals across 30 Slack workspaces?

Abstract illustration of many overlapping circles in teal and orange on a dark background, suggesting several separate workspaces being watched by one system

Run one poller that reads every channel you care about across all your Slack workspaces, classifies each new message by intent, and DMs you a digest of the matches. The working build watches 35 channels in 9 workspaces in three modes: every job posting, help-seeking phrases like 'how do I', and topic keywords. Each pass is a few seconds, fully unattended.

Why is monitoring 30 Slack workspaces by hand impossible?

Slack communities are where work gets announced before it gets posted anywhere else. A membership in thirty workspaces means thirty streams of job postings, help requests, and product questions — and no way to watch them. Open the client, click through nine workspaces, skim a dozen channels, repeat tomorrow. In practice you see a fraction of one workspace and miss everything in the rest.

The account at the center of this build sits in communities, vendor groups, and client teams. Two signals carried real value:

SignalWhy it mattersWhere it appears
Job postingsEvery posting is a potential contractJob and opportunity boards
Help requestsA question you can answer first is a leadCommunity and product help channels
Topic mentionsYour keywords surfacing in general chatterGeneral and announcement channels

The goal: watch the chosen channels across all workspaces, apply the right filter to each, and send one digest — as the account itself — only when something matches. Nothing joins anything. Nothing is visible to anyone.

Why can't Slack's own tools do this?

Slack notifies you inside Slack. The problem is not notification, it is relevance across workspaces — the client has no cross-workspace keyword alerts, and leaving notifications on for a dozen channels means alerting on everything, which is the same as alerting on nothing.

The developer route is a Slack app with user token scopes, and we built exactly that first: an app with read scopes for public channels, private channels, and DMs, plus chat:write for the digest. It failed at the final step for a mundane reason — in workspaces where app installs are admin-gated, a regular member cannot click Allow, and the token only exists after install. Thirty workspaces would mean thirty approval requests to thirty different admins.

The workaround that made the system universal: automate the session the account already has. The Slack web client authenticates its API calls with a session token plus a long-lived cookie — no app, no approval, works identically in every workspace the account is a member of.

Why does the engine have to be a real browser?

The theory of session auth is simple: replay the token against slack.com/api/ endpoints like conversations.history. The practice failed categorically. Every arrangement tried from plain Node fetch — bearer header, form body, full cookie jar, forged browser headers, forced HTTP/2 — returned the same error:

Attempt from a plain HTTP clientResult
Token as Authorization: Bearer + session cookieinvalid_auth
Token in form body, exactly as the web client sends itinvalid_auth
Complete cookie jar replayinvalid_auth
Forged Chrome headers and header orderinginvalid_auth
Forced HTTP/2 transportinvalid_auth

The same token, sent as a page navigation inside a real Chrome, returned {"ok": true, "user": "…"} on the first try. Slack binds its session tokens to a genuine browser fingerprint — TLS, HTTP/2 framing, header sequence, cookie correlation. Copying the headers does not copy the fingerprint.

This is the same lesson behind our extraction of a protected B2B directory, where the rule was to mint the session in a real browser and move the data with a cheap client. Slack removes the second half of that split — there is no cheap client at all — so the browser does both jobs.

How does the system stay invisible on your machine?

Nobody wants automation littering their daily browser with tabs. The system therefore owns a dedicated Chrome instance: its own profile directory, launched headless, driven over the Chrome DevTools protocol. The user's daily browser is never opened.

  ~/.slack-monitor-chrome        (dedicated profile)
        |
        |  one-time visible sign-in: email + code,
        |  click workspaces, close the window
        v
  monitor (Node)  --CDP websocket-->  headless Chrome
        |                               |
        |                    navigate background tab to
        |                    https://<workspace>.slack.com/api/<method>?token=…
        |                               |
        v                               v
  SQLite (cursors, alerts, runs)   JSON rendered as the page body

Every API call is a navigation to a URL that returns JSON, followed by reading the page body. Workspace tokens are discovered from the signed-in profile's localStorage — the same mechanism that lets one command enumerate every workspace the account belongs to. Setup for a new user is two commands: sign in once, capture sessions into a swappable file. After that, the system launches its own invisible Chrome on every run and never shows a window again.

The approach generalizes to any platform that binds sessions to browser fingerprints. The cost is speed — one navigation per API call — which at personal scale is seconds per pass.

How does the poller know what is new?

Polling without state means either re-reading everything or missing messages. The state lives in SQLite with one row per watched channel:

TableHolds
channel_stateLast-seen message timestamp per workspace + channel
alertsMatched messages, deduplicated by UNIQUE(team_id, msg_ts)
runsOne row per pass: started, finished, ok/failed
errorsScoped failures — which workspace, which channel

Two details make it idempotent. Slack's oldest parameter is exclusive, so the stored timestamp never re-processes its own message. And the dedupe constraint means even a re-scan cannot create a duplicate alert — a pass that runs twice is a pass that writes once.

For catch-up, a --since flag sweeps any historical window (2h, 24h, 3d) without disturbing the cursors. Regular polling resumes exactly where it left off.

How is a job channel matched differently from a help channel?

One filter cannot serve both signals. A job board needs everything; a help channel needs questions, not chatter. Matching is configured per channel, in three modes:

"workspaces": {
  "Smartlead": {
    "channels": {
      "C060RNT5NAZ": { "match": "all" },
      "C04S3JW4S21": { "keywordSet": "help" },
      "C0492PCUX7D": {}
    }
  }
}
ModeBehaviorSignal
match: "all"Alert on every messageJob-posting channels
keywordSet: "help"Match against a named intent listHelp / buying-signal channels
defaultGlobal + workspace keywordsGeneral channels

The help set matches the phrases people actually type when they have a problem: how do i, anyone know, can anyone, recommend, stuck, looking for. Keywords are word-boundary and case-insensitive; anything wrapped in slashes is regex. Per-workspace and per-channel lists layer on top, so one community can watch for a specific product name without polluting the global rules.

Adding a workspace or channel is plain-language friendly: one command lists every visible channel with its ID; one config edit turns it on.

What does observability look like?

A monitor that fails silently is worse than no monitor. Every pass writes a run row; every failure writes an error row with its scope. A report command answers "did it work this morning" in one line — runs, alerts, delivery status, latest errors.

Fault isolation is structural: one workspace failing mid-pass logs the error and moves on. A dead session in workspace four does not stop workspaces five through nine. Rate limits get honest backoff, and the system fails fast with instructions if Chrome is missing.

What did the finished system look like?

Verified facts from the running build, as of 24 August 2026:

FactValue
Workspaces monitored9
Conversations visible to the sessionsabout 80
Channels configured35 across three matching modes
Poll interval5 minutes, one pass per workspace
Sign-in windows after setupzero — headless from then on
Digest deliveryself-DM per workspace, as the account
StateSQLite, WAL mode, four tables
First 24-hour sweep9 messages read, 0 alerts

The quiet-day result deserves its row. Nine messages arrived, none matched the configured signals, and no notification was sent. Filtering is the product — the value of the system is measured in the digests you did not get.

What are the limits?

Stated plainly, so expectations are calibrated:

The same discipline runs through our other free automation tools and the rest of this build log: the goal is never more force against a platform — it is a smaller footprint that gets you the signal you actually went looking for.

Frequently asked questions

Phrases people type when they have a problem worth solving: 'how do I', 'anyone know', 'can anyone recommend', 'stuck', 'looking for'. These are tracked as a reusable keyword set applied per channel, so a vendor community can also watch for its own product name without changing global rules.

The poll interval is configurable and set to five minutes in this build, so worst-case delay is five minutes from post to digest. An ad-hoc flag can sweep any historical window — two hours, a day, three days — without disturbing the ongoing cursors.

No bot joins any channel and no app appears anywhere. Reads never mark channels as read. The single write is the digest, posted to the monitoring account's own self-DM, visible only to that account.

Two commands. The first opens a sign-in window for the system's own browser profile once; you enter email plus code and pick your workspaces. The second captures every signed-in workspace into a sessions file. After that the system launches its own invisible Chrome whenever it runs.

Thread replies, edited messages, and deletions — it tracks top-level messages and thread broadcasts. A five-minute poll means it is built for opportunities, not incidents. And it only ever sees what the signed-in account already sees.

← All articles Book A Free Call