Fireargs is in technical previewShare feedback

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, Route

Aliases

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 version

ProgramConfig

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:

  • version with version_option={"flags": "-v, --version"} keeps a short version flag.
  • render_error set 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_error registers 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 leaf

Suppress program-level --llms with llms_option=False in the program config; subcommands keep theirs.

Created with ❤ and Livemark