Three ways to talk to Dobby:
# 1. @-mention in any channel the bot is in
@dobby book lon-par tomorrow
@dobby book lon par next monday return friday 2a 1c premier
@dobby book par-bxl tomorrow unallocated
# 2. DM the bot directly (no @dobby prefix needed)
book lon-par tomorrow
book wheelchair to amster next saturday
# 3. Slash command - works EVERYWHERE without inviting the bot to a channel
/dobby book lon-par tomorrow
/dobby book lon par next monday return friday 2a 1c premier
/dobby wheelchair to amsterdam next saturday
/dobby (slash command) and @dobby (mention) share the same parser, the same booking pipeline, and the same SQLite jobs table. The only difference is how the response is delivered:
@dobby / DM → public reply in the channel/DM (chat.postMessage, then in-place updates)./dobby → an ephemeral ack visible only to the invoking user, with progress updates posted to Slack's response_url (also ephemeral, and they replace the previous message rather than appending).Both paths return a 200 to Slack within 3 s and run the booking in the background. The LLM fallback is on for both — when the regex doesn't match, Dobby calls the configured provider, validates the result against BookingRequestSchema, and echoes its interpretation ("Dobby thinks you mean: London → Amsterdam, 1 wheelchair adult + 1 companion") so users can spot misreads before the booking completes.
src/server/slack/parser.ts) — cheap, fast, handles the canonical grammar below.src/llm/parse-prompt.ts) — runs only when the regex parser fails. Uses the configured provider (Anthropic by default; OpenAI optional). The result is validated against the same BookingRequestSchema either way.book <od> <date> [return <date>] [<pax>...] [<class>] [snap|unallocated] [api] [market:<code>]
| Token | Examples |
|---|---|
<od> |
lon-par, lon par, london paris (anything findStation resolves) |
<date> |
tomorrow, next monday, 2026-05-12 (chrono-node) |
<pax> |
1a, 2c, 1s, 1y, 1i |
<class> |
standard, plus, premier |
Defaults: 1 adult, uk-en market.
| Env var | Required for |
|---|---|
SLACK_SIGNING_SECRET |
Request signature verification |
SLACK_BOT_TOKEN |
Posting & updating messages |
ANTHROPIC_API_KEY |
LLM fallback (default provider) |
OPENAI_API_KEY |
LLM fallback (alt. when DOBBY_LLM_PROVIDER=openai) |
DOBBY_LLM_ENABLED |
false to disable the LLM fallback (regex-only mode) |
DOBBY_LLM_DAILY_BUDGET_USD |
Daily cost cap (default 5) |
DOBBY_DB_PATH |
Optional sqlite override (default ~/.booking-bot/dobby.sqlite) |
Create a Slack app, then in App Manifest use the Events API plus these scopes:
{
"display_information": {
"name": "Dobby",
"description": "DotCom's staging-only booking bot",
"background_color": "#00286a",
"long_description": "Use dobby to make bookings in our staging environment, just tell Dobby what you need and he'll create the booking for you (surname is always 'test'). for advanced uses, use cli parameters to select specific classes of accommodation, service numbers, etc"
},
"features": {
"bot_user": {
"display_name": "Dobby",
"always_online": true
},
"app_home": {
"home_tab_enabled": false,
"messages_tab_enabled": true,
"messages_tab_read_only_enabled": false
},
"slash_commands": [
{
"command": "/dobby",
"url": "https://dobby.diegoalto.app/slack/command",
"description": "Book a staging Eurostar journey",
"usage_hint": "book lon-par tomorrow",
"should_escape": false
}
]
},
"oauth_config": {
"scopes": {
"bot": [
"app_mentions:read",
"channels:history",
"groups:history",
"im:history",
"mpim:history",
"chat:write",
"commands",
"reactions:write"
]
},
"pkce_enabled": false
},
"settings": {
"event_subscriptions": {
"request_url": "https://dobby.diegoalto.app/slack/events",
"bot_events": [
"app_mention",
"message.im"
]
},
"org_deploy_enabled": false,
"socket_mode_enabled": false,
"token_rotation_enabled": false,
"is_mcp_enabled": false
}
}
Why these scopes:
channels:history / groups:history / mpim:history — required for app_mention events to fire in public channels, private channels, and group DMs. Without them Slack delivers the event metadata but not the message text.im:history — required for message.im events (DMs). Pairs with the message.im event subscription so the user can DM Dobby without needing @dobby first.chat:write — post replies and update the booking-progress message (mention/DM path).commands — register and handle the /dobby slash command (slash-command path).reactions:write — Dobby reacts to acknowledge receipt (👀 → ✅ / ❌) on mention/DM acks.Why these manifest blocks:
app_home.messages_tab_read_only_enabled: false — Slack defaults this to true, which surfaces "Sending messages to this app has been turned off" in Dobby's DM. Setting it to false unlocks the DM composer.slash_commands — registers /dobby as a globally-available command, so users don't need to invite the bot to a channel before using it. Slack POSTs to request_url (/slack/command) with application/x-www-form-urlencoded; the same HMAC verifySlackRequest validates both event and command requests because the algorithm hashes the raw body, regardless of content type.oauth_config.scopes.user. User scopes mean Slack issues a user token and the app can act as the installing user — not what we want for a bot. Bot scopes only.app_mentions:read is a strict subset of channels:history for our purposes and Slack now flags it as a legacy scope; the *:history set replaces it cleanly.
Why we don't subscribe to
message.channels/message.groups/message.mpim: those would double-fire alongsideapp_mentionfor the same@dobby ...message. The controller'sisActionablepredicate (src/server/slack/controller.ts) reflects that — it acceptsapp_mentionfrom anywhere, plusmessageevents only whenchannel_type === "im".
Slack requires a publicly reachable HTTPS URL for both the events request_url and each slash command's url. Use ngrok http 4242 (or a tunnel of your choice) when iterating locally.
src/server/slack/jobs.ts keys on event_id so Slack retries (3× exponential backoff) are no-ops. Status transitions: received → parsing → searching → cart → hold → checkout → done | failed.