Dill is a Slack bot for managing team rotations and scheduled task queues. It provides a simple, interactive interface within Slack to create, manage, and automate fair user distribution across any channel – no engineering knowledge required.
Whether you're running on-call rotations, meeting facilitators, or any recurring team responsibility, Dill keeps things fair and hands-off.
GenAI project – Dill was built with generative AI tooling and has been validated and in active use by a team for over a year.
- Create, edit and delete rotations – manage multiple independent rotations within any Slack channel
- Fair distribution – the queue is ordered by date of last accepted pick, with the least recently active member always at the front
- Flexible scheduling – weekly, fortnightly or monthly, on any day(s) of the week, at any 30-minute interval, in any timezone (UTC-12 to UTC+14)
- Accept or skip – each pick posts a message with Accept/Skip buttons; skipped members move to the back of the queue until the next day
- In-place message updates – accepted picks update the original message rather than posting a new one, keeping channels tidy
- Silent skips – when someone skips, the next person is picked silently; a record of skips is logged in the thread
- Mark members as on leave – remove a member from upcoming picks without removing them from the rotation
- Automatic reinstatement – members return to the queue automatically when leave ends
- Rotation settings – manually set a member's last accepted date to reorder the queue (useful for bootstrapping a new rotation fairly)
- Daily skip reset – skip status is automatically cleared each day at 00:01 UTC
- Fortnightly and monthly – supports every-2-week and every-4-week frequencies, not just weekly
- Slack-channel backup – back up all rotation data to a private Slack channel; the bot restores from this backup on startup
- Local JSON fallback – data is also written to local JSON files for fast reads and startup
- Central management modal –
/dillopens a modal listing all rotations with upcoming pick previews - Live updates – saving a rotation immediately refreshes the modal
- Easy navigation – Save only appears when there is something to save; otherwise modals show Close
| Command | Description |
|---|---|
/dill |
Opens the main management modal |
/dill help |
Shows all available commands (private, ephemeral) |
/dill status |
Shows bot uptime, memory and active job counts |
/dill pick [name] |
Manually triggers a pick for a rotation |
/dill reset [name] |
Randomises a rotation's queue |
/dill restore-backup |
Restores data from the most recent Slack backup |
/dill delete-backup |
Deletes all backup messages from the backup channel |
/dill kill-kill-kill confirm |
Wipes all data and backups – irreversible |
- Node.js v20.14.0 or higher (or Docker)
- A Slack workspace where you can install apps
- A way to host the bot 24/7 (see Hosting)
Go to api.slack.com/apps and create a new app from scratch.
Enable Socket Mode under Settings → Socket Mode. Generate an App-Level Token with the connections:write scope – this becomes your SLACK_APP_TOKEN.
Add a slash command under Features → Slash Commands:
- Command:
/dill - Description: Manage team rotations
- Usage hint:
[help | pick | reset | status | ...]
Add OAuth scopes under Features → OAuth & Permissions → Bot Token Scopes:
| Scope | Purpose |
|---|---|
chat:write |
Post, update and delete Dill's own messages (including old backups) |
commands |
Register the /dill slash command |
users:read |
Resolve member display names |
channels:read |
Read public channel info |
groups:read |
Read private channel info |
channels:history |
Read backup channel history |
groups:history |
Read backup channel history (private channels) |
chat:write.public |
Post in public channels Dill hasn't joined |
groups:write |
Manage private channels Dill is in |
channels:join |
Join public channels when a pick is triggered |
reactions:read |
Read ❌ reactions on speaking-order lists |
im:history |
Read survey answers people send Dill by DM |
reactions:write |
React to thanks / "bad bot" DMs to Dill |
chat:write.customize |
Post survey answers with the person's name and avatar |
Subscribe to bot events under Features → Event Subscriptions: reaction_added and reaction_removed (used by speaking-order lists), message.im (survey answers) and message.channels / message.groups (summoning people when someone replies to their survey post).
Allow DMs under Features → App Home: tick Allow users to send Slash commands and messages from the messages tab, or nobody can reply to a survey.
Alternatively, paste slack-app-manifest.json into App Manifest to configure all of the above in one go.
Install the app to your workspace under Settings → Install App. Copy the Bot Token (xoxb-…) – this becomes your SLACK_BOT_TOKEN.
Copy the Signing Secret from Settings → Basic Information – this becomes your SLACK_SIGNING_SECRET.
git clone https://github.com/cookie-monster1649/dill.git
cd dill
npm installCopy the example file and fill in your values:
cp .env.example .env# Required
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_APP_TOKEN=xapp-your-app-token
SLACK_SIGNING_SECRET=your-signing-secret
# Optional – enables Slack-channel backup (recommended)
DILL_STORAGE_CHANNEL_ID=C1234567890
# Optional – AES-256 encryption key for backups (64 hex chars)
DILL_BACKUP_ENCRYPTION_KEY=
# Optional
NODE_ENV=production
LOG_LEVEL=INFO
PORT=3000See Persistent Storage for details on DILL_STORAGE_CHANNEL_ID.
npm startYou should see ⚡️ Dill Bot is running! in the console.
Add the bot to a channel:
- Public channels: run
/dill– the bot joins automatically - Private channels: run
/invite @dillbotfirst, then/dill
Dill requires a persistent process that stays online 24/7. It uses Slack's Socket Mode, so no public inbound URL is needed.
A pre-built image is published to the GitHub Container Registry on every release.
Run with Docker:
docker run -d \
--name dill-bot \
--restart unless-stopped \
-e SLACK_BOT_TOKEN=xoxb-... \
-e SLACK_APP_TOKEN=xapp-... \
-e SLACK_SIGNING_SECRET=... \
-e DILL_STORAGE_CHANNEL_ID=C... \
-e NODE_ENV=production \
ghcr.io/cookie-monster1649/dill:latestRun with Docker Compose:
services:
dill-bot:
image: ghcr.io/cookie-monster1649/dill:latest
restart: unless-stopped
environment:
SLACK_BOT_TOKEN: xoxb-...
SLACK_APP_TOKEN: xapp-...
SLACK_SIGNING_SECRET: ...
DILL_STORAGE_CHANNEL_ID: C...
NODE_ENV: productionBuild from source:
docker build -t dill-bot .
docker run -d --name dill-bot --restart unless-stopped --env-file .env dill-botNote on data persistence with Docker: By default, local JSON files are written inside the container and lost on restart. Use
DILL_STORAGE_CHANNEL_ID(Slack-channel backup) for persistence without volume mounts – the bot restores from Slack on every startup.
- Fork this repo to your own GitHub account
- Create a new project at railway.app and connect your fork
- Add environment variables in the Railway dashboard
- Railway deploys automatically on every push to
main
Railway's free tier has sleep behaviour – upgrade to a paid plan for always-on operation.
fly launch # creates fly.toml
fly secrets set \
SLACK_BOT_TOKEN=xoxb-... \
SLACK_APP_TOKEN=xapp-... \
SLACK_SIGNING_SECRET=... \
DILL_STORAGE_CHANNEL_ID=C...
fly deployFly's smallest machine (shared-cpu-1x, 256MB RAM) is sufficient. Set min_machines_running = 1 in fly.toml to prevent sleeping.
heroku create your-dill-bot
heroku config:set \
SLACK_BOT_TOKEN=xoxb-... \
SLACK_APP_TOKEN=xapp-... \
SLACK_SIGNING_SECRET=... \
DILL_STORAGE_CHANNEL_ID=C... \
NODE_ENV=production
git push heroku main
heroku ps:scale web=1Heroku's Eco dynos sleep after 30 minutes of inactivity. Use a Basic dyno or higher for always-on operation.
Dill can back up all rotation data to a private Slack channel. On startup the bot restores from the most recent backup, so data survives restarts and redeployments.
Setup:
- Create a private Slack channel (e.g.
#dill-storage) and invite your bot to it - Get the channel ID – right-click the channel name → Copy link, then extract the
C…segment from the URL - Add it to your environment:
DILL_STORAGE_CHANNEL_ID=C1234567890
How it works:
- One backup message is kept in the channel at all times (previous messages are deleted on each write)
- Backups are triggered automatically after any data change
- Large backups are split into multiple messages and automatically reassembled on restore
- Optionally set
DILL_BACKUP_ENCRYPTION_KEY(64 hex characters) to encrypt backups with AES-256 before posting
For each rotation, a ⚙️ settings button lets you manually set each member's last accepted date (YYYY-MM-DD). This controls queue order and is useful for:
- Bootstrapping a fair starting order for a new rotation
- Correcting order after members are added or removed
- Re-balancing after extended absences
Members with no date set appear at the front of the queue (they go first).
Every rotation has an optional Pick message notes field. It's a rich text editor, so you can add links (toolbar or ⌘K / Ctrl+K), bold, lists and so on. Whatever you write there (e.g. what to prepare, a Trello board link) is shown in each pick message, and Preview pick message shows how it will look before you save.
Tick Post a speaking order list on any rotation to post a shuffled, numbered list of members after each pick. For example, a fortnightly retro picks a facilitator on Tue and posts the speaking order on Wed.
- List day – the list posts at the rotation's pick time on this day (default Wed), but only in a week with a pick, so it follows the rotation's frequency
- Leave – members on leave that day are left off
- Opting out – anyone who reacts ❌ to the list is removed and the numbers close up. Removing the reaction puts them back in their original spot
- Post Speaking Order Now – posts the list immediately, handy for testing
Async standups in the style of Geekbot. Open /dill in the channel where answers should appear and choose Surveys → New Survey.
- Who and when – pick the people, days and a time. Each person gets a DM at that time in their own Slack timezone
- Answering – Dill asks one question at a time in the DM, and each reply answers the current question
- Posting – when someone finishes, their answers post to the channel, either as replies in one thread per day (the parent shows who's still to answer) or as one message per person. Posts show the person's name and avatar
- Replies – in one-message-per-person mode, the first reply from someone else under a person's post makes Dill reply "Summoning @person :wand:", since Slack doesn't notify them about replies to a post Dill made for them
- Say something else – the "Thanks!" DM also has a Say something else button that asks "Anything else?" and adds the reply to the end of the post. Adding again appends; Edit answers can change or clear it
- Non-answers – answers like "no", "nope", "n/a", "skip" or "no blockers" are left out of the channel post (the person's DM summary still shows them, marked as not posted). Longer answers such as "No, but the deploy is stuck" are kept
- Banter – outside a survey, short DMs to Dill get a response: "thanks" or "good bot" gets ❤️, "bad bot" gets 😢, and anything really rude gets "I'm going to report this to HR. BRB"
- Editing – edit any of your DM replies in Slack and the change is used, whether you're still answering or already posted. The "Thanks!" DM also has an Edit answers button. Either way the channel post updates in place
- Reminders – optionally, anyone who hasn't finished by the reminder time (again in their own timezone) gets one follow-up DM that repeats the question they're on
- Leave – people on leave in that channel aren't sent the survey
- Catch-up – if Dill was offline at send time, it still sends the survey up to 2 hours late, but no later
- Overlaps – if two surveys are due at once, the second waits until the first is finished. A new day's survey replaces yesterday's unfinished one
src/
├── index.js # Entry point – starts the app
├── app.js # Orchestrator – wires up Slack handlers and services
├── bot/ # Slack lifecycle and modal building
├── commands/ # Slash command handlers
├── handlers/ # Slack action (button/modal) handlers
├── services/ # Business logic – scheduling, analytics, storage
├── stores/ # Data layer – JSON file persistence
└── utils/ # Helpers – dates, rotations, Slack API wrappers
config.js # All tunable constants
Data is stored in JSON files (configs.json, rotations.json, activestate.json, analytics.json, leave.json) in the project root. These are created automatically on first run and excluded from version control.
Pull requests are welcome. For significant changes, open an issue first to discuss what you'd like to change.
npm run dev # Start with auto-restart on file changes
npm test # Run the test suiteLog verbosity can be increased with LOG_LEVEL=DEBUG in your .env.