Skip to content

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.

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.

Navigate to your bench directory and run:

Terminal window
cd ~/frappe-bench
bench new-app scoopjoy

The command walks you through a series of prompts:

App Title (default: Scoopjoy): ScoopJoy
App Description: Franchise outlet management, royalty tracking, and compliance for ScoopJoy's multi-location ice-cream business
App Publisher: ScoopJoy
App Email: dev@scoopjoy.example
Create GitHub Workflow (y/n, default n): y
Branch name for GitHub Workflow (default: version-16): version-16

After the prompts, Frappe scaffolds the app in ~/frappe-bench/apps/scoopjoy/.

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.

The generated hooks.py is the central configuration file. Here’s the initial skeleton (commented-out boilerplate trimmed):

scoopjoy/scoopjoy/hooks.py
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 work
required_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.

This file registers the modules (functional groupings) in your app:

scoopjoy/scoopjoy/modules.txt
ScoopJoy

Each 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:

scoopjoy/scoopjoy/modules.txt
ScoopJoy
ScoopJoy Compliance
ScoopJoy Analytics

Each new module needs a corresponding directory:

Terminal window
mkdir -p scoopjoy/scoopjoy/scoopjoy_compliance/doctype
touch scoopjoy/scoopjoy/scoopjoy_compliance/__init__.py
touch scoopjoy/scoopjoy/scoopjoy_compliance/doctype/__init__.py

The v16 scaffold includes a modern pyproject.toml:

scoopjoy/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"

With the app scaffolded, install it on your development site:

Terminal window
bench --site icecream.localhost install-app scoopjoy

Output:

Installing scoopjoy...
Updating Dashboard for scoopjoy

Verify the installation:

Terminal window
bench --site icecream.localhost list-apps
frappe
erpnext
scoopjoy

You can also verify from the Desk by navigating to Setup > Installed Applications. You should see “ScoopJoy” listed.

Frappe development follows a consistent cycle: Code → bench migratebench build → Test → Repeat.

Here’s what each step does:

  1. Code — edit Python files, create DocTypes via the Desk UI (in developer mode), write client scripts.

  2. 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
  3. bench build — compiles and bundles frontend assets (JS, CSS). Run this after modifying .js/.css files in the public/ directory or adding app-level includes.

    Terminal window
    bench build --app scoopjoy

    For development, use watch mode that auto-rebuilds on file changes:

    Terminal window
    bench watch
  4. Test — run your app’s tests:

    Terminal window
    bench --site icecream.localhost run-tests --app scoopjoy
  5. bench clear-cache — if you see stale data or UI not reflecting changes:

    Terminal window
    bench --site icecream.localhost clear-cache

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:

Terminal window
bench --site icecream.localhost set-config developer_mode 1

When 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.

Initialize git in your app directory (the scaffold may already have done this):

Terminal window
cd ~/frappe-bench/apps/scoopjoy
git init
git add .
git commit -m "Initial scaffold: ScoopJoy app for Frappe v16"

The auto-generated .gitignore handles common exclusions. It typically includes:

scoopjoy/.gitignore
.eggs/
*.egg-info/
*.egg/
*.pyc
*.pyo
__pycache__/
dist/
build/
*.swp
.DS_Store
node_modules/
.env

Add a remote and push:

Terminal window
git remote add origin git@github.com:scoopjoy/scoopjoy.git
git push -u origin version-16

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).

Terminal window
# From a public GitHub repo
bench get-app https://github.com/scoopjoy/scoopjoy.git
# From a private repo via SSH
bench get-app git@github.com:scoopjoy/scoopjoy.git
# From a specific branch
bench get-app https://github.com/scoopjoy/scoopjoy.git --branch version-16
# Then install on a site
bench --site icecream.localhost install-app scoopjoy

For 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:

  1. Create the app (answer the prompts as shown above):

    Terminal window
    cd ~/frappe-bench
    bench new-app scoopjoy
  2. Install on the development site:

    Terminal window
    bench --site icecream.localhost install-app scoopjoy
  3. Verify the installation:

    Terminal window
    bench --site icecream.localhost list-apps
  4. Start the development server:

    Terminal window
    bench start
  5. 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.

  6. Initialize version control:

    Terminal window
    cd apps/scoopjoy
    git init
    git 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.