ERPNext custom app development means building your business logic as a separate Frappe app installed alongside ERPNext — with its own DocTypes, scripts, and hooks — instead of editing ERPNext's core files. Because ERPNext is itself just a Frappe app, your app is a first-class citizen with the same powers, and it survives every version upgrade.
Short answer
A custom app is a Python package created with Frappe's bench tool and installed on your ERPNext site. It holds your custom DocTypes, server and client scripts, hooks, and fixtures in one versioned Git repository. Built this way, your logic extends ERPNext cleanly and carries across upgrades — no core edits, no lock-in, no breakage on update.
ERPNext ships with accounting, inventory, manufacturing, HR, and CRM out of the box, but no product fits every business exactly. The question is not whether to extend it, but how — and the how decides whether your system stays maintainable or turns into a fragile mess that breaks on every update. I am Manoj, an ERPNext and Frappe implementation consultant at Mith Tech in Bengaluru, and I have seen both outcomes.
The single biggest predictor of a healthy ERPNext deployment is not how much was customised — it is whether the customisation lives in a proper custom app or was hacked into core.
What is an ERPNext custom app?
A custom app is a standalone Frappe application — a Python package scaffolded with the bench new-app command — installed onto your ERPNext site alongside ERPNext itself. ERPNext is built on the Frappe framework, and ERPNext is technically just one Frappe app among others. Your custom app sits at the same level, with full access to the same APIs, database layer, and UI.
That parity is the whole point. Your app can define new record types, add fields to existing ERPNext DocTypes, run background jobs, expose REST endpoints, and react to events — all from its own folder and its own Git repository. Nothing in ERPNext's source gets touched, so an upgrade replaces ERPNext's files while leaving yours untouched. If you want the commercial framing rather than the technical one, the ERPNext customization services guide covers scope and engagement models.
Skimmable summary: A custom app is a separate Frappe app installed beside ERPNext, with equal access to DocTypes, APIs, and events. Your logic lives in your own repository, so ERPNext upgrades leave it untouched.
What are DocTypes and how do they work?
A DocType is Frappe's core building block — a single definition that describes a table's schema, its form layout, its permissions, and its business logic all at once. Every record type you see in ERPNext, from Sales Invoice to Item to Employee, is a DocType. Creating a DocType generates the database table, the form, the list view, and the API automatically.
For custom development, DocTypes come in two flavours that matter:
| Approach | What it does | Where it lives |
|---|---|---|
| New DocType in your app | Adds a record type ERPNext lacks — e.g. "Site Visit", "AMC Contract" | Your custom app's module folder |
| Custom Field on a core DocType | Adds a field to an existing ERPNext record — e.g. a warranty period on Item | A fixture in your app |
| Property Setter | Changes a property of a core field — label, mandatory, hidden | A fixture in your app |
Each DocType can carry a Python controller file — a class with lifecycle methods like validate, before_save, and on_submit — where your rules live. This is where genuine business logic belongs, versioned in your app and reviewable like any other code.
Skimmable summary: DocTypes define a record's schema, form, permissions, and logic in one place. Your app can add brand-new DocTypes, or attach custom fields and property setters to ERPNext's existing ones, each carried as a fixture.
Server scripts vs client scripts — what runs where?
Server scripts are Python that runs on the backend when a document event or an API call fires; client scripts are JavaScript that runs in the user's browser to make the form interactive. The two solve different problems. Server logic enforces rules and touches the database. Client logic reacts to what the user is doing on screen — showing a field, fetching a value, validating before submit.
| Script type | Language | Runs | Good for |
|---|---|---|---|
| Server script | Python | On the server, on doc events or API | Validation, calculations, enforcing rules, writing data |
| Client script | JavaScript | In the browser form | Dynamic fields, live totals, UX hints, pre-submit checks |
| App controller (in code) | Python | On the server, per DocType | Complex, versioned business logic |
| hooks.py handlers | Python | On the server, framework-wide | Reacting to events across DocTypes |
In-desk scripts are convenient, not free
Server Scripts and Client Scripts created inside the ERPNext desk (the admin UI) are stored in the database, not in your app's code. They are fast to write, but they escape Git and code review. For anything important, move the logic into your custom app so it is versioned, testable, and portable.
Skimmable summary: Server scripts run Python on the backend for validation and data rules; client scripts run JavaScript in the browser for interactive forms. In-desk scripts are quick but live in the database — put real logic in your app's code instead.
How do hooks extend ERPNext without touching core?
Hooks are the central configuration mechanism of a Frappe app, declared in a single hooks.py file that tells Frappe how your app plugs into everything else. Hooks let your app intercept document events, override a DocType's controller class, replace a whitelisted method, inject CSS or JavaScript, and schedule background jobs — all without editing one line of ERPNext's source.
The hooks that matter most in day-to-day custom development:
doc_events
Run your Python function whenever any DocType fires an event — validate, on_submit, on_cancel. This is how you attach cross-cutting rules to ERPNext's own records from outside.
scheduler_events
Register jobs to run hourly, daily, or weekly — nightly syncs, reminders, recalculations. Frappe's scheduler calls your functions on the interval you declare.
fixtures
List the Custom Fields, Property Setters, and other records your app owns so they export to JSON and rebuild on bench migrate.
override methods
Override a DocType's controller class or a whitelisted method to change core behaviour surgically — used sparingly, and always in your app.
Because everything routes through hooks.py, a reviewer can read one file and understand every way your app touches ERPNext. That transparency is what keeps a custom app auditable years later. Our approach to this is on the Frappe and ERPNext platform page.
Skimmable summary: hooks.py is the one file that declares how your app plugs into ERPNext — reacting to document events, scheduling jobs, carrying fixtures, and overriding methods. It extends core behaviour without ever editing core files.
How do you build a custom app on ERPNext?
Building a custom app follows a repeatable path with Frappe's bench tool: scaffold the app, install it on your site, add DocTypes and logic, then export your customisations as fixtures so they travel with the code. The sequence below is the backbone every clean Frappe project follows.
Scaffold the app
Run bench new-app your_app to generate the package skeleton — modules, hooks.py, and the folder structure Frappe expects. Initialise it as a Git repository from day one.
Install it on the site
Run bench --site yoursite install-app your_app to register the app on your ERPNext site. It now loads alongside ERPNext and can define DocTypes and respond to hooks.
Add DocTypes and logic
Create your new DocTypes inside the app, write controller methods for business rules, and wire up doc_events in hooks.py for cross-cutting behaviour. Keep meaningful logic in code, not in-desk scripts.
Capture customisations as fixtures
For custom fields and property setters on ERPNext's own DocTypes, list them under fixtures in hooks.py and run bench export-fixtures. This writes them to JSON in your app so they rebuild anywhere.
Migrate and test on staging
Run bench migrate to apply everything, then test on a staging copy before it ever reaches production. Staging-first is the habit that catches upgrade surprises early.
If you do not have an in-house developer for this, the guide to hiring an ERPNext developer in India covers what skills to look for.
Skimmable summary: Scaffold with bench new-app, install on your site, add DocTypes and controller logic, export custom fields as fixtures, then migrate and test on staging. Git from day one, staging before production.
How do you keep a custom app upgrade-safe?
Upgrade-safety comes from discipline, not a single setting: keep every customisation inside your app, capture field changes as fixtures, and test on staging before any bench update. The reason matters — a version upgrade overwrites ERPNext's files. Anything you changed in core is lost, and a system built by patching core breaks on every update, which pushes teams to skip updates and end up on unsupported, insecure software.
Three habits keep an app clean over time. First, prefer fixtures and hooks over in-desk scripts so your logic lives in Git. Second, audit periodically — list every custom field, server script, and hook, and delete the ones whose requirement no longer exists, because dead customisation is pure upgrade risk. Third, never skip staging; a full bench migrate on a copy of production surfaces conflicts before your users feel them. Built this way, customisations carry cleanly across major releases — the same discipline that made the ERPNext v15 to v16 upgrade manageable for well-structured deployments.
Skimmable summary: Keep everything in your app, capture field changes as fixtures, audit and delete dead customisations, and always run bench migrate on staging first. Core edits are lost on upgrade — your app is not.
When should you build a custom app instead of configuring?
Reach for a custom app only when configuration genuinely cannot meet the need — a new record type ERPNext does not ship, a scheduled background job, a bespoke integration, an event-driven rule across DocTypes, or logic you want under version control. Most requirements, though, are configuration: custom fields, workflows, print formats, and dashboards need no app and carry no upgrade risk.
Building an app too early is the expensive mistake — it adds maintenance and testing overhead for something the platform could have configured. The honest test I use: if it can be a Custom Field, a Workflow, or a Property Setter, it should be. Reserve the custom app for logic, integrations, and record types that only code can deliver. See the ERPNext product overview for where ERPNext ends and custom work begins.
Skimmable summary: Build a custom app for new DocTypes, scheduled jobs, integrations, event-driven rules, and versioned logic. For fields, workflows, and print formats, configure instead — coding too early adds cost without benefit.
Frequently asked questions
Is ERPNext free to develop custom apps on?
Yes. ERPNext is free and open source under the GPLv3 licence, and the Frappe framework it runs on is open source too. There is no licence fee for extending it and no per-developer charge to build a custom app. You pay for development effort — the time to design DocTypes, write logic, and test — not for the right to customise. Any hosting or edition pricing varies by provider, so check the vendor's official page.
What is the difference between a custom app and a custom script?
A custom app is a full Python package installed alongside ERPNext, holding DocTypes, controllers, hooks, and fixtures in your own Git repository. A custom script — a Server Script or Client Script created in the ERPNext desk — is a small piece of logic stored in the database. Scripts are quick for one-off tweaks; apps are the right home for anything you want versioned, tested, and portable across environments.
Do custom apps survive ERPNext upgrades?
Yes, when built correctly. Because a custom app lives in its own files and repository, an ERPNext upgrade replaces ERPNext's code while leaving your app untouched. The failure mode is editing ERPNext core directly — those changes are overwritten on every update. Keep customisations in your app, capture field changes as fixtures, and test on staging before upgrading, and your logic carries across major versions cleanly.
What version of ERPNext should I build on in 2026?
ERPNext v16 reached its production-ready release in December 2025 and is the current major line, built on Frappe framework v16. New projects in 2026 should generally target v16 to get the latest features and the longest support runway. The exact stable point release moves regularly, so confirm the current version on the official ERPNext release page before you start a build.
Can a custom app talk to other software?
Yes. Frappe exposes a REST API automatically for every DocType, and your custom app can add its own whitelisted endpoints, call external APIs, and run scheduled sync jobs through scheduler_events. This makes integrations — payment gateways, messaging tools, marketplaces, banks — a natural fit for a custom app rather than core edits. Building them inside your app keeps the integration versioned and upgrade-safe.
Who owns the custom app that is built for me?
You do, when the work is done right. Because ERPNext and Frappe are open source, your custom app and all its code should be delivered in a Git repository you own. That means you can maintain it, extend it, or move to another provider at any time with no lock-in. Confirm this ownership in writing before any development starts.
About the author
Manoj is an ERPNext and Frappe implementation consultant at Mith Tech in Bengaluru. He builds upgrade-safe custom apps — DocTypes, hooks, server and client logic, and integrations — for Indian businesses, with all code delivered in a repository the client owns.
Need a custom Frappe app built the right way?
Mith Tech designs and builds upgrade-safe ERPNext custom apps for Indian businesses — DocTypes, hooks, scripts, and integrations, all in a versioned repository you own. Read how to hire an ERPNext developer or see our Frappe and ERPNext platform work.