Custom CLI Commands (flask CLI)
Reviewed & published by Brayan K
A custom CLI command lets you run app tasks — seeding data, creating an admin, clearing a cache — from the terminal with flask <name>, with full access to your app.
Part of the free Flask course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll build Click commands with arguments and options, run them in Python, register them on a Flask app with access to its config, and meet flask db.
What You'll Learn in This Lesson
- Building a Click command with @click.command()
- Required arguments vs optional options
- Type conversion and flags
- Registering commands with @app.cli.command()
- Using app config inside a command
- Real extension commands like flask db
1 A Click Command: Arguments & Options
Flask's CLI is powered by Click. A command is just a function decorated with @click.command(). You add arguments (required, positional) with @click.argument and options (optional, named, with defaults) with @click.option. Click handles parsing and help text for you.
The runnable example below defines a hello command and invokes it with Click's test runner — no real terminal needed. Notice how the option's default applies when you don't pass --greeting.
# A click command function, invoked directly in Python
import click
from click.testing import CliRunner
@click.command()
@click.argument("name")
@click.option("--greeting", default="Hello", help="Word to greet with")
def hello(name, greeting):
"""Greet someone by NAME."""
click.echo(f"{greeting}, {name}!")
# Invoke the command without a real terminal:
runner = CliRunner()
result = runner.invoke(hello, ["Ada"])
print(result.output, end="") # Hello, Ada!
result = runner.invoke(hello, ["Bob", "--greeting", "Hi"])
print(result.output, end="") # Hi, Bob!
# ✅ Expected output:
# Hello, Ada!
# Hi, Bob!hello Ada uses the default greeting; hello Bob --greeting Hi overrides it. The argument is required, the option is not.
2 A Seed Command with Typed Options
A classic use is a seed command that fills your database with sample data for development. Options can declare a type so Click converts and validates the input — passing --count 5 arrives as the integer 5, and a non-number is rejected with a helpful error.
The runnable example seeds an in-memory list. Run it twice with different counts to see the typed option in action.
# A seed command that runs against an in-memory "database"
import click
from click.testing import CliRunner
db = [] # pretend table
@click.command()
@click.option("--count", default=3, type=int, help="How many rows to seed")
def seed(count):
"""Insert sample rows into the database."""
db.clear()
for i in range(1, count + 1):
db.append({"id": i, "title": f"Task {i}"})
click.echo(f"Seeded {len(db)} rows.")
runner = CliRunner()
print(runner.invoke(seed, []).output, end="") # Seeded 3 rows.
print(runner.invoke(seed, ["--count", "5"]).output, end="") # Seeded 5 rows.
print("db now has", len(db), "rows") # db now has 5 rows
# ✅ Expected output:
# Seeded 3 rows.
# Seeded 5 rows.
# db now has 5 rows3 Commands Run Inside the App Context
The big payoff of @app.cli.command is that Flask runs your command inside an application context. That means current_app, its config, and app-bound extensions (like the database) are available automatically — exactly as they are in a request.
The runnable example registers a create-admin command that reads current_app.config, and invokes it with Flask's own CLI test runner (which pushes the app context for you).
# Registering a command on a Flask app with app context
import click
from flask import Flask, current_app
app = Flask(__name__)
app.config["ADMIN_EMAIL"] = "admin@example.com"
@app.cli.command("create-admin")
@click.argument("username")
def create_admin(username):
"""Create an admin user (has access to app config)."""
# commands registered via @app.cli run inside the app context,
# so current_app and its config are available
email = current_app.config["ADMIN_EMAIL"]
click.echo(f"Created admin '{username}' <{email}>")
# Invoke it through Flask's CLI runner (it pushes an app context for us):
runner = app.test_cli_runner()
result = runner.invoke(args=["create-admin", "ada"])
print(result.output, end="") # Created admin 'ada' <admin@example.com>
# ✅ Expected output:
# Created admin 'ada' <admin@example.com>🔬 CLI Cheat Sheet
| You want to... | Use |
|---|---|
| Register a command | @app.cli.command("name") |
| Required input | @click.argument("x") |
| Optional input | @click.option("--y", default=...) |
| A boolean flag | @click.option("--v", is_flag=True) |
| Print output | click.echo("...") |
🎯 Your Turn: Fill in the Blanks
Complete the command below. Replace each ___ so it takes a required word and an optional --times.
# 🎯 YOUR TURN: replace each ___ to finish the command
import click
from click.testing import CliRunner
@click.command()
@click.___("word") # a required positional input
@click.option("--times", default=1, type=int)
def repeat(word, times):
"""Print WORD a number of times."""
for _ in range(times):
click.___(word) # print one line
runner = CliRunner()
print(runner.invoke(repeat, ["hi", "--times", "3"]).output, end="")
# hi
# hi
# hiCommon Errors & Quick Reference
❌ "Could not locate a Flask application"
The CLI doesn't know where your app is. Set FLASK_APP=app.py (or use an app factory in create_app) so flask <command> can import it.
❌ "Working outside of application context"
You accessed current_app or db in a plain @click.command not registered on the app. Register it with @app.cli.command so Flask pushes the context for you.
| Command | Does |
|---|---|
| flask run | Start the dev server |
| flask shell | Open a shell with app context |
| flask db upgrade | Apply migrations (Flask-Migrate) |
| flask --help | List all commands |
🏆 Mini Challenge: A Stats Command
Build a command that reports user counts, with a flag to filter.
- Add an --active-only boolean flag
- Without it, count all users; with it, count only active ones
- Echo the total
# 🏆 Mini Challenge: a stats command over an in-memory list
import click
from click.testing import CliRunner
users = [
{"name": "Ada", "active": True},
{"name": "Bob", "active": False},
{"name": "Cy", "active": True},
]
@click.command()
@click.option("--active-only", is_flag=True, help="Count only active users")
def stats(active_only):
"""Report user counts."""
rows = [u for u in users if u["active"]] if active_only else users
click.echo(f"{len(rows)} users")
runner = CliRunner()
print(runner.invoke(stats, []).output, end="") # 3 users
print(runner.invoke(stats, ["--active-only"]).output, end="") # 2 users❓ Frequently Asked Questions
Lesson complete — your app has its own toolbox!
You built Click commands with arguments, typed options, and flags, registered them on a Flask app with @app.cli.command, used current_app.config inside a command, and met flask db.
Practice quiz
Which library powers the flask CLI under the hood?
- argparse
- Click
- Typer
- docopt
Answer: Click. Flask's CLI is built on Click.
How do you register a custom command on a Flask app?
- @app.command('name')
- @click.flask('name')
- @app.cli.command('name')
- @app.register('name')
Answer: @app.cli.command('name'). Decorate with @app.cli.command('name') to register a command.
Which Click decorator declares a required positional input?
- @click.argument('x')
- @click.option('--x')
- @click.flag('x')
- @click.input('x')
Answer: @click.argument('x'). @click.argument('x') is a required positional argument.
Which Click decorator declares an optional named input?
- @click.argument('--x')
- @click.required('x')
- @click.positional('x')
- @click.option('--x', default=...)
Answer: @click.option('--x', default=...). @click.option('--x', default=...) is an optional named input.
Why can a @app.cli command use current_app and db directly?
- Flask runs it inside an application context
- Click imports them globally
- It runs in a request context
- Flask disables contexts for the CLI
Answer: Flask runs it inside an application context. Flask pushes an application context before running an @app.cli command.
Which call prints output from a Click command?
- print()
- click.echo('...')
- sys.stdout('...')
- click.print('...')
Answer: click.echo('...'). click.echo() prints output from a command.
How do you make an option a boolean flag?
- @click.option('--v', bool=True)
- @click.flag('--v')
- @click.option('--v', is_flag=True)
- @click.bool('--v')
Answer: @click.option('--v', is_flag=True). @click.option('--v', is_flag=True) makes a boolean flag.
Which command group is added by the Flask-Migrate extension?
- flask run
- flask shell
- flask routes
- flask db
Answer: flask db. flask db comes from Flask-Migrate (built on Alembic).
What error appears if Flask can't find your app for the CLI?
- Could not locate a Flask application
- 404 Not Found
- Working outside of request context
- Module not found: click
Answer: Could not locate a Flask application. Set FLASK_APP (or use a create_app factory) so the CLI can import the app.
What does @click.option('--count', type=int) do with '--count 5'?
- Keeps it the string '5'
- Raises an error
- Converts it to the integer 5
- Ignores the value
Answer: Converts it to the integer 5. Click converts the input to the declared type, so it becomes int 5.
Continue this course
- Previous: Logging & Monitoring
- Next: Signals (Blinker) — Decouple code by reacting to request and template signals
- Quick reference: Flask cheat sheet
Frequently asked questions
What is the flask CLI?
The flask command is a command-line interface built into Flask (powered by Click). It already gives you commands like 'flask run' and 'flask shell', and lets you register your own commands so you can run app tasks — seeding data, creating an admin, clearing a cache — straight from the terminal.
How do I register a custom command?
Decorate a function with @app.cli.command('name'). Flask adds it to the CLI so you can run 'flask name' in your terminal. Inside the function you have full access to your app — its config, database, and extensions — because Flask runs the command within an application context.
What is Click and how does it relate?
Click is the command-line library Flask uses under the hood. You add @click.argument('x') for required positional inputs and @click.option('--flag') for optional named inputs, and Click handles parsing, type conversion, help text, and error messages for you.
Why do CLI commands run inside an app context?
Many app tasks need the application — its config, database session, and extensions. Flask pushes an application context before running a @app.cli command, so current_app and things bound to the app (like db) work just as they do in a request, without you setting it up manually.
What is flask db and where does it come from?
flask db is a group of commands added by the Flask-Migrate extension (built on Alembic). Once installed, 'flask db migrate' generates a migration from your model changes and 'flask db upgrade' applies it. It's a real-world example of an extension registering its own CLI commands.