Bench Operations
Back up and move sites, switch app branches and major versions, run bulk fixes in the bench console, and repair database schema drift.
On this page
This is a working reference for the bench tasks that come up again and again on Frappe and ERPNext servers: moving a site between servers, pointing apps at a fork or another branch, upgrading major versions, cleaning up data from the bench console, and fixing tables that have drifted from their DocType. Run every command from the bench directory (for example /home/frappe/frappe-bench) as the bench user, not as root.
Back Up and Restore a Site
Use this to move a site to another server, or to clone production into a test bench.
Take the Backup
On the source server:
bench --site <SITE_NAME> backup --with-filesThis writes three files to sites/<SITE_NAME>/private/backups/: the database dump (*-database.sql.gz), the public files (*-files.tar), and the private files (*-private-files.tar). Add --compress to gzip the file archives.
If the site has backup encryption enabled, the backups are encrypted with the encryption_key from the source site's site_config.json. Copy that key along with the files, because the destination can't decrypt them without it.
Copy the backup files to the destination server with scp, rsync, or similar.
Restore on the Destination
Make sure the destination bench has every app the source site uses, on the same major version:
bench get-app --branch version-15 erpnextIf the site doesn't exist on the destination yet, create it first:
bench new-site <SITE_NAME>Restore the database and files:
bench --site <SITE_NAME> restore \ --with-public-files <PATH_TO>/<BACKUP_PREFIX>-files.tar \ --with-private-files <PATH_TO>/<BACKUP_PREFIX>-private-files.tar \ <PATH_TO>/<BACKUP_PREFIX>-database.sql.gzAdd
--encryption-key <ENCRYPTION_KEY>if the backup is encrypted and the destination site has a different key.Run migrations so the schema matches the code on the destination:
bench --site <SITE_NAME> migrate
Warning
bench restore drops and recreates the site's database. Everything currently in <SITE_NAME> on the destination is replaced. Take a backup of the destination site first if it holds anything you might need. --force additionally skips the downgrade and SQL validation checks, so only use it when you know why the check is failing.
Tip
bench restore also looks for the file in sites/<SITE_NAME>/private/backups/ and in the bench directory, so you can pass just the filename if you copied the backups there.
Switch App Remotes and Branches
Point an App at a Fork
bench remote-set-url changes the upstream remote of an installed app. It takes a single argument, the Git URL, and works out the app from the repository name at the end of the URL:
bench remote-set-url https://github.com/<GITHUB_ORG>/<APP_NAME>.gitThe repository name must match the app's directory under apps/. If it doesn't (for example the repository is My-App but the app is my_app), set the remote with Git instead:
git -C apps/<APP_NAME> remote set-url upstream https://github.com/<GITHUB_ORG>/<REPO_NAME>.gitTo point an app back at the official Frappe repository:
bench remote-reset-url <APP_NAME>Run bench remote-urls to list the remote of every app in the bench.
Switch Branches
bench switch-to-branch fetches from upstream and checks out the branch in each app you list. Without an app list it switches every app in the bench.
bench switch-to-branch <BRANCH> <APP_NAME>
bench update --patchAlways follow a branch switch with bench update --patch (which runs bench migrate on every site) so the database schema matches the new code.
Warning
switch-to-branch runs git checkout -f, which throws away uncommitted changes in the app. Commit or stash local work first.
Upgrade to a New Major Version
Bench refuses to cross a major version unless you pass --upgrade. With it, bench also reinstalls Python requirements, backs up every site, runs patches, and rebuilds assets.
Back up every site:
bench backup-all-sitesCheck the system requirements of the target version and upgrade them first. Frappe v15 needs Python 3.10 or newer and Node 18 or newer. Frappe v16 needs Python 3.14 and Node 24. If the Python version changes, rebuild the virtual environment:
bench migrate-env python3.14Switch Frappe and every app that follows Frappe's version branches:
bench switch-to-branch version-16 frappe erpnext hrms payments --upgradeReinstall requirements, run patches, and rebuild:
bench setup requirements bench update --patch --no-backup bench buildPull the latest commits on the new branches, resetting any leftover local differences:
bench update --reset
Check that every app actually has the target branch before you start (git -C apps/<APP_NAME> ls-remote --heads upstream). Custom apps often only have main or develop; switch those separately.
Warning
bench update --reset hard-resets every app to its upstream branch, discarding local commits and uncommitted changes. Skip it on benches where you edit app code in place.
Tip
Recent bench releases use uv for Python packages by default. If bench setup requirements fails on one app, you can reinstall it by hand with uv pip install --python ./env/bin/python -e ./apps/<APP_NAME> (or ./env/bin/pip install -e ./apps/<APP_NAME> when BENCH_DISABLE_UV=1).
Switch to Develop
Use this only on development benches:
bench switch-to-branch develop frappe erpnext --upgrade
bench setup requirements
bench update --reset
bench buildRun Fixes in the Bench Console
The bench console is an IPython shell with the site already connected:
bench --site <SITE_NAME> consoleChanges made with frappe.db.* or doc.save() are not permanent until you call frappe.db.commit(). Exit with exit or Ctrl+D; uncommitted changes are rolled back.
Warning
Everything in this section changes or deletes production data, and most of it bypasses the normal checks that ERPNext runs through the UI. Take a backup with bench --site <SITE_NAME> backup first, and try the script on a copy of the site before running it on the real one.
Update a Field on Many Documents
Loop through the documents and save each one when you need validations and hooks to run:
for name in frappe.get_all("Employee Checkin", filters={"employee": "<EMPLOYEE_ID>"}, pluck="name"):
doc = frappe.get_doc("Employee Checkin", name)
doc.employee_name = "<NEW_EMPLOYEE_NAME>"
doc.save()
frappe.db.commit()When you only need to change column values and don't want hooks or modified timestamps to change, the query builder does it in one statement:
BOMOperation = frappe.qb.DocType("BOM Operation")
(
frappe.qb.update(BOMOperation)
.set(BOMOperation.time_in_mins, 0)
.where(BOMOperation.parenttype == "BOM")
).run()
frappe.db.commit()See Query Builder & PyPika Extensions for more query builder patterns.
Delete a Specific Document
frappe.delete_doc("Stock Entry", "<DOCUMENT_NAME>")
frappe.db.commit()Submitted documents can't be deleted. Cancel them first (frappe.get_doc(doctype, name).cancel()), then delete.
Delete Every Document of a Type
For log doctypes such as Error Log, empty the table directly:
frappe.db.truncate("Error Log")TRUNCATE is DDL, takes effect immediately, and can't be rolled back. Use frappe.db.delete("Error Log") instead if you want a DELETE that waits for frappe.db.commit(). Both remove only the doctype's own table; they skip child tables, comments, versions, and controller hooks, which is fine for logs but not for business documents.
For business documents, delete through the ORM so child rows and linked records are cleaned up:
for doctype in ["<DOCTYPE_1>", "<DOCTYPE_2>"]:
for name in frappe.get_all(doctype, pluck="name"):
if frappe.db.get_value(doctype, name, "docstatus") == 1:
# Mark submitted documents as cancelled so they can be deleted.
# This skips the real cancel logic (ledger reversals and so on).
frappe.db.set_value(doctype, name, "docstatus", 2, update_modified=False)
frappe.delete_doc(doctype, name, force=True)
frappe.db.commit()force=True skips the "is this document linked elsewhere" check.
Warning
Forcing docstatus to 2 and deleting with force=True leaves ledger entries and links behind. Only do this to clear out test or junk data. To wipe all transactions for a company before go-live, use ERPNext's Transaction Deletion Record instead.
Delete Cancelled Stock Transactions
Cancelled stock documents keep their GL and stock ledger rows. To remove them completely, for example to clean up test data before go-live:
for doctype in ["Stock Reconciliation", "Stock Entry", "Delivery Note"]:
for name in frappe.get_all(doctype, filters={"docstatus": 2}, pluck="name"):
for ledger in ["GL Entry", "Stock Ledger Entry", "Repost Item Valuation"]:
frappe.db.delete(ledger, {"voucher_type": doctype, "voucher_no": name})
frappe.delete_doc(doctype, name)
frappe.db.commit()Warning
This deletes ledger history permanently. Never run it on a company with closed accounting periods or audited books.
Revert Cancelled Bank Transactions to Unreconciled
If bank transactions were cancelled by mistake, you can put them back in the reconciliation queue instead of re-importing the statement. Saving with an empty payment_entries table makes the Bank Transaction controller recalculate the allocated and unallocated amounts and set the status back to Unreconciled.
for name in frappe.get_all("Bank Transaction", filters={"docstatus": 2}, pluck="name"):
frappe.db.set_value("Bank Transaction", name, "docstatus", 1, update_modified=False)
doc = frappe.get_doc("Bank Transaction", name)
doc.payment_entries = []
doc.save()
frappe.db.commit()Warning
This changes docstatus directly, which ERPNext never does on its own. Filter it down to the specific transactions you mean to revert (for example by bank_account or date) rather than running it on every cancelled transaction.
Clear a Background Job Queue
To drop every pending job in a queue for all sites on the bench:
bench purge-jobs --queue longAdd --site <SITE_NAME> to only remove that site's jobs, or --event daily to only remove scheduled jobs of that type.
From the console, get_queue builds the correct queue name for you (since v15, queue names in Redis are prefixed with the bench ID, so a bare rq.Queue("long") points at the wrong queue):
from frappe.utils.background_jobs import get_queue
queue = get_queue("long")
queue.count
queue.empty()Emptying a queue doesn't stop jobs that are already running. Stop those from the RQ Job list in the desk, or from the console:
from rq.command import send_stop_job_command
from frappe.utils.background_jobs import get_queue, get_redis_conn, get_workers
for worker in get_workers(get_queue("long")):
job = worker.get_current_job()
if job:
send_stop_job_command(get_redis_conn(), job.id)Repair Database Schema
Recreate or Resync a DocType Table
If a doctype's table is missing, or its columns and indexes no longer match the DocType definition, sync it from the console:
frappe.db.updatedb("<DOCTYPE>")
frappe.db.commit()updatedb creates the table if it doesn't exist and adds or changes columns and indexes to match the DocType. For a standard doctype whose JSON file has changed on disk, frappe.reload_doctype("<DOCTYPE>", force=True) reloads the definition from the file and syncs the table in one step.
Note
updatedb doesn't repair a table that MariaDB itself reports as corrupted. If CHECK TABLE fails, restore the table from a backup, or drop it and let updatedb recreate it empty.
Remove Orphaned Columns
When fields are deleted from a DocType, Frappe leaves their columns in the table. Preview what would be dropped:
bench --site <SITE_NAME> trim-tables --dry-runThen run it for real:
bench --site <SITE_NAME> trim-tablesWarning
trim-tables drops columns and the data in them. It takes a full backup first unless you pass --no-backup; don't skip it.
Sources
- docs.frappe.io/framework/user/en/bench/bench-commands
- docs.frappe.io/framework/user/en/bench/reference/restore
- docs.frappe.io/framework/user/en/bench/reference/trim-tables
- github.com/frappe/bench/blob/develop/bench/commands/update.py
- github.com/frappe/bench/blob/develop/bench/commands/git.py
- github.com/frappe/frappe/blob/version-15/frappe/commands/site.py
- github.com/frappe/frappe/blob/version-15/frappe/utils/background_jobs.py
This article is in the public domain (CC0 1.0), code samples included. Use it however helps you.