Slash Command Grammar DSL
View SourceThe slash/2 DSL is built to make slash commands deterministic, easy to maintain, and fast. Instead of manually splitting strings or juggling regexes, you describe the format you expect and SlackBot generates a parser at compile time. Write your grammar by chaining primitives (literal/2, value/2, optional/1, etc.) and always finish with a single handle/3 clause—nothing may appear after handle/3, and the compiler raises if you try. This guide teaches the DSL in layers so you can follow along as the commands grow in complexity.
Why use the DSL?
- Deterministic parsing – handlers receive structured maps, not ad-hoc token lists.
- Readable expectations – the command format lives next to the handler, making code reviews and maintenance straightforward.
- Compile-time validation – malformed definitions fail fast, before your bot ships.
- Battle-tested parsing – handles quoting, whitespace, and tricky edge cases without extra work on your part.
1. Literal-only commands
Great for “one-shot” commands that trigger behavior without arguments.
slash "/cmd" do
literal "project"
literal "report"
handle payload, ctx do
# payload["parsed"] => %{command: "cmd"}
Reports.generate(ctx)
end
endSlack input: /cmd project report
2. Capturing values
Use value/1 to bind user-provided tokens to names that show up in the parsed payload.
slash "/cmd" do
literal "team", as: :mode, value: :team_show
value :team_name
literal "show"
handle payload, ctx do
%{team_name: name} = payload["parsed"]
Teams.show(name, ctx)
end
endSlack input: /cmd team marketing show
Parsed payload: %{command: "cmd", mode: :team_show, team_name: "marketing"}
3. Optional segments
Wrap anything that isn’t required in optional. Omitted segments simply don’t appear in
the parsed map.
slash "/cmd" do
literal "list", as: :mode, value: :list
optional literal("short", as: :short?)
value :app
handle payload, _ctx do
payload["parsed"]
end
endSlack input: /cmd list short foo
Parsed payload: %{command: "cmd", mode: :list, short?: true, app: "foo"}
4. Repeating segments
repeat lets you express “zero or more” patterns. Each value inside becomes a list.
slash "/cmd" do
literal "report", as: :mode, value: :report_teams
repeat do
literal "team"
value :teams
end
handle payload, _ctx do
payload["parsed"]
end
endSlack input: /cmd report team alpha team beta team gamma
Parsed payload: %{teams: ["alpha", "beta", "gamma"], mode: :report_teams}
5. Branching with choice
Many commands act like subcommands. choice lets you express each branch declaratively.
slash "/cmd" do
choice do
sequence do
literal "list", as: :mode, value: :list
optional literal("short", as: :short?)
value :app
end
sequence do
literal "project", as: :mode, value: :project_report
literal "report"
end
end
handle payload, ctx do
parsed = payload["parsed"]
handle_mode(parsed.mode, parsed, ctx)
end
endSlack inputs covered: /cmd list app, /cmd list short app, /cmd project report
6. End-to-end example
The tests (test/slack_bot/router_test.exs) contain a full “GrammarRouter” that combines
all the primitives. Here’s how a few Slack inputs map to payloads:
| Slack input | Parsed payload |
|---|---|
/cmd list short app param one param two | %{mode: :list, short?: true, app: "app", params: ["one","two"]} |
/cmd project report | %{mode: :project_report} |
/cmd team marketing show | %{mode: :team_show, team_name: "marketing"} |
/cmd report team one team two team three | %{mode: :report_teams, teams: ["one","two","three"]} |
Each branch is explicit, and the handler simply reacts to structured data.
Handler payload structure
Every DSL handler receives an enriched payload under payload["parsed"]:
%{
command: "cmd",
mode: :list,
short?: true,
app: "foo",
params: ["one", "two"],
teams: ["alpha", "beta"],
extra_args: ["leftover"] # present only if tokens remain unmatched
}- Repeated values become lists.
- Optional literals store the
value:option (defaulttrue) when matched. - Any leftover tokens land in
:extra_args, allowing custom fallbacks.
Quick reference
| Macro | Purpose | Example |
|---|---|---|
literal value, opts \\ [] | Match a literal token, optionally tagging metadata | literal "list", as: :mode, value: :list |
value name, opts \\ [] | Capture a token and assign it to name | value :service |
optional do ... end | Optional group; skipped segments leave previous values untouched | optional literal("short", as: :short?) |
repeat do ... end | Repeat group until it no longer matches | repeat do literal "team"; value :teams end |
choice do ... end | First matching branch wins | choice do sequence ... end |
sequence do ... end | Explicit grouping (helpful inside choice) | sequence do literal "project"; literal "report" end |
handle payload, ctx do ... end | Handler that receives the enriched payload | handle payload, ctx do ... end |
Tips
- Use
SlackBot.Diagnostics.list/2+replay/2to capture real commands and verify they parse as expected. - Prefer small, focused
choicebranches over one giant handler with nestedcase. - Need raw tokens? Call
SlackBot.Command.lex/1yourself. - See
test/slack_bot/router_test.exsfor more real-world examples.
Next Steps
- Getting Started — set up a Slack App and run your first handler
- Rate Limiting — understand how SlackBot paces Web API calls
- Diagnostics — capture and replay commands for debugging
- Telemetry Dashboard — monitor handler execution and timing