Extension Commands
Present an extension tool as an operator verb — declaration, nested paths, projected flags, and how exec differs from tool invoke.
An extension can present any of its tools as an operator-facing command:
compozy extension exec notes --cmd add --text "ship the beta" --tag releaseA command is presentation metadata on a tool, not a second runtime. exec resolves the command
descriptor and then performs exactly one POST /api/tools/ext__notes__add/invoke. Tool policy,
approvals, risk gates, availability, and trusted_workspace apply unchanged — a command can never
reach past a gate the equivalent tool invoke would enforce.
Declare a command
Add a command block to a tool registration. Everything else — the input schema, the handler, the
risk class — is the tool declaration you already wrote.
compozysdk.Tool[addInput](
extension,
"add",
compozysdk.ToolOptions{
Description: "Store one note",
InputSchema: addInputSchema,
Command: &compozysdk.ExtensionCommandSpec{
Verb: "add",
Summary: "Store one note",
Example: `--text "ship the beta" --tag release --tag beta`,
Flags: map[string]string{"text": "text", "tag": "tags"},
},
},
handleAdd,
)TypeScript is the same shape:
extension.tool<AddInput>(
"add",
{
description: "Store one note",
inputSchema: addInputSchema,
command: {
verb: "add",
summary: "Store one note",
example: '--text "ship the beta" --tag release --tag beta',
flags: { text: "text", tag: "tags" },
},
},
handleAdd
);flags maps a CLI flag name to a top-level field of the tool's input schema. The flag name and
the field name do not have to match.
Nested paths and groups
A verb is a path whose segments are joined with /, the same hierarchy idiom the Host API uses for
method paths:
Command: &compozysdk.ExtensionCommandSpec{
Verb: "list/recent",
Summary: "List the most recent notes",
Example: "--limit 3 --format markdown",
Flags: map[string]string{"limit": "limit", "format": "format"},
},Declare the parent so it carries help text:
extension.CommandGroup("list", "Read stored notes")extension.commandGroup("list", "Read stored notes");Rules, all enforced at build and at manifest load:
| Rule | Detail |
|---|---|
| Maximum depth 2 | group/leaf. A third segment is rejected. |
| Groups are never executable | --cmd list errors and lists the group's leaves; nothing reaches the tool runtime. |
| Every leaf is exactly one tool | One handler, one input schema, one flag set, one risk class. |
| No group-level flags | Each leaf declares its complete flag set, so no invocation inherits state. |
| Paths are unique per extension | A duplicate path fails the build, naming both tools. |
| A leaf cannot also be a group prefix | list and list/recent cannot both be leaves. |
| A declared group needs at least one leaf | Leafless groups, multi-segment group paths, empty segments, and leading or trailing slashes are rejected. |
Groups may also be left implicit: the help tree still groups by prefix, just without a description.
Command paths are decoupled from tool IDs. list/recent backs ext__notes__list_recent, so the
presentation tree can be reorganized without breaking the IDs agents call.
Projected flags
The daemon projects each mapped field into a typed flag. The accepted subset, after canonical $ref
resolution:
| Schema | Projection |
|---|---|
string, boolean, integer, number | One scalar flag. |
["string", "null"] and other nullable scalars | The optional scalar. An explicit null needs --input. |
array whose items is one supported scalar | A repeatable flag; each occurrence appends. |
enum of the projected scalar type | Choices render in help; values are validated by the daemon on invoke. |
default, minimum, maximum | Render in help. The CLI never injects a default — the tool's own validation applies it. |
Everything else is rejected with an error naming the field and pointing at --input: object,
nested arrays, tuples and prefixItems, oneOf/anyOf/allOf/not, conditional schemas,
multi-type unions beyond nullable, and unresolvable or cyclic $ref.
Conversion is fixed:
- Booleans are presence-true; write
--flag=falseto sendfalseexplicitly. - An absent optional flag omits the field. It is never sent as
null. - A schema-required field becomes a required flag, checked before the invoke leaves the CLI.
- A conversion failure names the flag and the field it feeds.
These flag names are reserved for the host and rejected in a flags map:
agent, approval-token, cmd, help, input, json, o, output, session, workspace.
Walk through the sample extension
sdk/examples/notes-commands is a complete public-SDK extension with one flat leaf, one declared
group, and one nested leaf. Build and link it:
compozy extension dev ./sdk/examples/notes-commandsbuild generates the command block into the manifest — this is the real generated output:
[[resources.command_groups]]
path = "list"
summary = "Read stored notes"
[resources.tools.add.command]
example = "--text \"ship the beta\" --tag release --tag beta"
summary = "Store one note"
verb = "add"
[resources.tools.add.command.flags]
tag = "tags"
text = "text"
[resources.tools.list_recent.command]
example = "--limit 3 --format markdown"
summary = "List the most recent notes"
verb = "list/recent"
[resources.tools.list_recent.command.flags]
format = "format"
limit = "limit"Discover
compozy extension commands notesThe human tree renders groups above their leaves; -o json and -o jsonl return a flat,
path-keyed array with each leaf's projected flags, risk class, and approval_required.
compozy extension commands notes -o jsonRun a flat leaf
compozy extension exec notes --cmd add --text "ship the beta" --tag release --tag beta--tag is repeatable because tags is an array of strings. The two occurrences become
{"tags":["release","beta"],"text":"ship the beta"}.
Run a nested leaf
compozy extension exec notes --cmd list/recent --limit 3 --format markdownAsk for help
compozy extension exec notes --cmd list/recent --helpHelp is generated from the descriptor, so it always reflects the installed extension:
Usage: compozy extension exec notes --cmd list/recent [flags]
List the most recent notes
Projected flags:
--format string, field=format, choices=text|markdown
--limit integer, field=limit, default=5, minimum=1, maximum=50Refuse a group
compozy extension exec notes --cmd listerror: cli: command path "list" is a group and is not executable; available leaves: list/recentNo invocation reaches the runtime. A misspelled path prints the installed command list plus a did-you-mean suggestion computed from it.
Escape hatch: --input
When a tool takes a nested object, skip projection entirely:
compozy extension exec notes --cmd add --input '{"text":"ship the beta","tags":["release"]}'--input and projected flags are mutually exclusive; using both is an error rather than a silent
merge.
extension exec versus tool invoke
Both execute the same tool through the same daemon route.
compozy extension exec | compozy tool invoke | |
|---|---|---|
| Addressed by | Command path (--cmd list/recent) | Tool ID (ext__notes__list_recent) |
| Input | Projected flags or --input | --input, --input-file, or stdin |
| Help | Generated per command from the descriptor | Generic tool help |
| Round trips | Descriptor fetch, then invoke | Invoke |
| Intended for | Operators typing a verb | Agents and scripts holding a tool ID |
Policy is identical: an approval-gated command is refused through exec exactly as through
tool invoke, with the same deterministic error, and succeeds after compozy tool approve with the
same --approval-token. A command backed by an unavailable tool reports that tool's reason code.
Agents do not need exec. They already invoke ext__* tools natively; exec is the human skin over
the same runtime.
Host flags
These are consumed by exec itself and never projected: --cmd, --input, -o/--output,
--json, --workspace, --session, --agent, --approval-token, --help.
Related
Extension Permissions
Declare the Host API methods an extension calls, read the consent areas Compozy derives from them, and understand the ceilings applied per install source.
Publish an Extension
Ship an extension to other people through GitHub Releases or a plain Git repository — no catalog gatekeeper, no pull request to Compozy.