Anatomy of a declared command
One line, four parts. The colour of each part says where it lands in the handler.
/deploy
Version
4.2.1
#releases
as the composer shows it
-
/deploy
The command name. Routes to the execution handler, exactly as today.
-
Version
Which signature is in play — read back as
invocation.get("signature")
-
4.2.1
A declared option. Typed and checked before send, then
params.get("version")
-
#releases
An entity option. Arrives hydrated as a chat, and still in
mentions
Where a keystroke goes
Three handlers, each on its own trigger. Only the third exists today for every command.
1
Suggestion handler
Already in Cliq. Fills the list when a slot with suggestions opens.
on slot open
2
Autocomplete handler
New. Narrows a list against the partial text, scoped to options already filled.
per keystroke · 25 rows · 3s
3
Execution handler
Unchanged, plus params and invocation. Runs once, on send
on send
Between 2 and 3 the composer checks types, pattern, min, max and choices itself — so a malformed command never reaches the handler.
The declaration
A command gains schemaVersion and a signatures array. Presence of schemaVersion is the switch — without it a command behaves exactly as it does today.
// /deploy
{
"schemaVersion": 2,
"name": "deploy",
"contexts": ["channel", "thread"],
"signatures": [{
"id": "by-ref",
"options": [
{ "name": "service", "type": "STRING",
"required": true, "autocomplete": true },
{ "name": "environment", "type": "STRING",
"required": true,
"choices": [{ "label": "Staging", "value": "staging" },
{ "label": "Production", "value": "prod" }] },
{ "name": "version", "type": "STRING",
"required": true, "autocomplete": true },
{ "name": "notify", "type": "CHAT",
"required": false, "chatTypes": ["public", "org"] }
]
}]
}
Execution handler
Everything the handler receives today is unchanged. Two attributes are added.
| Attribute | Contents |
| arguments | The raw string after the command name. Still delivered, including for declared commands. |
| selections | What you picked from a suggestion handler's list, as a list of maps (title, description, imageurl). It is the pick itself, not a typed value — which is why a duration you type is an option, and a duration you choose is a selection. Unchanged. |
chat, user, options, mentions, attachments, location | As documented today, unchanged. |
| paramsnew | Map keyed by option name. Scalars coerced; entities hydrated. An absent optional option is an absent key, not a null. |
| invocationnew | Map: signature, subcommand, group, schemaVersion, obsolete. |
// execution handler · Deluge
sig = invocation.get("signature");
if(sig == "by-run")
{
build = params.get("run"); // long, already >= 1
}
else
{
build = resolve_ref(params.get("version"));
}
target = params.get("notify"); // hydrated chat, or absent
cutover = params.get("date"); // a real date, not a string
msg = "Deploying " + params.get("service");
msg = msg + " on " + cutover.toString("dd MMM");
response = Map();
response.put("text", msg);
return response;
Option types
The type is what the composer checks before send, and what params hands back. options still carries the same value as a string, so an old handler keeps working.
| Type | In params | Composer shows |
| STRING | text, against pattern | a text slot |
INTEGER DECIMAL | number, against min/max | a numeric slot |
| BOOLEAN | true / false | a two-state slot |
| DATEnew | a date, not a string — no parsing in the handler, no ambiguity over 03/04 | a date picker |
| DURATIONnew | a number of seconds. Written 90m, 2h30m, 1d — the unit is part of the value, which is why a bare INTEGER will not do | a duration slot |
USER, CHAT, FILE | the hydrated entity; also still in mentions / attachments | a picker with a thumbnail |
A value picked from a suggestion list stays in selections as it is today. A typed, checked value is what params adds — the two answer different questions, so both are delivered.
Autocomplete handler
A third handler beside execution and suggestion. It fires per keystroke on options marked autocomplete.
| Attribute | Contents |
| focused | Map: name, type, value — the partial text typed so far. |
| params | Options filled so far, so a version list can be scoped to the service already chosen. |
// autocomplete handler · Deluge
typed = focused.get("value");
service = params.get("service"); // scope to what is filled
rows = List();
for each tag in tags_for(service)
{
if(tag.get("name").containsIgnoreCase(typed))
{
row = Map();
row.put("title", tag.get("name"));
row.put("description", tag.get("pushed_at"));
row.put("value", tag.get("sha")); // sent to the handler
rows.add(row);
}
}
response = Map();
response.put("data", rows);
return response;
- 25 rows maximum, in a 3 second budget — past that the composer keeps the last good list.
- Row shape is today's suggestion row plus a
value distinct from its label: title, description, imageurl, value.
- Rows are advisory. A returned value still passes
pattern, min, max and choices — which is why a typed value that isn't listed is offered back rather than refused.
Entity options
An option typed APP_ENTITY routes to the app's own search handler — the same one serving @ mentions and global search, so an extension implements entity search once.
| Search field | In a command slot |
| category.id / name | Matched against the option's category; the name becomes a section heading. |
| item.id | The value delivered in params. |
| item.name / description / icon | Row title, subtitle and thumbnail. |
| item.action | Not run. The command's own handler is the action. |
Responses
| Mode | Behaviour |
| visibility: invoker | Default. Rendered to the caller only, no chat history entry. |
| visibility: chat | Posted as a message, attributed to the command. |
| defer: true | Acknowledges at once with a pending state, then the handler edits it. Window: 15 minutes. |
Back-linking
Declared per option as backlink, fired after the handler returns. Off by default — a hyperlinked entity with a preview is usually enough, and a second message in the referenced channel is noise. The exception is a FILE option, which checks access by default, because that failure is otherwise silent.
Limits
| Limit | Value |
| Options per signature | 5 — past that they stop fitting on one line of the composer |
| Signatures per command | 4 |
| Subcommands / nesting | 25 / one level only |
| Choices per option | 25 |
| Autocomplete rows / budget | 25 / 3s |
| Deferred follow-up window | 15m |
| Search-handler categories | 10 |
Compatibility
- No
schemaVersion means the old path, forever.
arguments is delivered to declared commands too, so a handler can migrate one option at a time.
- Adding an optional option, or a new signature, is backward compatible. Making an option required, renaming one, or narrowing a type is breaking — add a signature and mark the old one
obsolete.
- Ordinary messages are untouched. The slash is the only thing that opts you in.