Turn Slack conversations into an audit ledger.
slackledger is a self-hosted Slack bot that reads the channels it is invited to and records the
things that matter for an audit: decisions, commitments, approvals, action items, handoffs and
escalations, each tied to a real Slack user, a timestamp, a channel, and a permalink to the
message it came from. Names mentioned in text ("ask sam", "Sam K said", @sam) are resolved to
canonical Slack user IDs with a confidence score and a recorded resolution method, so the ledger
answers who without guessing.
{"id": 41, "kind": "commitment", "summary": "sam.kim committed to ship the migration by Friday",
"actor_id": "U01ABC", "channel": "C0DEF", "source_ts": ["1726790400.000100"],
"permalinks": ["https://acme.slack.com/archives/C0DEF/p1726790400000100"],
"occurred_at": "2026-09-20T00:00:00+00:00", "confidence": 0.9, "model": "claude-opus-5",
"extracted_at": "2026-09-20T00:02:11+00:00",
"subjects": [{"mention": "priya", "user_id": "U02XYZ", "confidence": 0.95, "method": "unique_name"}]}- Live via Slack Socket Mode (no public URL) and backfill of channel history.
- SQLite by default, Postgres for real deployments. One env var switches.
- LLM extraction with Claude, structured output, only records what is explicitly said and keeps the verbatim quote and source message(s).
- Person disambiguation ladder:
<@U…>mention → exact handle/name → unique first name among thread participants → channel members → workspace → LLM only when still ambiguous → else recorded as unresolved, never silently guessed. - Idempotent: re-running a backfill never duplicates events. Edits update, deletions are flagged (never purged) so the ledger stays a ledger.
- Export as JSONL or CSV, filter by time, channel, kind, or person.
pip install slackledger # or: pip install "slackledger[postgres]"
slackledger init # pick a template, save tokens, verify Slack (about 2 minutes)
slackledger run # live, over Socket Mode
slackledger backfill "#channel" --days 7
slackledger export --format table # or jsonl / csvslackledger init asks four questions: what to record (a template), your Slack tokens, how to run
the model (API key or your Claude Code login), and where to store the ledger. It writes .env
(mode 600) and slackledger.toml, connects to Slack, and lists the channels the bot can see.
Pick the one that matches the job. Each is a starting taxonomy; edit slackledger.toml afterwards
to rename kinds, add your own, or change sensitivity. The extraction prompt follows the file.
| Template | For | Kinds | Sensitivity |
|---|---|---|---|
engineering |
Product and engineering teams: a decision and commitment log without ceremony | decision, commitment, approval, action_item, handoff, escalation | precise |
compliance |
Regulated teams that need a complete, evidence-backed record | the core six + attestation, exception, access_change | thorough |
management |
Managers tracking who promised what, by when | commitment, action_item, decision, handoff, escalation, completion, slip | balanced |
incident |
On-call teams reconstructing an incident channel | detection, hypothesis, mitigation, decision, escalation, handoff, resolution | thorough |
slackledger templates prints the same list. A kind the model invents that isn't in your file is
recorded as other, never as a new kind.
- Go to https://api.slack.com/apps → Create New App → From a manifest → paste
manifest.yaml. - Install to Workspace. Copy the Bot User OAuth Token (
xoxb-…) toSLACK_BOT_TOKEN. - Basic Information → App-Level Tokens → generate one with
connections:write. Copy thexapp-…token toSLACK_APP_TOKEN. - Invite the bot:
/invite @slackledgerin each channel you want audited.
Scopes requested and why are commented in manifest.yaml. Direct messages are not read.
Set ANTHROPIC_API_KEY (or log in with ant auth login). The default model is claude-opus-5;
override with AUDIT_MODEL. Server-side refusal fallbacks are enabled.
No API key? If you have Claude Code installed and logged in,
set AUDIT_LLM_BACKEND=claude-cli. Extraction then runs through claude -p --json-schema under
your Claude Code login. It is slower and intended for trying things out, not for production volume.
| Command | What it does |
|---|---|
slackledger init [--template X] [--yes] |
Guided setup: template, tokens, model backend, storage. |
slackledger templates |
List the built-in use-case templates. |
slackledger run |
Start the Socket Mode bot. Runs migrations first. |
slackledger backfill "#channel" --days 30 |
Read history + threads, extract, store. Safe to re-run. |
slackledger export [--since] [--until] [--channel] [--kind] [--user] [--format jsonl|csv|table] |
Print the ledger to stdout. |
slackledger users sync |
Refresh the user directory. |
slackledger db upgrade |
Apply schema migrations. |
Everything is an environment variable (a .env file is read too).
| Variable | Default | Meaning |
|---|---|---|
SLACK_BOT_TOKEN |
xoxb-… |
|
SLACK_APP_TOKEN |
xapp-… (Socket Mode) |
|
ANTHROPIC_API_KEY |
Anthropic key | |
AUDIT_DATABASE_URL |
sqlite:///./slackledger.db |
Any SQLAlchemy URL. Postgres: postgresql+psycopg://user:pw@host/db |
AUDIT_MODEL |
claude-opus-5 |
Extraction / disambiguation model |
AUDIT_LLM_BACKEND |
anthropic |
anthropic (SDK, needs API key) or claude-cli (claude -p, needs Claude Code login) |
AUDIT_WINDOW_MSGS |
10 |
Extract a thread after this many new messages… |
AUDIT_WINDOW_SECS |
120 |
…or after this many seconds of quiet |
AUDIT_CHANNELS |
(all) | JSON list of channel IDs to audit |
AUDIT_LOG_JSON |
0 |
Emit JSON log lines |
AUDIT_CONFIG |
slackledger.toml |
Taxonomy, sensitivity, notes and channel list (written by init) |
cp .env.example .env && $EDITOR .env
docker compose up -d # bot + Postgres 16
docker compose exec bot slackledger export --format csv > ledger.csvThe image alone (SQLite in a volume): docker run --env-file .env -v ledger:/data ghcr.io/…/slackledger.
Slack events ─▶ bot.py ─▶ pipeline.ingest ─▶ messages table
│ (debounce per thread: N msgs or T secs quiet)
▼
extract.py (Claude, structured output)
▼
people.py (resolve mentions → user IDs)
▼
repo.add_events (ON CONFLICT DO NOTHING on dedup_key)
domain.py– plain models shared by every layer.db.py/migrations/– SQLAlchemy schema, Alembic migrations, portable across SQLite and Postgres.repo.py– the only code that talks to the database.slack_gateway.py– the only code that talks to Slack's Web API.extract.py– prompts, schemas, and the Anthropic client.people.py– pure resolution ladder; unit-tested without any network.pipeline.py– orchestration, used identically by the live bot andbackfill.
users, messages (ground truth; edits update the row, deletes set deleted=true), reactions,
audit_events (dedup_key is unique: channel + source messages + kind + normalised summary),
event_subjects (one row per person referenced by an event, with method and confidence),
cursors.
decision · commitment · approval · action_item · handoff · escalation · other
| method | confidence | meaning |
|---|---|---|
mention |
1.0 | <@U…> in the text |
exact |
1.0 | Handle, display name or real name matched exactly |
unique_name |
0.95 / 0.9 / 0.7 | First name (or first + last initial) unique among thread participants / channel members / workspace |
llm |
model's | Several candidates; the model chose using the sentence and candidate profiles |
ambiguous |
0 | Several candidates; the model declined |
unresolved |
0 | No candidate at all |
- Only channels the bot is explicitly invited to. No DMs, no group DMs.
- Message text is stored (it is the evidence). Deleted messages are flagged, not removed; delete rows manually if a retention policy requires it.
users:read.emailis optional; remove the scope from the manifest if you do not want emails stored.- Transcripts are sent to Anthropic for extraction. See their data policies for retention.
uv venv && uv pip install -e ".[dev]"
ruff check src tests && mypy src && pytest
# also against Postgres:
docker run -d --name pg -e POSTGRES_PASSWORD=pw -p 55432:5432 postgres:16-alpine
POSTGRES_URL=postgresql+psycopg://postgres:pw@localhost:55432/postgres pytestSchema changes: edit db.py, then slackledger db revision -m "describe change" and review the
generated file in src/slackledger/migrations/versions/.
- Read-only HTTP API / dashboard over the ledger
- Natural-language queries ("what did Sam commit to this week?")
- Mapping Slack users to external identities (HR, GitHub)
MIT
