Upgrading ERPNext custom apps for version 16

Custom Frappe apps need code, runtime and permission changes before they run on ERPNext version 16. This page sets out what breaks, the upgrade path, and what drives the effort.

On this page

Why custom ERPNext apps must be upgraded for version 16 before the site moves

If your team runs custom Frappe apps on version 14 or 15, those apps do not come along for free when the site moves to version 16. Custom apps are the main upgrade risk, because Frappe and ERPNext are upgraded as a set and any app that depends on changed framework behavior must be changed and tested in the same window. The core apps and your custom apps are upgraded together, so the version 16 target is set by the whole bench, not by ERPNext alone. Start by inventorying every custom app, its pinned branch, its hooks and its API clients, then decide which ones need code changes before the site is touched.

The supported path runs through the version 15 line. Frappe's supported versions page lists version 14 as end of life from January 31, 2026, with version 15 planned for support until the end of 2027 and version 16 until the end of 2029. A site on version 14 therefore has to move through version 15 before it reaches version 16, and each hop needs its own migrate run and custom app test pass. Confirm the current support status of each version with Frappe before you fix a target, since support windows are published by Frappe and can change.

Because version 16 is supported until the end of 2029 according to that same page, it is the sensible destination for a custom app estate that is planned to run for several years. Treat the custom app work as a scoped project with its own owner, test cases and sign-off, and do not leave it as a side task of the platform upgrade. Ask whoever maintains each app to confirm which framework behaviors it depends on, because that list drives the effort far more than the number of lines of code.

Which version 16 changes break custom apps and what each one requires

This table lists the version 16 changes that affect custom Frappe apps, the impact each one has on existing code, and the action to take before migrating. Read each row against your app's hooks, server code, child tables and API clients, since a change can break an app without any visible error until a specific page or integration runs. The main takeaway is that most of the work is a code review of hooks and API calls, so it should be scoped and tested before any production upgrade.

ChangeImpact on custom appsAction needed
Python 3.14+ and Node.js 24+ minimum dependenciesBenches built on older Python or Node runtimes will not run version 16. Custom apps with pinned packages or native build steps may fail to install.Confirm the runtime versions on your build hosts and images, then rebuild the virtual environment and frontend assets. The Frappe version 16 migration guide sets the minimums.
Desk frontend moved from /app to /deskBookmarks, hard-coded links, redirects, reverse proxy rules and external tools that point to /app will lead to broken or unexpected routes.Search custom code, print formats, email templates, Nginx configuration and integrations for /app paths, and update them to /desk.
has_permission hooks must return True explicitlyA hook that returns None or falls through is no longer treated as granting access, so users may lose access to documents they previously saw.Review every has_permission hook and make each allow path return True explicitly. Test access for each role with a user who should see the record.
Document hooks no longer commit database transactionsCode in hooks such as on_update or on_submit that calls commit will be out of step with the request transaction, which can leave partial data or inconsistent state.Remove commit calls from document hooks and let the framework manage the transaction. Move any work that must be committed separately into a background job.
Item Wise Tax Detail child table replaces the JSON fieldCustom code that reads or writes the item-wise tax JSON field on transactions will return empty or stale values, which affects tax reports and custom print formats.Rewrite reads and writes to use the Item Wise Tax Detail child table. Check that migrated tax totals match the source before go-live, as described in the ERPNext version 16 migration guide.
Several whitelisted methods used by API clients removedExternal systems, mobile apps and integration scripts that call removed endpoints will fail with errors that may surface only in the downstream system.Inventory every API client and log of calls, map each removed method to its documented replacement in the ERPNext version 16 migration guide, and retest each integration end to end.

How each breaking change reaches your code, by layer

Version 16 raises the minimum dependencies to Python 3.14 and NodeJS 24, according to the Frappe version 16 migration guide. Runtime problems rarely look like application bugs. They show up as failed installs, failed builds or import errors when the bench starts, so this layer is usually the first to break and the cheapest to check. Confirm the target runtime versions in the Frappe migration guide before you change anything.

  • Python version pins: Search for python_requires, requires-python, classifiers and CI matrices that still list older interpreters.
  • Node version pins: Check .nvmrc, engines fields in package.json and any CI or Docker base image that builds frontend assets.
  • Dependency wheels: Look for pinned packages that have no wheels for the new interpreter and need a version bump or a source build.
  • Passing test: A clean virtual environment and a clean Node install run bench build for the custom app, and bench --site <site> migrate completes with no import errors.

Record the exact interpreter and Node versions in the build image so that the result can be reproduced later.

What to prepare before changing any custom app code

Custom apps break on version 16 for reasons that are easy to miss in code review, so preparation comes first. The six items below connect: each gives you a restore point, a map of what touches what, or a place to prove the upgrade works before production sees it.

Full backup of database and files

Take a complete database dump and a copy of the site's private and public files before any branch is touched. Store the backup outside the bench that will be migrated, and test one restore on a separate host. Confirm with your team who owns the backup schedule during the upgrade window and where the encryption keys are kept.

Inventory of custom apps and hooks

List every custom app, its branch, and each hook it registers in hooks.py, including has_permission, document events and scheduled jobs. DeployOpen inventories this map from your repositories and a running bench. You provide repository access, the list of sites that load each app, and any apps you have forked from ERPNext or Frappe.

Whitelisted methods used by integrations

Identify every whitelisted method that external systems call, including the ones ERPNext removes in version 16. Each entry should name the caller, the payload, and the owner of the integration. You provide access to integration owners and their logs, since a method that looks unused in code may still run from a scheduled job at a partner.

Test site with production data copies

Restore a recent production backup to a separate version 16 test site and run the migration there first. Use copies of real records with sensitive fields masked where policy requires it. You provide the test environment, a data owner who signs off on the results, and a window long enough for the team to run the business workflows that depend on the custom app.

CI pipeline pinned to Python 3.14 and Node 24

Pin the CI runners to Python 3.14 and Node 24, the minimum dependencies for Frappe version 16, so that tests run against the runtime production will use. Failures that appear only on older interpreters should surface early. Confirm that any build images, linters and dependency lock files in your repositories support these versions before the migration branch is created.

How to port a custom app to version 16 in order

This sequence moves a custom Frappe app from version 15 to version 16 in a fixed order, so each change is tested on its own and the version 15 site stays available as a fallback until the team signs off.

  1. Create a branch for each custom app

    The app owner creates a version 16 working branch in each custom app repository, starting from the commit currently running in production. Keep the version 15 branch untouched so the original code stays available for comparison and rollback. Record the commit hashes of every app in the change ticket. Done when each repository has a named branch, the production commit is tagged or noted, and the team can check out the original code at any time.

  2. Install the app on a version 16 bench

    The developer builds a separate version 16 bench on a development machine, using Python 3.14 or later and Node 24 or later, as the Frappe version 16 migration guide requires. They fetch the matching ERPNext and Frappe branches, add the custom app from its new branch, and install it on a test site. Done when the bench builds without dependency errors and the app appears in the installed apps list of the test site.

  3. Fix runtime errors before changing behavior

    Start the test site, open the affected pages and run the app's background jobs and scheduled tasks while watching the logs. Fix import errors, renamed functions and broken doctype references first, and leave behavior changes for later steps. This separates framework-level breakage from the version 16 changes that follow. Done when the site starts cleanly, the app's doctypes open without errors, and the log shows no new exceptions from the app's own code.

  4. Return True explicitly from has_permission hooks

    The developer reviews every has_permission hook in hooks.py and the functions they call, then makes each path return True explicitly when access is granted, instead of falling through to an implicit result. Test with users of each role to confirm that access is neither wider nor narrower than on version 15. Done when a role-by-role access test on the test site matches the documented permission matrix, and no hook returns an implicit None.

  5. Move commits out of document hooks

    The developer searches the app for database commits inside document lifecycle methods such as validate, before_save, on_update and on_submit, then removes them. Version 16 stops those hooks from committing transactions, so any write that must persist should happen in the normal save flow or in a dedicated, explicitly committed background job. Done when a test save of each affected document rolls back cleanly on error, and the data written by the hook appears only after the document saves.

  6. Update /app references to /desk

    The developer searches the app's Python, JavaScript, templates and configuration for hard-coded /app paths and replaces them with /desk, while using the framework's route helpers wherever possible. Check notification links, email templates, print formats and any redirect in custom pages. Done when a search for /app returns no unintended matches, and links generated by the app open the desk on the test site.

  7. Migrate API calls off removed whitelisted methods

    The developer lists every whitelisted method the app exposes or calls, and compares them with the methods removed in the ERPNext version 16 migration guide. Update calls to supported replacements, and tell the owners of external API clients about any change that affects them. Keep a record of each replaced call. Done when the app and its known integrations call only supported endpoints in tests, and each external client owner has confirmed the new call.

  8. Run bench migrate on a copy, then test and sign off

    The operations team takes a fresh backup of the version 15 production site, restores it to a separate copy, and runs bench migrate there with the version 16 app set. They then run the regression tests, the role-by-role access checks and the key business processes agreed with the process owners. Keep the version 15 bench and the backup until the business signs off. Done when the copy passes the agreed tests, and the written sign-off names the rollback point to retire.

Moving item-wise tax data to the child table

The version 16 migration moves item-wise tax details out of a JSON field on each transaction item and into the Item Wise Tax Detail child table, so every tax amount becomes a row that can be queried, filtered and reported like other child records. Any custom report, print format, server script or API client that parses the old JSON string will return empty or wrong values after the upgrade, because the source field is no longer the place the data lives. Before the first migrate, search your custom apps for the JSON field name and for direct reads of the tax column in frappe.db.get_all, frappe.get_doc loops and Jinja templates, then rewrite each read to query the child table by parent document and item row. The ERPNext version 16 migration guide lists the field changes and should be the reference for the exact names.

Verification needs a fixed baseline taken from the source site before migration. For a sample of submitted sales and purchase invoices, record the grand total, the net total, and the tax amount per tax account, then compare those figures to the same documents on the migrated test site. Rows with multiple tax accounts, zero-rated items and documents with discounts deserve their own sample, since they are where a conversion error is most likely to hide. Re-run your custom reports on both sites and check that each total matches to the cent. Totals that differ in rounding only should be explained in writing, not accepted silently.

Accounting sign-off is required before the change goes to production. The tax figures feed the general ledger, VAT or GST returns and any statutory filing built on them, so your finance or tax lead should approve the baseline comparison and the reconciliation report, not only the IT team. Confirm with that owner which filings depend on item-level tax, and whether any period must be closed or reconciled on the old version before the cutover date. Keep the pre-migration backup and the signed reconciliation together as the record of the change.

What makes a version 16 custom app port take longer

This table lists the factors that most often stretch a custom app port to version 16 and the effect each one has on effort. Read each row as a question to answer about your own sites before estimating the work. The main takeaway is that the count of apps and doctypes matters less than how deeply the code depends on behavior that version 16 changed.

DriverEffect on effortWhat to check
Number of custom apps and DocTypesEach app needs its own dependency, hook and test pass, and effort grows with every DocType that has custom controllers or permission logicInventory every app in the bench, its DocTypes, and which ones define custom controllers, hooks or whitelisted methods
Use of Server ScriptsServer Scripts are disabled by default from version 15 and must be enabled with server_script_enabled, so scripts may silently stop running after the moveConfirm whether server_script_enabled is set on each site, and check that Frappe Cloud public shared benches do not allow Server Scripts before you rely on them
Depth of overrides to core ERPNextOverridden controllers and patched core methods must be re-read against changed upstream code, which often takes more time than new codeList every override_doctype_class, doc_events entry and monkey patch, and compare each with the version 16 migration guide
API client countEach integration using a removed whitelisted method needs a replacement endpoint and a regression test on the client sidePull API access logs for the last few months and map each method called to the version 16 removal list
Custom print formats and reportsPrint formats and script reports that read item-wise tax data must move to the Item Wise Tax Detail child tableIdentify every print format and report that uses the old JSON tax field and test totals against known invoices
Data volume for test migrationLarger datasets lengthen each trial migrate and reconciliation run, so fewer full test cycles fit in the scheduleConfirm the record counts for affected DocTypes and plan a representative subset alongside one full-copy run
Third-party apps without a version 16 branchAn app with no version 16 branch either waits on its maintainer or needs a fork, which adds ownership and upkeep workCheck each dependency's repository for a version 16 branch and decide early whether to wait, fork or replace it

Questions teams ask before porting custom apps to version 16

How much does it cost to port a custom app to version 16?

The cost depends mainly on how many apps and customizations you run, how much of the code touches the areas that changed, and how thoroughly your tests cover them. The version 16 changes that most often need code work are the has_permission hooks, document hooks that commit, whitelisted methods your API clients call, and any Python or Node tooling that must move to the new minimums. Each app is a separate line of work, and the tests you already have determine how much manual checking remains. Pricing is not published as a list. Ask for a fixed-scope quote that lists each app, each change, the test approach, and which items are your team's responsibility, and separate the one-time port from any recurring support you may want afterward.

How long does a version 16 port take, and what sets the schedule?

The schedule is set by the number of apps, the size of the custom code, the quality of existing tests, and how quickly your team can review changes and provide test data. Work usually runs in an order that fixes the platform dependencies first, then ports each app against a test site, then runs your business processes end to end. Ask the partner to show the planned phases and the review points in the written scope, and confirm which of your staff must be available for sign-off. Any duration you are given should be traced back to those inputs rather than accepted as a fixed number.

What does the work need from our team?

A port needs people who know the business rules behind each custom app, access to a copy of production data or a representative test dataset, and someone who can approve changes to permissions and integrations. Your team should also provide the current Git history for each app, the list of API clients and the methods they call, and the Python and Node versions your servers run. Plan for testers to repeat core workflows such as invoicing, stock movement and period close after each change. Confirm these commitments in the scope, because delays usually come from waiting on people rather than from code.

Can an in-house team port custom apps to version 16 without a partner?

Yes, a capable in-house team can do the port if it has Frappe framework experience, a test site that matches production, and time to work through the migration guides for both Frappe and ERPNext. The version 16 migration guides list the breaking changes, and the checklist is specific: Python 3.14 or later, Node 24 or later, the /desk route, has_permission hooks that return True explicitly, document hooks that no longer commit, and removed whitelisted methods. The risk is usually in the parts nobody has documented. Compare your team's capacity and skills with the scope, and consider a partner for a fixed-scope review of the riskiest apps.

Does Frappe warranty coverage depend on a matching support plan?

Yes. Frappe's supported versions policy says customers must run a listed version and subscribe to a matching Frappe Cloud support plan to receive warranty coverage from Frappe. This applies to the platform itself, so confirm it with Frappe for your hosting setup before you plan your upgrade. Self-hosting ERPNext is free under its open-source license, but the warranty question is separate from the software license. Ask Frappe directly which plan, if any, matches your deployment, and record the answer in your upgrade plan. You can find the current list of supported versions on the Frappe supported versions page.

What should we do with Server Scripts on shared hosting?

Server Scripts are disabled by default from version 15 onward and must be enabled with the server_script_enabled setting. Frappe Cloud public shared benches do not allow them, so any custom app logic written as a Server Script will not run there. Move that logic into a custom app with tested Python code, which gives you version control, code review and a clear place for the has_permission and document hook changes. Decide early which scripts are worth keeping, and confirm with your host whether you can run a dedicated bench where scripts are allowed.

How do we confirm an app is ready before cutover?

Confirm readiness with evidence rather than a sign-off meeting. Run the app's tests on a version 16 test site built from a recent copy of production data, then check that every has_permission hook returns True explicitly, that no document hook commits a transaction, that every API method your clients call still exists, and that the app installs and migrates cleanly on the required Python and Node versions. Walk through your core business workflows with real users, and keep a rollback plan with a tested backup taken before the production migration. Record the results in a checklist that your team and the partner both sign.

Testing and rolling back if a custom app fails on version 16

Test the custom app on a copy of production before any production site is touched. The test plan should combine four checks: unit tests for the app's own Python logic, a bench migrate run against a fresh restore of the production database, role-based walkthroughs of every document type the app changes, and scripted API regression calls for each whitelisted method your integrations use. The migrate run matters most, because the Frappe version 16 migration guide tightens document hooks and permission hooks in ways that tend to fail quietly rather than at install time. Confirm with your developers which whitelisted methods the migration guide removes from ERPNext, and make sure each one your integrations call has been replaced or retired before sign-off.

Sign off on evidence, not on a clean migrate log alone. The release owner should accept the upgrade only when the unit suite passes on Python 3.14 and Node 24, each role walks through its core documents (including has_permission results for users who should be denied access), the item-wise tax figures on a sample of invoices match the pre-upgrade numbers after the move to the Item Wise Tax Detail child table, and the regression calls return the same fields and status codes as version 15. Record who signed each item, the build versions tested, and the backup identifier they were tested against, so that an auditor or your own change board can trace the decision later.

Plan the rollback before the production window opens. Keep a full backup taken immediately before the upgrade, verified by a test restore, and keep the version 15 bench and the app versions that ran on it available until the new site has run in production for a period you agree in advance. If the custom app fails, roll back by restoring the pre-upgrade backup and switching the bench back to version 15, not by reversing database migrations by hand. Version 15 is supported until the end of 2027, according to Frappe's supported versions page, so there is time to stage the work, fix the app on a test site, and schedule a second attempt. Confirm with your Frappe support plan whether it covers the version you run during this window.

How DeployOpen scopes, ports and supports custom ERPNext apps

DeployOpen starts with an inventory of each custom app against the version 16 breaking changes, then ports the code in a test copy of your bench before anything touches production. Each change is tested, documented and handed over with the work so your team can run the apps after the engagement ends.

How we scope

  • Inventory every custom app, hook, whitelisted method and client script that your sites depend on.
  • Map each finding to the version 16 changes: Python 3.14, Node 24, the /desk route and document hook commits.
  • Agree the scope, deliverables and timeline in writing before work starts, since most engagements are fixed-scope.

How we build

  • Port has_permission hooks so they return True explicitly and review each permission path for unintended access.
  • Move commits out of document hooks and check that transactions are handled by the calling code.
  • Replace removed whitelisted methods used by API clients and test each affected integration on a staging site first.

How we hand over

  • Provide a changelog of code changes, the reasoning behind each port and the test results for each app.
  • Document the runtime versions, bench commands and migration steps your team needs to repeat the upgrade.
  • Walk your engineers through the test site and the rollback plan before the production cutover.

How we support

  • Offer ongoing support, upgrades and continuous testing for custom apps as a retainer after the engagement.
  • Retest custom apps when new ERPNext minor releases arrive, so the port stays current with your bench updates.
  • Confirm with your team which support window and plan apply to your Frappe and ERPNext versions before scoping.

Sources and further reading

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.