Skip to main content

Chapter 7 of 7 · Changing it

Custom field, custom app or fork

What each choice stores, what an upgrade costs, and how to pick.

Reviewed 14 Oct 20263 min readFrappe v16.52.0

The implementation guide sorts customisation into configuration, extension and modification, and says they age differently. This chapter shows what each one is inside Frappe.

Answer up to three questions below, and the module names the lightest option that does the job, with what an upgrade will cost.

Try it

What do you need to change?

Answer one question at a time. The lightest choice that does the job comes up, with what an upgrade will cost.

What do you need to change?

Frappe v16.52.0 and ERPNext v16.50.0; v15 marked. The cost words are our reading, not a measurement.

Compared

What each choice costs at upgrade

Our reading from where each one lives, not a measured cost.

What each choice costs at upgrade: where it lives, whether it is in Git, whether it survives bench update, what can break and what you re-test
ChoiceWhere it livesIn Git by defaultSurvives bench updateWhat can breakYou re-test
Custom Field or Property SetterLowDatabase rows (Custom Field, Property Setter)No, until you export them to an appYesAn upstream field with your fieldname; Custom Permissions are replaced on syncWhen a DocType you changed gains or renames fields
Client Script or Server ScriptLow to mediumDatabase rows (Client Script, Server Script)No, until you export themYesThe names the script calls change; Server Scripts need server_script_enabled, and their document-event scripts do not run during install or migrateEvery major upgrade, by hand: nothing tests it
Custom app using hooksMediumYour app’s Git repositoryYesYes: it is its own repositoryA hook’s contract changes (v16: has_permission returning None denies; doc_events handlers cannot commit)Every upgrade, with your tests
Custom app extending a class (v16)MediumYour app’s Git repositoryYesYesA method you add to, or call through super(), changes upstreamEvery upgrade, with your tests
Custom app replacing a classHighYour app’s Git repositoryYesThe app does; upstream fixes to that controller do not reach itSilently: the original class changes and yours does not follow; the last app in the list winsEvery release that touches that controller
Fork of Frappe or ERPNextHighestYour fork’s Git repositoryYesOnly if you merge each tag; bench update --reset overwrites local editsEvery mergeEvery release

Custom fields and property setters: rows, not code

Frappe builds a DocType's definition when it loads it: the app's JSON, then the Custom Field rows for that DocType, then the Property Setter rows that override its properties. Nothing in the app's files changes, which is why bench update does not touch them.

They live in the database, not in Git, until you export them to your app. A Custom Field whose fieldname you leave blank gets the prefix custom_. One whose fieldname you type keeps it as typed, and can collide with a field a later release adds. An exported file with sync_on_migrate is re-applied at every migrate, and Custom Permissions are deleted and re-inserted, not merged.

Scripts: quick, and outside Git

A Client Script runs in the browser. A Server Script runs in a restricted Python sandbox, only if server_script_enabled is set in common_site_config.json, and its document-event scripts do not run during install or migrate. Both are database rows, so nothing tests them when you upgrade. They suit a one-site rule. Move the script into an app the day it needs a test.

A custom app: code in your own repository

A separate Frappe app, in its own Git repository, loaded like any other. Hooks (doc_events, scheduler_events, fixtures, the permission hooks) let it react without touching Frappe. It survives bench update because it is a separate repository. It breaks where a hook's contract changes: on v16, has_permission returning None denies, and doc_events handlers cannot commit. Our post lists the rest.

Replacing a class is a fork of one file

override_doctype_class swaps the controller for your subclass. Upstream changes to that controller never reach you unless you merge them by hand. If two apps override one DocType the last one wins, and bench migrate prints a warning.

v16 adds extend_doctype_class. Your mixin sits in front of the controller, and several apps can stack as long as each calls super(). ERPNext made that move itself: Address is an override in v15.122.0 and an extension in v16.50.0.

A fork: you own every merge

bench update backs up, pulls every app, installs requirements, builds, runs patches and restarts. Its --reset flag hard-resets the branches over local edits. A fork means merging every tag yourself, and support windows are finite: v14 ended on 31 January 2026, v15 is planned to end in 2027 and v16 in 2029, so every major version is another merge.

A fork suits a fix you are sending upstream. Pin the commit, and drop the fork when the fix merges. It is a poor fit for a change you mean to keep, because that merge never ends.

How do you choose?

Ask in order: can it be a row, a script, a hook, a mixin? The first yes is the cheapest. The module below the questions lists what each costs at upgrade.

Where this comes from

Read at Frappe v16.52.0 and ERPNext v16.50.0. “(v15)” marks Frappe v15.123.0 and “(ERPNext v15)” marks ERPNext v15.122.0. Each link opens that file at that tag, on that line.

Official page: Customizing DocTypes, read 14 October 2026. Method: Over-customisation, in the implementation guide.

Your next step

See it live

Run a sprint

Finish a task in this week's sprint, log the hour it took, and see the sprint's burndown, all on ERPNext's own records.

You end with hours booked on a task · 2 screens · about 3 min

No sign-up, no card, no account.

Next step · Architecture guide

Now see how we build a Frappe app

A separate app in your own Git, so upgrades keep working.