Search
Search documentation
Programs
Updated Oct 21, 2026
Program(router={...}, **config) builds a CLI tree: router maps each
subcommand name to a command or a nested program, and the remaining keyword
arguments are the program's config. A Program is a subclass of click's
Group.
from typing import Literal
from pydantic import BaseModel
from fireargs import Program, command
class GreetInput(BaseModel):
name: str
class Greeting(BaseModel):
greeting: str
@command(description="Greet someone")
def greet(input: GreetInput) -> Greeting:
return Greeting(greeting=f"hello {input.name}")
class DeployInput(BaseModel):
env: Literal["staging", "prod"]
class Deployed(BaseModel):
ok: bool
@command(description="Deploy the app")
def deploy(input: DeployInput) -> Deployed:
return Deployed(ok=True)
cli = Program(
name="myapp",
description="My CLI",
version="1.0.0",
router={"greet": greet, "deploy": deploy},
)
if __name__ == "__main__":
cli()myapp greet --name world, myapp deploy --env prod, myapp --help,
myapp --llms all work, and myapp alone prints the help.
The router key wins as the subcommand name
router={"greet": some_command} attaches some_command under the name
"greet" even if the leaf was built with name="something-else" or from a
function with another name. Decoupling the program-level routing from each
leaf's own naming makes it easy to mount the same command twice or rename at
attach time.
Nesting
A route is either a Command (from @command(...)) or a Program, so a
program can be a subcommand of another program:
api = Program(description="API tasks", router={"greet": greet, "deploy": deploy})
db = Program(description="Database tasks", router={"migrate": migrate, "seed": seed})
cli = Program(name="myapp", router={"api": api, "db": db, "status": status})
# myapp api greet --name world
# myapp db migrate
# myapp status--llms flattens the tree to space-separated tool names — the inner greet
becomes "api greet" in the manifest. See LLMs mode.
Command, Program and Route
Command subclasses click's Command and Program subclasses click's
Group, so main(...), --help, CliRunner and the rest of click work
unchanged. Route is their union. A plain click command isn't a Route:
without fireargs' schemas it would be missing from --llms, so Program
rejects it with a TypeError.
from fireargs import Command, Program, RouteAliases
aliases=["ls"] on a command or nested program lets its parent route the
extra names to it too. Aliases are not listed in --help and add no tools to
--llms; mount the route under a second router key when it should be
listed.
Grouping commands in help
Set help_group on a command or a nested program to list it under its own
heading in the parent's --help, the same way Option(help_group=...)
groups options. Click adds the trailing colon, so write "Ops", not
"Ops:". Routes without one stay under Commands, and headings print in
the order their first route appears.
@command(description="Deploy", help_group="Ops")
def deploy(input: DeployInput) -> Deployed: ...
users = Program(description="Manage users", help_group="Ops", router={"list": list_users})
Program(name="myapp", router={"deploy": deploy, "users": users, "version": version})
# Ops:
# deploy Deploy
# users Manage users
#
# Commands:
# version Print versionProgramConfig
Same keywords as CommandConfig minus
json_option — programs dispatch, they don't read JSON input. Everything
else applies: name, description, aliases, help_group, version,
version_option, llms_option, render_error and click's
context_settings, epilog, short_help, options_metavar,
add_help_option, no_args_is_help, hidden and deprecated.
Two of them reach past the program itself:
versionwithversion_option={"flags": "-v, --version"}keeps a short version flag.render_errorset on a program covers every leaf below it that has no hook of its own, so one hook formats errors for the whole tree (see JSON mode).@cli.render_errorregisters it as a decorator.
Options belong to their level
Click reads options up to the subcommand name as the program's and leaves
the rest to the subcommand, so myapp --llms is the program's flag and
myapp greet --llms the leaf's. No setting is needed for tree CLIs.
--help and --llms per level
Each level keeps its own:
myapp --help # top-level help, lists subcommands
myapp greet --help # leaf-level help, lists args/options
myapp --llms # entire tree as a flat MCP tools/list
myapp greet --llms # just the greet leafSuppress program-level --llms with llms_option=False in the program
config; subcommands keep theirs.
Created with ❤ and Livemark