Adding Commands

July 6, 2026 · View on GitHub

Interface-first plugins: to declare a reviewable command surface separately from its implementation (the two-phase contribution workflow used by teams outside the CLI core), see writing-a-plugin.md.

Design sign-off before writing code

Before implementing any change to the user-facing interface — new commands, command groups, arguments, options, or output format — get maintainer sign-off.

What sign-off covers:

  • Command and subcommand names
  • Flag names, types, and defaults
  • Lifecycle stage (PrPr, PuPr, or GA) — see lifecycle.md
  • Output format and result type
  • Where the command lives (new group or existing one)

What sign-off does not cover: implementation details inside the plugin directory — service/manager structure, helper functions, file layout. Those are yours to decide as long as they stay encapsulated within _plugins/<your-plugin>/ and do not add to api/ or _app/.

Where to get sign-off: open a GitHub Issue describing the interface and tag a maintainer for review.


Design principles

Verbs for commands, nouns for groups. The group is the entity (snow git, snow stage), the command is the action (fetch, create, list). Avoid dashes in command names unless the concept truly cannot be expressed otherwise.

Find the closest existing command first. Match its flag names, output type, and default behavior. Consistency across groups beats local cleverness.

Commands are what, flags are how. Flags are configuration and conditions for a command (--force, --delta, --if-exists), not an alternate operation.


Which case are you in?

Adding a command to an existing group — skip to Writing commands. No registration changes needed.

New top-level command group — read Creating a command group first, then Registering the plugin.


Creating a command group

Flat structure

Use a flat layout when the group manages a single domain (one entity type or one set of related operations). snow git is the canonical example:

src/snowflake/cli/_plugins/<name>/
    __init__.py
    commands.py       # @app.command() definitions
    manager.py        # business logic / Snowflake API calls
    plugin_spec.py    # pluggy hook — wires commands into the CLI

commands.py creates the SnowTyperFactory app and defines all commands on it. See src/snowflake/cli/_plugins/git/ as a reference.

Nested structure

Use a nested layout when the group is a capability or platform that hosts multiple independent entity types — not just because the group is large. snow spcs hosts compute pools, services, image registries, and image repositories as separate sub-groups because each is a distinct entity type with its own lifecycle.

src/snowflake/cli/_plugins/<name>/
    __init__.py           # assembles the top-level app from sub-typers
    plugin_spec.py
    <entity-a>/
        commands.py
        manager.py
    <entity-b>/
        commands.py
        manager.py

__init__.py creates the parent app and adds each sub-typer:

from snowflake.cli._plugins.<name>.<entity_a>.commands import app as entity_a_app
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory

app = SnowTyperFactory(name="<name>", help="...")
app.add_typer(entity_a_app)

See src/snowflake/cli/_plugins/spcs/__init__.py as a reference.

Start flat. Move to nested only when a second distinct entity type is needed — the nesting should reflect domain structure, not anticipate it.


Registering the plugin

Two files need to change for a new top-level group.

plugin_spec.py — the pluggy hook that attaches the app to the CLI:

from snowflake.cli._plugins.<name> import commands
from snowflake.cli.api.plugins.command import (
    SNOWCLI_ROOT_COMMAND_PATH,
    CommandSpec,
    CommandType,
    plugin_hook_impl,
)

@plugin_hook_impl
def command_spec():
    return CommandSpec(
        parent_command_path=SNOWCLI_ROOT_COMMAND_PATH,
        command_type=CommandType.COMMAND_GROUP,
        typer_instance=commands.app.create_instance(),
    )

For a nested layout, app is defined in __init__.py, so import it from the package directly:

from snowflake.cli._plugins.<name> import app
...
    typer_instance=app.create_instance(),

See src/snowflake/cli/_plugins/spcs/plugin_spec.py as a reference.

src/snowflake/cli/_app/commands_registration/builtin_plugins.py — add an import and a dict entry in get_builtin_plugin_name_to_plugin_spec():

from snowflake.cli._plugins.<name> import plugin_spec as <name>_plugin_spec

def get_builtin_plugin_name_to_plugin_spec():
    plugin_specs = {
        ...
        "<name>": <name>_plugin_spec,
    }
    return plugin_specs

The plugin name string is what appears in snow --help. Pick a short, lowercase, hyphen-separated name that matches the SnowTyperFactory name= argument.


Writing commands

Every command must return a CommandResult subtype. Never call print() directly — use cli_console for progress messages (see conventions.md) and return a CommandResult for the command's final output.

The function's docstring becomes the command's --help text. Write it as a short, imperative sentence describing what the command does from the user's perspective:

@app.command()
def create(name: str):
    """Creates a new table."""
    ...

Return types

All types are in src/snowflake/cli/api/output/types.py:

TypeWhen to use
MessageResultPlain string confirmation — "Statement executed successfully."
QueryResultMulti-row cursor result (SHOW, SELECT returning many rows)
SingleQueryResultSingle-row cursor result (DESCRIBE, CREATE ... RETURN ...)
CollectionResultIterable of dicts with no cursor (locally constructed results)
ObjectResultSingle dict with no cursor

Other specialized types (MultipleResults, StreamResult, EmptyResult) exist for less common cases — see src/snowflake/cli/api/output/types.py.

Lifecycle and visibility

New commands start in PrPr behind a feature flag. See lifecycle.md for the SnowTyperFactory and @app.command() patterns that hide commands until the flag is enabled, and for the PrPr → PuPr → GA progression.

Destructive commands

Commands that modify or delete existing resources should use ForceOption and InteractiveOption from snowflake.cli.api.commands.flags.

Behavior matrix:

--force--interactiveResult
unsetFalse (non-interactive default, e.g. CI or piped input)Abort — safe default for scripts
unsetTrue (interactive terminal default)Prompt the user
seteitherProceed without prompting

Testing

Every new command needs comprehensive unit and integration tests covering the happy path and the main error paths. See testing.md for the runner fixture, snapshot tests, and how to test feature-flagged commands.

Reusable code in src/snowflake/cli/api/

Before writing new utilities, check src/snowflake/cli/api/ — it contains shared flags, helpers, and base classes that most plugins rely on:

ModuleWhat it provides
commands/flags.pyStandard flags: ForceOption, InteractiveOption, OutputFormatOption, PatternOption, and others
commands/snow_typer.pySnowTyperFactory — the standard way to create a command group
output/types.pyAll CommandResult subtypes
identifiers.pyFQN — fully-qualified name handling (see conventions.md)
project/util.pyto_string_literal, identifier_to_show_like_pattern
console/__init__.pycli_console for user-visible output
exceptions.pyCliError subclasses

For commands that operate on Snowflake objects, check whether an ObjectManager subclass already exists for your object type before writing raw SQL queries.