Can I add fields and logic to ERPNext without my customizations disappearing every time there is an upgrade? Yes — but only if you understand the line between what ERPNext protects and what it overwrites. Cross that line, and a routine bench migrate silently reverts weeks of work. Stay on the right side, and your customizations survive every version bump cleanly.
Two Types of Customization and Why the Distinction Matters
ERPNext draws a hard boundary between core code and custom code. Core code lives in the Frappe and ERPNext Python packages — it is replaced wholesale when you run bench update. Custom code lives outside that tree, either in the database (for UI customizations) or in your own Frappe app (for logic and doctypes). Only the second category survives upgrades intact.
What gets overwritten on upgrade
- Any edits made directly to files inside
apps/frappe/orapps/erpnext/ - Monkey-patching existing Python classes or functions by modifying the original file
- JavaScript injected directly into core asset files
What survives upgrades
- Custom Fields added via Customize Form in the UI (stored in the database, not in files)
- Custom Doctypes you create (stored in your custom app's folder)
- Server Scripts and Client Scripts created in the Scripts modules
- Hooks defined in your custom app's
hooks.py - Entire custom Frappe apps installed alongside ERPNext
The practical rule is: if you created it through the ERPNext UI or through your own app folder, it persists. If you opened a file in apps/erpnext/ and edited it, that change is on borrowed time.
Adding Fields: Use Customize Form, Not the DocType Editor on Core Types
When you need to add a field to an existing core doctype — say, a custom approval field on Sales Order — never edit the sales_order.json file directly. Instead, navigate to Customize Form, select the doctype, and add your field there. ERPNext stores this as a Custom Field record in the database. It appears and behaves exactly like a native field, it shows in forms and reports, and it is completely unaffected by upgrades to the Sales Order module.
Custom Field best practices
- Prefix custom fields with your company or app abbreviation (e.g.,
custom_approval_stageormyco_region) to avoid collisions with future ERPNext additions - Set appropriate field types — Link fields for relational data, Select for enumerations, Data for free text
- Use "Insert After" to position fields logically in the form layout
- Export your Customize Form settings via Export Customizations so they can be version-controlled and re-applied to staging environments
Custom Doctypes: Building New Data Models the Right Way
When your business needs to track data that does not map to any existing ERPNext doctype — a custom job scheduling system, a supplier evaluation form, a project milestone tracker — create a new Custom Doctype rather than misusing an existing one.
Custom Doctypes created through the UI are stored in the database and persist through upgrades. However, for production-grade work, they belong in a custom Frappe app for two reasons: version control and portability. A custom app lets you track doctype changes in git, deploy consistently across environments, and export the schema cleanly.
Creating a custom Frappe app
- Run
bench new-app my_custom_appon your server - Install it on your site:
bench --site yoursite.com install-app my_custom_app - Create doctypes inside the app using the Frappe UI — they automatically serialize to
my_custom_app/my_custom_app/doctype/ - Add controllers in Python files at the same path for business logic
- Use
hooks.pyin your app to hook into ERPNext events without touching core files
Server Scripts vs Python Controllers: Choosing the Right Tool
ERPNext offers two ways to add business logic: Server Scripts (written in the UI, stored in the database) and Python controllers (written in your app's file system). Each has a legitimate use case.
| Server Scripts | Python Controllers (App) | |
|---|---|---|
| Version control | No (database-stored) | Yes (git) |
| Upgrade safety | Yes | Yes |
| Debugging | Limited (frappe.log_error) | Full Python tooling |
| Performance | Slight overhead (eval) | Native Python speed |
| Access to third-party libraries | Restricted sandbox | Full access |
| Best for | Quick validations, auto-set fields, simple notifications | Complex logic, integrations, scheduled tasks |
Server Scripts are fine for adding a validation that prevents saving a document if a required custom field is empty, or for auto-populating a field based on another. For anything that calls an external API, processes large datasets, or requires robust error handling — write a Python controller in your custom app.
Hooking Into ERPNext Events Without Modifying Core
The hooks.py file in your custom app is the upgrade-safe way to attach logic to ERPNext's document lifecycle events. You are not touching core code — you are registering a listener that ERPNext calls at the appropriate moment.
Commonly used hooks for customization
- doc_events — trigger custom Python functions on
before_save,after_insert,on_submit,on_cancelfor any doctype - scheduler_events — run background tasks on cron-like schedules (hourly, daily, weekly) without touching ERPNext's scheduler
- override_whitelisted_methods — replace a specific API endpoint with your own implementation
- app_include_js / app_include_css — inject custom JavaScript or CSS globally without editing core asset files
- fixtures — export and re-import configuration (roles, custom fields, print formats) as part of your app's deployment
A hook that runs a custom function on Sales Invoice submission looks like a three-line entry in hooks.py. It has no footprint in ERPNext's core. When ERPNext upgrades its sales invoice module, your hook remains completely intact.
Client Scripts for UI Logic
Client Scripts run in the browser and allow you to manipulate form behavior — hide fields based on conditions, filter Link field results, trigger server calls, validate before save. They are stored in the database under Client Script in ERPNext and survive upgrades natively.
For complex client-side requirements, you can also use your app's public/js/ folder and load scripts via app_include_js in hooks — giving you full module-based JavaScript with proper version control.
What to Do Before Every ERPNext Upgrade
Even with perfect upgrade-safe customizations, upgrades need a pre-flight process:
- Pull the upgrade on a staging environment first
- Run your custom app's tests against the new version
- Check the ERPNext release notes for deprecated APIs your code uses
- Verify print formats render correctly — Jinja template behavior occasionally changes
- Run a representative set of business transactions end-to-end
- Only then apply to production, during a low-traffic window
Teams that skip staging upgrades find out about breaking changes in production. The ERPNext changelog is specific and readable — budget 30–60 minutes to review it before every major version upgrade.
If your organisation has accumulated a significant layer of ERPNext customization, having an experienced Frappe development partner like Mexilet Technologies audit your customization architecture can prevent upgrade breakage before it happens rather than after.
Frequently Asked Questions
Can I extend an existing ERPNext doctype's Python class without editing the core file?
Yes, through doc_events hooks in your app's hooks.py. You register a custom function to run before or after the core doctype's method, adding behavior without overriding the original class. For more complex overrides — replacing entire methods — ERPNext also supports override_doctype_class in hooks, which lets you subclass a core controller in your own app and register the override cleanly.
How do I customize ERPNext print formats without risking upgrades?
Create new custom print formats in Print Format under Settings — never modify the default ones. Your custom format is database-stored and upgrade-safe. Export it as a fixture in your custom app so it deploys to new environments automatically. Jinja templating in print formats is powerful enough to replicate virtually any layout requirement.
What happens to my Custom Fields if I reinstall ERPNext from scratch?
Custom Fields stored in the database are tied to your site's database, not the ERPNext codebase. If you restore a database backup to a fresh installation, the Custom Fields come back with it. If you are setting up a new site from scratch (not from backup), you must re-apply them — which is why exporting customizations via fixtures in a custom app is the professional approach. Run bench --site yoursite install-app my_custom_app and all your fixtures re-apply automatically.
Is there a risk that ERPNext adds a core field with the same name as my custom field?
Yes, it has happened. The naming convention prefix (e.g., custom_, myco_) exists specifically to minimize this risk. As of Frappe v14+, ERPNext auto-prefixes custom fields created in Customize Form with custom_ — which is a good default. Check the ERPNext release notes for new field additions in modules you have customized; conflicts are rare but resolvable when caught early on staging.
If you'd rather not build it alone, see our ERP & ERPNext services and business automation solutions.
If you are about to start a customization project and want to make sure you build it correctly from the beginning — not six months later when an upgrade breaks everything — consider starting with a focused paid pilot sprint. Reach out to the Mexilet team to scope a small initial engagement where we review your requirements, prototype the core custom doctypes and hooks, and hand you a pattern you can build on confidently.
