
Frappe Ops Bench
- 58 installs
- 159 repo stars
- Updated July 8, 2026
- openaec-foundation/erpnext_anthropic_claude_development_skill_package
Run Frappe bench CLI commands for site creation, multi-tenancy, and domain routing using bench init, new-site, and common_site_config.
About
A complete bench CLI reference for creating sites, configuring multi-tenancy, and managing domains in Frappe. A developer uses it when running bench commands or setting up multi-tenant environments.
- Complete bench CLI reference for site and app management
- Covers bench init, new-site, multi-tenancy, and DNS-based routing
Frappe Ops Bench by the numbers
- 58 all-time installs (skills.sh)
- Ranked #666 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/erpnext_anthropic_claude_development_skill_package --skill frappe-ops-benchAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 58 |
|---|---|
| repo stars | ★ 159 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/erpnext_anthropic_claude_development_skill_package ↗ |
What it does
Run Frappe bench CLI commands for site creation, multi-tenancy, and domain routing using bench init, new-site, and common_site_config.
Files
Bench CLI Complete Reference
Complete bench CLI reference for site management, app lifecycle, configuration, and multi-tenancy.
Version: v14/v15/v16
---
Quick Reference: Essential Commands
| Task | Command |
|---|---|
| Create bench | bench init myproject --frappe-branch version-15 |
| Create site | bench new-site mysite.localhost --admin-password admin |
| Set default site | bench use mysite.localhost |
| Get app | bench get-app erpnext --branch version-15 |
| Install app | bench --site mysite install-app erpnext |
| Start dev server | bench start |
| Run migrations | bench --site mysite migrate |
| Build assets | bench build --app myapp |
| Backup site | bench --site mysite backup |
| Restore backup | bench --site mysite restore /path/to/backup.sql.gz |
| Open console | bench --site mysite console |
| Open DB shell | bench --site mysite mariadb |
| Check scheduler | bench doctor |
| View pending jobs | bench show-pending-jobs |
| Update everything | bench update |
| Drop site | bench drop-site mysite --force |
---
Workflow 1: Creating a New Bench
# Initialize bench with specific Frappe version
bench init myproject --frappe-branch version-15
# With Python version
bench init myproject --frappe-branch version-15 --python python3.11
# Enter bench directory (REQUIRED for all subsequent commands)
cd myprojectWhat `bench init` creates:
myproject/
├── apps/ # Installed Frappe apps (frappe is default)
├── sites/ # All sites and shared config
│ └── common_site_config.json
├── config/ # Redis, Procfile, supervisor configs
├── env/ # Python virtual environment
├── logs/ # Log files
└── Procfile # Process definitions for bench startCritical Rules
- ALWAYS specify
--frappe-branchto pin Frappe version - ALWAYS run commands from inside the bench directory
- NEVER run bench commands as root — use a dedicated frappe user
---
Workflow 2: Site Management
Creating Sites
# Basic site creation
bench new-site mysite.localhost --admin-password admin
# With specific database
bench new-site mysite.localhost --db-name mysite_db --admin-password admin
# With MariaDB root password
bench new-site mysite.localhost --mariadb-root-password rootpass --admin-password admin
# Install apps during creation
bench new-site mysite.localhost --admin-password admin --install-app erpnextSetting Default Site
bench use mysite.localhost
# OR set environment variable for current session:
export FRAPPE_SITE=mysite.localhostDropping a Site
bench drop-site mysite.localhost --force
# Deletes database and archives site directorySite Directory Structure
sites/mysite.localhost/
├── site_config.json # Site-specific config (db credentials)
├── private/ # Auth-required files, backups
├── public/ # Publicly accessible files
├── locks/ # Scheduler lock files
└── task-logs/ # Scheduler task logs---
Workflow 3: App Management
# Download app from GitHub
bench get-app erpnext --branch version-15
bench get-app https://github.com/org/custom-app.git --branch main
# Install app on a site
bench --site mysite install-app erpnext
# List installed apps
bench --site mysite list-apps
# Remove app from site (creates backup first)
bench --site mysite uninstall-app custom_app
# Remove app from bench entirely
bench remove-app custom_app
# Switch app branch
bench switch-to-branch version-15 erpnext frappe
# Exclude app from updates
bench exclude-app custom_app
# Re-include app in updates
bench include-app custom_appCritical Rules
- ALWAYS
get-appbeforeinstall-app— get downloads, install activates - ALWAYS backup before
uninstall-app— it deletes app-related data - NEVER manually delete app folders — use
bench remove-app
---
Workflow 4: bench update: What It Does
# Full update (pull + migrate + build + restart)
bench update
# Update specific app only
bench update --pull --app erpnext
# Skip build step
bench update --no-build
# Skip backup
bench update --no-backup
# Reset to upstream (DESTROYS local changes)
bench update --reset`bench update` executes these steps in order: 1. Backup all sites 2. Pull latest code for all apps (git pull) 3. Install Python/Node requirements 4. Build static assets (bench build) 5. Run migrations on all sites (bench migrate) 6. Restart bench processes
Critical Rules
- ALWAYS run
bench updatein a screen/tmux session — it takes time - NEVER use
--resetin production without understanding it doesgit reset --hard - ALWAYS test updates on staging first
---
Workflow 5: bench migrate
# Migrate specific site
bench --site mysite migrate
# Migrate all sites
bench --site all migrate
# Check if safe to migrate (no pending jobs)
bench --site mysite ready-for-migrationWhat `bench migrate` does: 1. Runs schema sync (DocType changes → database) 2. Runs patches (data migrations) 3. Rebuilds search index 4. Syncs translations 5. Rebuilds Dashboard cache
When to Migrate
- After
bench update(done automatically) - After changing hooks.py
- After adding/modifying DocTypes
- After pulling code changes
- NEVER skip migrate after code changes — leads to schema mismatches
---
Workflow 6: bench build
# Build all apps
bench build
# Build specific app
bench build --app myapp
# Build with bundle analyzer
bench build --app myapp --production
# Watch mode (auto-rebuild on file changes)
bench watchWhen to Build
- After changing JS/CSS files
- After
bench get-app(done automatically) - After modifying
package.json - ALWAYS build after modifying client-side assets
---
Workflow 7: Console and Database Access
# IPython console (with Frappe loaded)
bench --site mysite console
# In console:
# >>> frappe.get_doc("Sales Invoice", "INV-001")
# >>> frappe.db.sql("SELECT name FROM `tabUser` LIMIT 5")
# Auto-reload on code changes
bench --site mysite console --autoreload
# MariaDB shell
bench --site mysite mariadb
# >>> SELECT name, email FROM tabUser LIMIT 5;
# PostgreSQL shell
bench --site mysite postgres
# Execute a method directly
bench --site mysite execute myapp.tasks.daily_cleanup
bench --site mysite execute myapp.api.process --kwargs '{"name": "INV-001"}'
# Make authenticated request as Administrator
bench --site mysite request GET /api/resource/User---
Workflow 8: Backup and Restore
# Backup (database + files)
bench --site mysite backup
# Creates: sites/mysite/private/backups/
# YYYY-MM-DD_HHMMSS-mysite-database.sql.gz
# YYYY-MM-DD_HHMMSS-mysite-files.tar
# YYYY-MM-DD_HHMMSS-mysite-private-files.tar
# Backup all sites
bench backup-all-sites
# Backup with encryption
bench --site mysite backup --backup-encryption-key mykey
# Restore from backup
bench --site mysite restore /path/to/database.sql.gz
# Restore with files
bench --site mysite restore /path/to/database.sql.gz \
--with-public-files /path/to/files.tar \
--with-private-files /path/to/private-files.tar
# Partial restore
bench --site mysite partial-restore /path/to/database.sql.gzCritical Rules
- ALWAYS backup before
bench update,uninstall-app, ordrop-site - Backups older than 24 hours auto-deleted by default — configure
keep_backups_for_hours - ALWAYS test restore on a staging site before relying on a backup
---
Workflow 9: Scheduler and Background Jobs
# Enable/disable scheduler
bench --site mysite scheduler enable
bench --site mysite scheduler disable
bench --site mysite scheduler pause
bench --site mysite scheduler resume
# Check scheduler health
bench doctor
# View queued jobs
bench show-pending-jobs
# Purge pending jobs
bench --site mysite purge-jobs
# Manually trigger scheduler event
bench --site mysite trigger-scheduler-event hourly
# Start worker manually (for debugging)
bench worker --queue short---
Workflow 10: Multi-Tenancy
DNS-Based Routing (Recommended)
# Enable DNS multi-tenancy
bench config dns_multitenant on
# Create sites with proper hostnames
bench new-site site1.example.com --admin-password admin
bench new-site site2.example.com --admin-password admin
# Regenerate nginx config
bench setup nginx
# Reload nginx
sudo service nginx reloadRequests are routed by matching the Host header to site names.
Port-Based Routing (Alternative)
bench config dns_multitenant off
bench new-site site2.localhost --admin-password admin
bench set-nginx-port site2.localhost 8082
bench setup nginx
sudo service nginx reloadCustom Domain Mapping
# Add domain to site
bench setup add-domain site1.example.com --site mysite
bench setup nginx
sudo service nginx reload---
Configuration: common_site_config.json
Located at sites/common_site_config.json — applies to ALL sites.
Essential Keys
| Key | Default | Purpose |
|---|---|---|
background_workers | 1 | Number of background job workers |
developer_mode | false | Auto-sync DocType changes to files |
dns_multitenant | false | Enable DNS-based multi-tenancy |
gunicorn_workers | 2 | Web server worker count (min: 2) |
maintenance_mode | 0 | Take all sites offline |
pause_scheduler | 0 | Pause job scheduler |
serve_default_site | — | Default site when host not matched |
server_script_enabled | false | Enable Server Scripts |
scheduler_tick_interval | 60 | Seconds between scheduler checks |
webserver_port | 8000 | Development server port |
socketio_port | 9000 | Socket.IO port |
live_reload | false | Auto-reload on asset rebuild |
Redis Configuration
| Key | Default |
|---|---|
redis_cache | redis://localhost:13000 |
redis_queue | redis://localhost:11000 |
redis_socketio | redis://localhost:13000 |
Setting Config Values
# Set common config (all sites)
bench config set-common-config -c background_workers 4
bench config set-common-config -c developer_mode 1
# Set site-specific config
bench --site mysite set-config developer_mode 1
bench --site mysite set-config maintenance_mode 1
# View current config
bench --site mysite show-config---
Configuration: site_config.json
Per-site config at sites/<sitename>/site_config.json.
Mandatory Keys
| Key | Purpose |
|---|---|
db_type | mariadb or postgres |
db_name | Database name |
db_password | Database password |
Important Optional Keys
| Key | Purpose |
|---|---|
admin_password | Administrator initial password |
host_name | Full site URL (with protocol) |
install_apps | Apps to install on restore/reinstall |
allow_cors | CORS origins ("*", URL, or array) |
max_file_size | Upload limit (default: 10MB) |
mute_emails | Disable all outgoing email |
logging | Debug level (0-2, level 2 shows SQL queries) |
Environment Variable Overrides
Environment variables override config files. Key mappings: FRAPPE_REDIS_QUEUE, FRAPPE_REDIS_CACHE, FRAPPE_DB_HOST, FRAPPE_DB_PORT, FRAPPE_DB_NAME, FRAPPE_DB_PASSWORD.
Priority: Environment Variable > site_config.json > common_site_config.json > Default
---
Production Setup
sudo bench setup production frappe-user # nginx + supervisor + fail2ban
bench setup lets-encrypt mysite.example.com # SSL
sudo bench restart # Restart services
bench disable-production # Back to development---
Version Differences
| Feature | V14 | V15 | V16 |
|---|---|---|---|
| bench init | Yes | Yes | Yes |
| Scheduler tick interval | ~240s | ~240s | 60s |
db_user config (separate) | No | No | Yes |
console --autoreload | No | Yes | Yes |
trim-tables command | No | Yes | Yes |
trim-database command | No | Yes | Yes |
request command | No | Yes | Yes |
| Gettext translations | No | No | Yes |
---
Reference Files
| File | Contents |
|---|---|
| commands.md | Full command reference with all options |
| examples.md | Common workflow examples |
| custom-commands.md | Creating custom bench CLI commands with Click |
| anti-patterns.md | Common bench mistakes and fixes |
Bench Anti-Patterns
Anti-Pattern 1: Running Bench as Root
# WRONG — creates permission issues, security risk
sudo bench start
sudo bench new-site mysite.localhostFix: ALWAYS run bench as a dedicated user (e.g., frappe):
# Create dedicated user
sudo adduser frappe
# Run all bench commands as frappe user
su - frappe
bench init myprojectAnti-Pattern 2: Skipping Migrate After Code Changes
# WRONG — schema out of sync, runtime errors
git pull origin main
bench build # builds assets but does NOT sync schema
# Missing: bench --site mysite migrateFix: ALWAYS migrate after pulling code:
git pull origin main
bench --site mysite migrate # Sync schema + run patches
bench build --app myapp # Rebuild assetsAnti-Pattern 3: Using bench update --reset in Production
# WRONG — destroys ALL local changes without warning
bench update --resetFix: Understand what --reset does (git reset --hard). In production:
# Check for local changes first
cd apps/erpnext && git status
# If you have intentional patches, stash them
git stash
# Then update
bench update
# Re-apply patches
git stash popAnti-Pattern 4: Not Setting --frappe-branch on bench init
# WRONG — gets latest develop branch (unstable)
bench init myprojectFix: ALWAYS specify the Frappe version:
bench init myproject --frappe-branch version-15Anti-Pattern 5: Manually Deleting App Directories
# WRONG — leaves orphaned data in sites, broken references
rm -rf apps/custom_appFix: Use proper uninstall sequence:
# First uninstall from all sites
bench --site mysite uninstall-app custom_app
# Then remove from bench
bench remove-app custom_appAnti-Pattern 6: Editing site_config.json by Hand Without Validation
# WRONG — typos, invalid JSON, missing commas
vim sites/mysite/site_config.jsonFix: Use bench commands for config changes:
bench --site mysite set-config developer_mode 1
bench --site mysite show-config # VerifyIf you must edit manually, ALWAYS validate JSON before saving.
Anti-Pattern 7: No Backup Before Destructive Operations
# WRONG — no recovery if something goes wrong
bench --site production.example.com reinstallFix: ALWAYS backup first:
bench --site production.example.com backup
bench --site production.example.com reinstall # Now safe to proceedAnti-Pattern 8: Running bench update During Business Hours
# WRONG — causes downtime, locks database during migration
bench update # at 2:00 PM on a MondayFix: Schedule updates during off-hours:
# 1. Put in maintenance mode
bench --site mysite set-maintenance-mode 1
# 2. Wait for pending jobs to complete
bench --site mysite ready-for-migration
# 3. Update
bench update
# 4. Verify
bench doctor
# 5. Remove maintenance mode
bench --site mysite set-maintenance-mode 0Anti-Pattern 9: Too Few Gunicorn Workers
// WRONG — single worker blocks all requests
{ "gunicorn_workers": 1 }Fix: Set workers based on CPU cores:
// Formula: (2 * CPU_cores) + 1
// For 4-core server:
{ "gunicorn_workers": 9 }Minimum is ALWAYS 2. NEVER set to 1 in production.
Anti-Pattern 10: Ignoring bench doctor Warnings
bench doctor
# Output: "scheduler disabled" or "workers not running"
# Ignored...Fix: ALWAYS investigate bench doctor output:
bench doctor
# If scheduler disabled:
bench --site mysite scheduler enable
# If workers not running:
sudo supervisorctl status
sudo supervisorctl restart all
# If jobs stuck:
bench show-pending-jobs
bench --site mysite purge-jobsAnti-Pattern 11: Not Testing Restore
# WRONG — assuming backups work without testing
bench --site mysite backup # and never testing restoreFix: Periodically test restore on a staging site:
# Create test site
bench new-site test-restore.localhost --admin-password admin
# Restore production backup
bench --site test-restore.localhost restore /path/to/backup.sql.gz
# Verify data integrity
bench --site test-restore.localhost console
# >>> frappe.db.count("Sales Invoice")
# Clean up
bench drop-site test-restore.localhost --forceBench Commands — Full Reference
General Commands
| Command | Description |
|---|---|
bench init [bench-name] | Create new bench instance |
bench init [name] --frappe-branch version-15 | Create bench with specific Frappe version |
bench init [name] --python python3.11 | Create bench with specific Python |
bench --version | Display bench version |
bench version | Show version of all installed apps |
bench version -f | Show version with branch and commit info |
bench src | Display bench repo directory |
bench start | Start development server (Procfile) |
bench serve | Start web server only |
bench restart | Restart production services (supervisor/systemd) |
bench update | Pull, migrate, build, restart |
bench update --reset | Reset to upstream (destroys local changes) |
bench update --no-build | Skip asset build step |
bench update --no-backup | Skip backup step |
bench update --pull --app [app] | Update specific app only |
bench --help | List all commands |
bench [command] --help | Help for specific command |
Site Commands
| Command | Description |
|---|---|
bench new-site [site] | Create new site |
bench new-site [site] --admin-password [pw] | Create site with password |
bench new-site [site] --install-app [app] | Create site and install app |
bench new-site [site] --db-name [name] | Create site with specific database name |
bench new-site [site] --mariadb-root-password [pw] | Provide MariaDB root password |
bench use [site] | Set default site |
bench drop-site [site] --force | Delete site and database |
bench --site [site] reinstall | Fresh reinstall (deletes all data) |
bench --site [site] browse | Open site in browser |
bench --site [site] browse --user [email] | Open with auto-login |
bench --site [site] add-to-hosts | Add site to /etc/hosts |
App Commands
| Command | Description |
|---|---|
bench get-app [app/url] | Download app from repository |
bench get-app [url] --branch [branch] | Download specific branch |
bench --site [site] install-app [app] | Install app on site |
bench --site [site] uninstall-app [app] | Remove app and its data |
bench --site [site] list-apps | List installed apps |
bench remove-app [app] | Remove app from bench |
bench new-app [app-name] | Create new app scaffold |
bench switch-to-branch [branch] [apps...] | Switch apps to branch |
bench exclude-app [app] | Exclude from updates |
bench include-app [app] | Re-include in updates |
Migration and Build
| Command | Description |
|---|---|
bench --site [site] migrate | Run patches, sync schema, rebuild |
bench --site [site] ready-for-migration | Check for pending jobs |
bench --site all migrate | Migrate all sites |
bench build | Build assets for all apps |
bench build --app [app] | Build assets for specific app |
bench build --production | Production build with minification |
bench watch | Watch and rebuild on file changes |
bench --site [site] clear-cache | Clear all caches |
bench clear-website-cache | Clear website cache |
Backup and Restore
| Command | Description |
|---|---|
bench --site [site] backup | Backup database + files |
bench --site [site] backup --backup-encryption-key [key] | Encrypted backup |
bench backup-all-sites | Backup all sites |
bench --site [site] restore [path] | Restore from .sql.gz |
bench --site [site] restore [path] --with-public-files [tar] | Restore with public files |
bench --site [site] restore [path] --with-private-files [tar] | Restore with private files |
bench --site [site] partial-restore [path] | Partial restore to existing site |
Scheduler and Jobs
| Command | Description |
|---|---|
bench --site [site] scheduler enable | Enable scheduler |
bench --site [site] scheduler disable | Disable scheduler |
bench --site [site] scheduler pause | Pause scheduler |
bench --site [site] scheduler resume | Resume scheduler |
bench doctor | Scheduler diagnostics for all sites |
bench show-pending-jobs | Display queued background jobs |
bench --site [site] purge-jobs | Remove pending periodic tasks |
bench --site [site] purge-jobs --event [name] | Purge specific event |
bench --site [site] trigger-scheduler-event [event] | Manually trigger event |
bench --site [site] set-maintenance-mode [0/1] | Toggle maintenance mode |
bench worker --queue [queue] | Start specific queue worker |
bench schedule | Start scheduler process |
Console and Debugging
| Command | Description |
|---|---|
bench --site [site] console | IPython console with Frappe loaded |
bench --site [site] console --autoreload | Console with auto-reload |
bench --site [site] mariadb | MariaDB interactive console |
bench --site [site] postgres | PostgreSQL interactive console |
bench db-console | Database console (auto-detects DB type) |
bench --site [site] jupyter | Start Jupyter Notebook |
bench execute [method] | Execute Python method |
bench execute [method] --args '[...]' | Execute with positional args |
bench execute [method] --kwargs '{...}' | Execute with keyword args |
bench --site [site] request [method] [path] | Authenticated HTTP request |
bench --site [site] start-recording | Start Frappe Recorder |
bench --site [site] stop-recording | Stop Frappe Recorder |
bench ngrok | Create shareable URL for local site |
Configuration
| Command | Description |
|---|---|
bench --site [site] set-config [key] [value] | Set site config value |
bench config set-common-config -c [key] [value] | Set common config value |
bench config remove-common-config [key] | Remove common config key |
bench --site [site] show-config | Display site configuration |
bench config dns_multitenant on/off | Toggle DNS multi-tenancy |
bench config restart_supervisor_on_update on/off | Auto-restart supervisor |
bench config restart_systemd_on_update on/off | Auto-restart systemd |
bench config http_timeout [seconds] | Set HTTP timeout |
bench set-nginx-port [site] [port] | Set site port for nginx |
User Management
| Command | Description |
|---|---|
bench --site [site] add-system-manager [email] | Add system manager |
bench --site [site] add-user [email] --roles [roles] | Add user with roles |
bench --site [site] disable-user [email] | Disable user account |
bench --site [site] set-password [user] [password] | Set user password |
bench --site [site] set-admin-password [password] | Set Administrator password |
bench --site [site] destroy-all-sessions | Force logout all users |
Data Import/Export
| Command | Description |
|---|---|
bench --site [site] data-import | Import from CSV/XLSX |
bench --site [site] import-csv [path] | Import via CSV |
bench --site [site] import-doc [path] | Import from JSON files |
bench --site [site] export-csv [doctype] | Export DocType data to CSV |
bench --site [site] export-doc [doctype] [name] | Export single document |
bench --site [site] export-json [doctype] [name] | Export as JSON |
bench --site [site] export-fixtures | Export fixtures to app |
bench --site [site] export-fixtures --app [app] | Export fixtures for specific app |
bench --site [site] bulk-rename [csv_path] | Bulk rename from CSV |
Setup Commands
| Command | Description |
|---|---|
bench setup production [user] | Full production setup |
bench setup nginx | Generate nginx configuration |
bench setup supervisor | Generate supervisor configuration |
bench setup redis | Generate Redis configuration |
bench setup lets-encrypt [site] | Configure SSL certificate |
bench setup env | Configure Python virtual environment |
bench setup requirements | Install Python and Node dependencies |
bench setup add-domain [domain] --site [site] | Add custom domain |
bench disable-production | Disable production setup |
Database Maintenance
| Command | Description |
|---|---|
bench --site [site] transform-database --tables [list] | Modify table engine/format |
bench trim-tables | Remove orphaned database columns |
bench trim-database | Delete ghost tables from deleted DocTypes |
bench --site [site] reload-doctype [doctype] | Reload DocType schema |
bench --site [site] reload-doc [doctype] [name] | Reload specific document |
bench --site [site] reset-perms | Reset permissions to defaults |
bench --site [site] build-search-index | Rebuild full-text search index |
bench rebuild-global-search | Rebuild global help search |
Testing
| Command | Description |
|---|---|
bench --site [site] run-tests | Run Python unit tests |
bench --site [site] run-tests --app [app] | Run tests for specific app |
bench --site [site] run-tests --doctype [doctype] | Run tests for DocType |
bench run-parallel-tests | Run tests in parallel (CI) |
bench run-ui-tests | Run Cypress UI tests |
Translation (Gettext/PO)
| Command | Description |
|---|---|
bench generate-pot-file | Create translation template |
bench create-po-file [app] [lang] | Create new PO file |
bench update-po-files | Sync PO files with POT |
bench compile-po-to-mo | Compile PO to binary MO |
bench migrate-csv-to-po [app] | Convert CSV to PO format |
Misc
| Command | Description |
|---|---|
bench --site [site] run-patch execute:[code] | Run one-off Python patch |
bench --site [site] publish-realtime [event] [data] | Publish realtime event |
bench migrate-to [provider] | Migrate to hosting provider |
bench migrate-env [python-version] | Migrate virtual environment |
Custom Bench Commands
Overview
Frappe apps can register custom CLI commands that run via bench. Commands use the Click framework and are auto-discovered by bench from your app's commands module.
---
Step 1: Create the Commands Module
Place commands in your app at one of these locations:
# Single file
frappe-bench/apps/my_app/my_app/commands.py
# Or as a package
frappe-bench/apps/my_app/my_app/commands/__init__.pyBench automatically discovers commands.py (or commands/__init__.py) in every installed app. No hooks.py entry is required for basic discovery.
Optional: Explicit Registration via hooks.py
If your commands live outside the default commands module, register them explicitly:
# hooks.py
commands = [
"my_app.custom_cli.commands"
]This points bench to a custom module path containing the commands list.
---
Step 2: Write Click Commands
Minimal Command (No Site Context)
# my_app/commands.py
import click
@click.command("hello")
def hello():
"""Say hello from my_app."""
click.echo("Hello from my_app!")
commands = [hello]Usage: bench hello
Command With Site Context (Most Common)
Most commands need access to the Frappe database. Use pass_context and get_site from frappe.commands:
# my_app/commands.py
import click
import frappe
from frappe.commands import pass_context, get_site
@click.command("sync-inventory")
@click.option("--warehouse", help="Warehouse name to sync")
@click.option("--dry-run", is_flag=True, help="Preview without changes")
@pass_context
def sync_inventory(context, warehouse=None, dry_run=False):
"""Sync inventory from external system."""
site = get_site(context)
frappe.init(site=site)
frappe.connect()
try:
# Your logic here
filters = {"warehouse": warehouse} if warehouse else {}
items = frappe.get_all("Item", filters=filters, fields=["name", "item_code"])
for item in items:
if not dry_run:
# perform sync
pass
click.echo(f"{'[DRY RUN] ' if dry_run else ''}Synced: {item.item_code}")
except Exception as e:
click.echo(f"Error: {e}", err=True)
raise SystemExit(1)
finally:
frappe.destroy()
commands = [sync_inventory]Usage: bench --site mysite sync-inventory --warehouse "Main" --dry-run
---
Step 3: The Site Context Pattern
CRITICAL: ALWAYS Follow This Pattern
When your command needs database access, you MUST use the init/connect/destroy lifecycle:
@pass_context
def my_command(context, **kwargs):
site = get_site(context)
frappe.init(site=site)
frappe.connect()
try:
# ALL your logic goes here
pass
finally:
frappe.destroy()What Each Step Does
| Step | Purpose |
|---|---|
get_site(context) | Extracts site name from --site flag; raises SiteNotSpecifiedError if missing |
frappe.init(site) | Loads site config, sets up module paths |
frappe.connect() | Opens database connection |
frappe.destroy() | Closes DB connection, cleans up thread locals |
Rules
- ALWAYS call
frappe.destroy()in afinallyblock — leaked connections cause pool exhaustion - ALWAYS use
get_site(context)— NEVER hardcode site names - NEVER assume
frappe.dbis available without callingfrappe.connect()first - NEVER call
frappe.init()more than once per command invocation
---
Common Command Patterns
Data Migration Command
@click.command("migrate-legacy-data")
@click.option("--batch-size", default=100, help="Records per batch")
@click.option("--skip-existing", is_flag=True)
@pass_context
def migrate_legacy_data(context, batch_size=100, skip_existing=False):
"""Migrate data from legacy fields to new structure."""
site = get_site(context)
frappe.init(site=site)
frappe.connect()
try:
records = frappe.get_all(
"Sales Invoice",
filters={"custom_legacy_id": ["is", "set"]},
fields=["name", "custom_legacy_id"],
limit_page_length=0
)
total = len(records)
for i in range(0, total, batch_size):
batch = records[i:i + batch_size]
for record in batch:
if skip_existing and frappe.db.exists("New DocType", record.custom_legacy_id):
continue
# migration logic here
pass
frappe.db.commit()
click.echo(f"Processed {min(i + batch_size, total)}/{total}")
click.echo(f"Migration complete: {total} records processed")
finally:
frappe.destroy()Bulk Operation Command
@click.command("bulk-update-status")
@click.argument("doctype")
@click.argument("status")
@click.option("--filters", help="JSON filters string")
@pass_context
def bulk_update_status(context, doctype, status, filters=None):
"""Bulk update workflow status for documents."""
import json
site = get_site(context)
frappe.init(site=site)
frappe.connect()
try:
filter_dict = json.loads(filters) if filters else {}
docs = frappe.get_all(doctype, filters=filter_dict, pluck="name")
for name in docs:
doc = frappe.get_doc(doctype, name)
doc.status = status
doc.flags.ignore_permissions = True
doc.save()
frappe.db.commit()
click.echo(f"Updated {len(docs)} {doctype} records to '{status}'")
finally:
frappe.destroy()Maintenance / Cleanup Command
@click.command("cleanup-old-logs")
@click.option("--days", default=90, help="Delete logs older than N days")
@click.option("--confirm", is_flag=True, help="Actually delete (default is dry run)")
@pass_context
def cleanup_old_logs(context, days=90, confirm=False):
"""Remove old Error Log and Activity Log entries."""
from frappe.utils import add_days, now_datetime
site = get_site(context)
frappe.init(site=site)
frappe.connect()
try:
cutoff = add_days(now_datetime(), -days)
for doctype in ["Error Log", "Activity Log", "Scheduled Job Log"]:
count = frappe.db.count(doctype, {"creation": ["<", cutoff]})
if confirm:
frappe.db.delete(doctype, {"creation": ["<", cutoff]})
frappe.db.commit()
click.echo(f"Deleted {count} {doctype} records older than {days} days")
else:
click.echo(f"[DRY RUN] Would delete {count} {doctype} records older than {days} days")
if not confirm:
click.echo("Pass --confirm to actually delete records")
finally:
frappe.destroy()---
Multiple Commands in a Package
For apps with many commands, organize as a package:
my_app/commands/
├── __init__.py # Aggregates all commands
├── data_commands.py # Data migration commands
└── maintenance.py # Maintenance commands# my_app/commands/__init__.py
from my_app.commands.data_commands import commands as data_commands
from my_app.commands.maintenance import commands as maintenance_commands
commands = data_commands + maintenance_commands# my_app/commands/data_commands.py
import click
from frappe.commands import pass_context, get_site
@click.command("import-data")
@pass_context
def import_data(context):
# ...
pass
commands = [import_data]---
Click Features Reference
Useful Decorators and Types
| Feature | Example | Purpose |
|---|---|---|
@click.option | --limit 100 | Named parameters with defaults |
@click.argument | Positional arg | Required positional parameters |
@click.option(is_flag=True) | --verbose | Boolean flags |
@click.option(type=click.Choice([...])) | --format csv | Constrained choices |
@click.option(type=click.Path(exists=True)) | --file /path | File path validation |
@click.confirmation_option | --yes | Skip confirmation prompt |
click.echo() | Output text | Use instead of print() |
click.secho(..., fg="green") | Colored output | Styled terminal output |
click.progressbar() | Progress bar | Visual progress for long operations |
Progress Bar Example
with click.progressbar(records, label="Processing") as bar:
for record in bar:
# process each record
pass---
Best Practices
1. ALWAYS use frappe.destroy() in finally — prevents connection leaks 2. ALWAYS commit in batches for bulk operations — prevents long-running transactions 3. ALWAYS provide --dry-run flags for destructive operations 4. ALWAYS use click.echo() instead of print() — respects output redirection 5. NEVER import frappe at module level if using pass_context — import inside the function or at top level but only use after frappe.init() 6. NEVER forget to export the commands list — bench silently ignores modules without it 7. ALWAYS add docstrings to commands — they appear in bench --help output 8. ALWAYS handle KeyboardInterrupt gracefully for long-running commands
---
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Command not found | Missing commands list export | Add commands = [my_command] at module level |
SiteNotSpecifiedError | No --site flag passed | Use bench --site mysite my-command |
ImportError on command | App not installed on bench | Run bench get-app and bench install-app |
| DB connection errors | Missing frappe.connect() | Add frappe.init(site) + frappe.connect() before DB access |
| Stale data after command | Missing frappe.db.commit() | ALWAYS commit after write operations |
| Command hangs on exit | Missing frappe.destroy() | ALWAYS call in finally block |
Bench Examples — Common Workflows
Example 1: Fresh Development Setup
# Install bench (one-time)
pip install frappe-bench
# Create bench with Frappe v15
bench init my-erp --frappe-branch version-15
cd my-erp
# Get ERPNext
bench get-app erpnext --branch version-15
# Create site
bench new-site dev.localhost --admin-password admin --install-app erpnext
# Set as default
bench use dev.localhost
# Enable developer mode
bench --site dev.localhost set-config developer_mode 1
# Start development server
bench start
# Site available at http://dev.localhost:8000Example 2: Adding a Custom App
# Create new app
bench new-app custom_erp
# Install on site
bench --site dev.localhost install-app custom_erp
# After making changes to DocTypes/hooks:
bench --site dev.localhost migrate
bench build --app custom_erpExample 3: Production Deployment
# Create bench
bench init /opt/frappe-bench --frappe-branch version-15
cd /opt/frappe-bench
# Get apps
bench get-app erpnext --branch version-15
# Create production site
bench new-site erp.example.com --admin-password SecurePass123 --install-app erpnext
# Setup production (as root or sudo)
sudo bench setup production frappe
# Setup SSL
bench setup lets-encrypt erp.example.com
# Enable scheduler
bench --site erp.example.com scheduler enableExample 4: Multi-Tenant Setup
# Enable DNS multi-tenancy
bench config dns_multitenant on
# Create multiple sites
bench new-site company-a.example.com --admin-password pass1 --install-app erpnext
bench new-site company-b.example.com --admin-password pass2 --install-app erpnext
# Regenerate nginx config (includes all sites)
bench setup nginx
sudo service nginx reload
# Each site is now accessible via its hostnameExample 5: Backup and Restore to New Site
# Create backup
bench --site production.example.com backup
# Output:
# Database: sites/production.example.com/private/backups/20240115_120000-production-database.sql.gz
# Files: sites/production.example.com/private/backups/20240115_120000-production-files.tar
# Private: sites/production.example.com/private/backups/20240115_120000-production-private-files.tar
# Create new site for restore
bench new-site staging.example.com --admin-password admin
# Restore
bench --site staging.example.com restore \
sites/production.example.com/private/backups/20240115_120000-production-database.sql.gz \
--with-public-files sites/production.example.com/private/backups/20240115_120000-production-files.tar \
--with-private-files sites/production.example.com/private/backups/20240115_120000-production-private-files.tar
# Run migrations (in case versions differ)
bench --site staging.example.com migrateExample 6: Debugging with Console
bench --site dev.localhost console
# In console:
>>> import frappe
# Query documents
>>> frappe.get_all("Sales Invoice", filters={"status": "Unpaid"}, limit=5)
# Get specific document
>>> doc = frappe.get_doc("Sales Invoice", "INV-001")
>>> doc.grand_total
# Direct SQL
>>> frappe.db.sql("SELECT COUNT(*) FROM `tabSales Invoice` WHERE status='Paid'")
# Test a method
>>> from myapp.api import process_invoice
>>> process_invoice("INV-001")
# Commit changes (if needed)
>>> frappe.db.commit()Example 7: Updating Production
# Put site in maintenance (optional)
bench --site erp.example.com set-maintenance-mode 1
# Check no pending jobs
bench --site erp.example.com ready-for-migration
# Update
bench update
# Verify
bench --site erp.example.com list-apps
bench doctor
# Remove maintenance mode
bench --site erp.example.com set-maintenance-mode 0Example 8: common_site_config.json for Production
{
"background_workers": 4,
"dns_multitenant": true,
"gunicorn_workers": 9,
"gunicorn_max_requests": 5000,
"redis_cache": "redis://localhost:13000",
"redis_queue": "redis://localhost:11000",
"redis_socketio": "redis://localhost:13000",
"restart_supervisor_on_update": true,
"serve_default_site": true,
"scheduler_tick_interval": 60,
"socketio_port": 9000,
"webserver_port": 8000
}Rule of thumb for gunicorn_workers: (2 * CPU_cores) + 1
Example 9: Scheduler Troubleshooting
# Check overall health
bench doctor
# Output shows: scheduler status, active workers, queue lengths
# View pending jobs
bench show-pending-jobs
# If scheduler is stuck:
bench --site mysite scheduler disable
bench --site mysite purge-jobs
bench --site mysite scheduler enable
# Test a scheduler task manually
bench --site mysite trigger-scheduler-event hourly
# Or execute directly
bench --site mysite execute myapp.tasks.daily_cleanupExample 10: Database Maintenance
# Remove orphaned columns (safe — only removes columns for deleted fields)
bench --site mysite trim-tables
# Remove ghost tables from deleted DocTypes
bench --site mysite trim-database
# Reset all permissions to default
bench --site mysite reset-perms
# Rebuild search index
bench --site mysite build-search-index