Creating a Custom App
Every real-world ERPNext deployment eventually needs custom functionality. For ScoopJoy, that means tracking franchise outlets, calculating royalty fees, and building compliance dashboards. The Frappe framework is designed for exactly this: you build a custom app that extends ERPNext without touching its core code. Your customizations live in a separate, version-controlled repository you can install, update, and share independently.
In this chapter we’ll create the scoopjoy app from scratch and walk through
every step of the development workflow. If you’ve worked in Node.js, think of it
as npm init for a domain package that plugs into a much larger host
application — except the host hands you an ORM, an admin UI, and an API layer for
free.
Why custom apps?
Section titled “Why custom apps?”ERPNext is open source — you could fork it and modify the source directly. But that’s a maintenance nightmare. Every time ERPNext releases an update, you’d have to merge your changes with upstream code, resolve conflicts, and hope nothing breaks.
Custom apps solve this cleanly:
- Isolation — your code lives in its own directory, separate from ERPNext.
- Upgradability — ERPNext updates don’t touch your app; your app doesn’t touch ERPNext.
- Portability — install your app on any Frappe bench with a single command.
- Collaboration — multiple developers work on the custom app without affecting core.
- Marketplace — publish your app for the community or sell it commercially.
The framework provides hooks, events, and APIs that let custom apps deeply integrate with ERPNext while staying completely decoupled from its source code.
Creating the app: bench new-app
Section titled “Creating the app: bench new-app”Navigate to your bench directory and run:
cd ~/frappe-benchbench new-app scoopjoyThe command walks you through a series of prompts:
App Title (default: Scoopjoy): ScoopJoyApp Description: Franchise outlet management, royalty tracking, and compliance for ScoopJoy's multi-location ice-cream businessApp Publisher: ScoopJoyApp Email: dev@scoopjoy.exampleCreate GitHub Workflow (y/n, default n): yBranch name for GitHub Workflow (default: version-16): version-16After the prompts, Frappe scaffolds the app in ~/frappe-bench/apps/scoopjoy/.
App directory structure
Section titled “App directory structure”Here’s what gets created:
Directoryscoopjoy/
Directoryscoopjoy/
- __init__.py
- hooks.py app configuration and hooks
- patches.txt migration patches registry
- modules.txt registered modules
Directorytemplates/
- __init__.py
Directorypages/
- __init__.py
Directoryscoopjoy/ default module (same name as app)
- __init__.py
Directorydoctype/ DocTypes go here
- __init__.py
Directoryconfig/
- __init__.py
- desktop.py legacy — v16 uses Workspace JSON files instead
Directorypublic/
Directoryjs/
- …
Directorycss/
- …
Directory.github/
Directoryworkflows/
- ci.yml auto-generated CI workflow
- setup.py
- pyproject.toml
- setup.cfg
- requirements.txt
- license.txt
- MANIFEST.in
- README.md
Let’s understand the key pieces.
hooks.py: the app’s configuration hub
Section titled “hooks.py: the app’s configuration hub”The generated hooks.py is the central configuration file. Here’s the initial
skeleton (commented-out boilerplate trimmed):
app_name = "scoopjoy"app_title = "ScoopJoy"app_publisher = "ScoopJoy"app_description = "Franchise outlet management, royalty tracking, and compliance for ScoopJoy's multi-location ice-cream business"app_email = "dev@scoopjoy.example"app_license = "MIT"
# Required apps for this app to workrequired_apps = ["frappe/erpnext"]
# Each module in the app has a separate folder; modules are listed in modules.txt
# Document Events — hook on document methods and events# doc_events = {# "*": {# "on_update": "method",# }# }
# Scheduled Tasks# scheduler_events = {# "daily": [],# "hourly": [],# "cron": {"0 */6 * * *": []},# }The required_apps field is critical. Since our app extends ERPNext, we declare
the dependency. Frappe will refuse to install our app if ERPNext isn’t already
installed on the site.
modules.txt
Section titled “modules.txt”This file registers the modules (functional groupings) in your app:
ScoopJoyEach line is a module name. When you create DocTypes, you assign them to a module. The module name maps to a subdirectory under your app’s inner package. You can add more modules later:
ScoopJoyScoopJoy ComplianceScoopJoy AnalyticsEach new module needs a corresponding directory:
mkdir -p scoopjoy/scoopjoy/scoopjoy_compliance/doctypetouch scoopjoy/scoopjoy/scoopjoy_compliance/__init__.pytouch scoopjoy/scoopjoy/scoopjoy_compliance/doctype/__init__.pypyproject.toml
Section titled “pyproject.toml”The v16 scaffold includes a modern pyproject.toml:
[project]name = "scoopjoy"dynamic = ["version"]requires-python = ">=3.14,<3.15"dependencies = []
[build-system]requires = ["flit_core >=3.4,<4"]build-backend = "flit_core.buildapi"
[tool.bench.frappe-dependencies]frappe = ">=16.0.0"erpnext = ">=16.0.0"Installing your app
Section titled “Installing your app”With the app scaffolded, install it on your development site:
bench --site icecream.localhost install-app scoopjoyOutput:
Installing scoopjoy...Updating Dashboard for scoopjoyVerify the installation:
bench --site icecream.localhost list-appsfrappeerpnextscoopjoyYou can also verify from the Desk by navigating to Setup > Installed Applications. You should see “ScoopJoy” listed.
The development workflow
Section titled “The development workflow”Frappe development follows a consistent cycle: Code → bench migrate →
bench build → Test → Repeat.
Here’s what each step does:
-
Code — edit Python files, create DocTypes via the Desk UI (in developer mode), write client scripts.
-
bench migrate— applies schema changes to the database. Run this after creating or modifying DocTypes, adding patches, or changing fixtures.Terminal window bench --site icecream.localhost migrate -
bench build— compiles and bundles frontend assets (JS, CSS). Run this after modifying.js/.cssfiles in thepublic/directory or adding app-level includes.Terminal window bench build --app scoopjoyFor development, use watch mode that auto-rebuilds on file changes:
Terminal window bench watch -
Test — run your app’s tests:
Terminal window bench --site icecream.localhost run-tests --app scoopjoy -
bench clear-cache— if you see stale data or UI not reflecting changes:Terminal window bench --site icecream.localhost clear-cache
Developer mode
Section titled “Developer mode”Developer mode is essential. It lets you create and modify DocTypes from the Desk UI, auto-generate their JSON and Python files, and write those files to disk so you can commit them to Git.
Enable it:
bench --site icecream.localhost set-config developer_mode 1When developer mode is on, every time you save a DocType in the Desk, Frappe writes the updated JSON definition and controller files to your app’s directory on disk. This is how UI-based development flows back into version-controlled code.
Version control
Section titled “Version control”Initialize git in your app directory (the scaffold may already have done this):
cd ~/frappe-bench/apps/scoopjoygit initgit add .git commit -m "Initial scaffold: ScoopJoy app for Frappe v16"The auto-generated .gitignore handles common exclusions. It typically includes:
.eggs/*.egg-info/*.egg/*.pyc*.pyo__pycache__/dist/build/*.swp.DS_Storenode_modules/.envAdd a remote and push:
git remote add origin git@github.com:scoopjoy/scoopjoy.gitgit push -u origin version-16Publishing and sharing apps
Section titled “Publishing and sharing apps”Once your app is on a Git repository, anyone can install it. In Node.js terms,
bench get-app is your npm install <git-url> — it clones the app into the bench;
install-app then activates it on a specific site (tenant).
# From a public GitHub repobench get-app https://github.com/scoopjoy/scoopjoy.git
# From a private repo via SSHbench get-app git@github.com:scoopjoy/scoopjoy.git
# From a specific branchbench get-app https://github.com/scoopjoy/scoopjoy.git --branch version-16
# Then install on a sitebench --site icecream.localhost install-app scoopjoyFor Frappe Cloud deployments, you add your app through the Frappe Cloud dashboard, pointing to your Git repository. Frappe Cloud handles building and deploying automatically.
Complete walkthrough: from zero to running app
Section titled “Complete walkthrough: from zero to running app”Putting it all together — start to finish, creating and verifying the ScoopJoy app:
-
Create the app (answer the prompts as shown above):
Terminal window cd ~/frappe-benchbench new-app scoopjoy -
Install on the development site:
Terminal window bench --site icecream.localhost install-app scoopjoy -
Verify the installation:
Terminal window bench --site icecream.localhost list-apps -
Start the development server:
Terminal window bench start -
Open the browser and confirm. Navigate to
http://icecream.localhost:8000, log in to the Desk, go to Setup > Installed Applications, and confirm “ScoopJoy” appears. -
Initialize version control:
Terminal window cd apps/scoopjoygit initgit add .git commit -m "Initial scaffold: ScoopJoy app"
At this point you have a working, empty custom app installed on your site, under version control, and ready for development. Next, you’ll fill it with DocTypes — see Custom DocTypes, Fields & Forms.