Server Scripts are quick. Custom apps are durable. Learn when to use Custom Fields, Client Scripts, Server Scripts, or a full custom app in Frappe and ERPNext, with a side-by-side example.

Almost every Frappe or ERPNext project reaches the same question: a customer wants a small change, and you have to decide where to put it.
You can add a Custom Field, write a Client Script, write a Server Script, or build the logic inside a custom app. All of them work. The difference shows up months later, when you need to upgrade, debug, test, or hand the project to someone else.
In this post, we'll explain what each option is for, when scripts are enough, when a custom app is the better choice, and how to decide quickly.
The first three live inside the site's database. The last one lives in files.
Scripts are a good fit when the change is small, specific to one site, and easy to understand at a glance:
Their biggest advantage is speed. You can write, test, and change them from the browser, with no deployment step.
A custom app starts to pay off when the change is bigger, riskier, or needs to live for years:
If you are building a SaaS product or a vertical solution, a custom app is almost always the right home for your core logic.
Upgrades are where the difference becomes real.
Scripts stored in the database:
Custom app code:
bench migrateEither way, test every upgrade on a copy of the site first. But with a custom app, you have far better tools to know what could break.
None of these are reasons to avoid scripts completely. They are reasons to keep scripts small and few.
Say a client wants to block any Sales Order with a discount above 10%.
As a Server Script (Document Event, before save, on Sales Order):
if (doc.additional_discount_percentage or 0) > 10:
frappe.throw("Discount cannot exceed 10%")It takes a few minutes. It lives in that site's database, and it's invisible to anyone reading your code repository.
As a custom app:
In hooks.py:
doc_events = {
"Sales Order": {
"validate": "my_app.sales.validate_discount"
}
}In my_app/sales.py:
import frappe
from frappe import _
MAX_DISCOUNT = 10
def validate_discount(doc, method=None):
if (doc.additional_discount_percentage or 0) > MAX_DISCOUNT:
frappe.throw(_("Discount cannot exceed {0}%").format(MAX_DISCOUNT))It takes longer to set up, but it's in Git, it can be tested, it can be installed on every site, and it travels with your code.
For a single rule on a single site, the script is perfectly fine. For a rule that is part of your product, the app version is the safer choice.
You don't have to decide forever on day one. A common, practical approach:
This gives you speed at the start and durability later.
Add your own real example here: a script that grew into a problem, or the moment you moved from scripts to an app, and what you learned. Remove client names and sensitive details before publishing.
Ask these questions, and lean toward a custom app for every "yes":
If most answers are "no", a script is probably enough.
Scripts are for speed. Custom apps are for durability. Use scripts for small, local, low-risk changes, and move anything that is part of your product, or that would hurt if it broke, into a custom app with version control and tests.
At Flitz Interactive, we build custom Frappe and ERPNext apps and help teams clean up script-heavy sites, from planning through deployment and upgrades. If you'd like help structuring your own customizations, we'd be happy to talk it through.