Skip to main content

Overview

Bench allows you to extend its functionality by creating custom commands within your Frappe applications. These commands become available through the bench CLI, making it easy to add application-specific functionality to your development workflow.

How Custom Commands Work

Bench utilizes frappe.utils.bench_manager to discover and load commands from:
  • The Frappe framework itself
  • Any custom applications installed in your bench environment
When you run a bench command, bench communicates with Frappe, which in turn searches for available commands across all installed applications.

Creating Custom Commands

Directory Structure

To make custom commands available to bench, create a commands module under your application’s parent module:
frappe-bench/
├── apps/
│   ├── frappe/
│   └── custom_app/
│       ├── README.md
│       ├── custom_app/
│       │   ├── commands/    ← commands module
│       │   │   ├── __init__.py
│       │   │   └── ...
│       ├── license.txt
│       ├── requirements.txt
│       └── setup.py
The commands module can be:
  • A single file: commands.py
  • A directory with an __init__.py file

Command Structure

Custom commands use the Click library for command-line interface creation. Here’s the basic structure:
# file_path: frappe-bench/apps/custom_app/custom_app/commands.py
import click

@click.command('my-command')
@click.argument('state', type=click.Choice(['on', 'off']))
@click.option('--verbose', is_flag=True, help='Enable verbose output')
def my_command(state, verbose):
    """Description of what your command does."""
    from custom_app.utils import do_something
    do_something(state=state, verbose=verbose)

# Export the command in a list
commands = [
    my_command
]

Key Components

1

Import Click

Use the Click library for command definition:
import click
2

Define Command

Use the @click.command() decorator to define your command:
@click.command('command-name')
def command_name():
    pass
3

Add Arguments and Options

Use Click decorators to add arguments and options:
@click.argument('arg_name')
@click.option('--option-name', help='Description')
4

Export Commands

Create a commands list containing all your command functions:
commands = [
    command_one,
    command_two,
]

Example: Flag Management Command

Here’s a complete example for an application named ‘flags’:
# file_path: frappe-bench/apps/flags/flags/commands.py
import click

@click.command('set-flags')
@click.argument('state', type=click.Choice(['on', 'off']))
def set_flags(state):
    """Set application flags on or off."""
    from flags.utils import set_flags
    set_flags(state=state)

commands = [
    set_flags
]

Usage

Once the command is defined, you can execute it from within your bench directory:
bench set-flags on
# Output: Flags are set to state: 'on'

Advanced Command Features

Multiple Arguments

@click.command('process-data')
@click.argument('input_file')
@click.argument('output_file')
def process_data(input_file, output_file):
    """Process data from input file to output file."""
    # Your implementation
    pass

Options with Values

@click.command('export-data')
@click.option('--format', type=click.Choice(['json', 'csv', 'xml']), default='json')
@click.option('--limit', type=int, default=100, help='Maximum records to export')
def export_data(format, limit):
    """Export data in specified format."""
    # Your implementation
    pass

Multiple Commands in One Module

import click

@click.command('import-data')
@click.argument('file')
def import_data(file):
    """Import data from file."""
    pass

@click.command('export-data')
@click.argument('file')
def export_data(file):
    """Export data to file."""
    pass

@click.command('validate-data')
def validate_data():
    """Validate all data."""
    pass

commands = [
    import_data,
    export_data,
    validate_data,
]

Best Practices

Choose clear, descriptive names that follow bench’s naming convention (lowercase with hyphens).
@click.command('sync-customers')  # Good
@click.command('sc')              # Avoid
Always include docstrings and help text for commands and options:
@click.command('sync-data')
@click.option('--force', is_flag=True, help='Force sync even if data exists')
def sync_data(force):
    """Synchronize data from external source."""
    pass
Implement proper error handling and provide meaningful error messages:
def my_command():
    try:
        # Your code
        pass
    except Exception as e:
        click.secho(f"Error: {e}", fg='red')
        sys.exit(1)
Leverage Click’s features for better user experience:
  • Type validation: type=int, type=float, type=click.Path()
  • Choices: type=click.Choice(['opt1', 'opt2'])
  • Prompts: prompt=True
  • Confirmation: click.confirm('Continue?', abort=True)

Listing Available Commands

To see all available commands, including custom ones:
bench --help
Your custom commands will appear in the command list alongside built-in bench commands.

Click Documentation

Learn more about the Click library

All Commands

See all available bench commands

Build docs developers (and LLMs) love