Project Structure & Hooks
Standard Frappe app layout, the main hooks.py settings, bundle naming, and post-migration scripts.
On this page
App Directory Layout
A Frappe app follows this standard structure:
myapp/
├── myapp/
│ ├── __init__.py
│ ├── hooks.py # Central configuration file
│ ├── modules.txt # List of modules (one per line)
│ ├── patches.txt # Migration patches
│ ├── migrate.py # Post-migration hook (optional)
│ ├── mymodule/
│ │ ├── doctype/
│ │ │ └── my_doctype/
│ │ │ ├── my_doctype.json # DocType definition
│ │ │ ├── my_doctype.py # Controller
│ │ │ ├── my_doctype.js # Form script
│ │ │ ├── my_doctype_list.js # List view customization (optional)
│ │ │ └── test_my_doctype.py # Tests
│ │ ├── page/
│ │ │ └── my_page/
│ │ │ ├── my_page.json # Page definition
│ │ │ ├── my_page.py # Empty (required for registration)
│ │ │ └── my_page.js # Page script (or built bundle)
│ │ ├── report/
│ │ │ └── my_report/
│ │ │ ├── my_report.json # Report definition
│ │ │ ├── my_report.py # Data logic
│ │ │ ├── my_report.js # Client-side config
│ │ │ ├── my_report.sql # External SQL (optional)
│ │ │ ├── my_report.css # Report styles (optional)
│ │ │ └── columns.json # External column defs (optional)
│ │ └── web_form/
│ │ └── my_form/
│ │ ├── my_form.json # Web form definition
│ │ ├── my_form.py # get_context() hook
│ │ └── my_form.js # Client-side logic
│ ├── custom/ # DocType overrides (see customizations-api.md)
│ │ ├── doctype_name.json
│ │ └── doctype_name.py
│ ├── public/
│ │ ├── js/
│ │ │ ├── app.bundle.js # Auto-included desk JS bundle
│ │ │ └── my_control.bundle.js # Named bundle (include via hooks)
│ │ └── dist/ # Vite/Rollup build output
│ └── utils/ # Shared utilities
├── pyproject.toml
├── package.json # Node dependencies (if using Vite/Rollup)
├── vite.config.ts # Vite build config (optional)
└── tsconfig.json # TypeScript config (optional)hooks.py Reference
hooks.py is the central configuration file for a Frappe app. Key properties:
Basic App Info
app_name = "myapp"
app_title = "My App"
app_publisher = "Avunu LLC"
app_description = "Description"
app_license = "MIT"
export_python_type_annotations = True # Enable DF.* type annotation system
required_apps = ["frappe", "erpnext"] # App dependenciesAsset Inclusion
# Global desk JS/CSS (loaded on every desk page)
app_include_js = "myapp.app.bundle.js"
app_include_css = "myapp.app.bundle.css"
# Website-only assets
web_include_js = ["/assets/myapp/js/web.js"]
web_include_css = ["/assets/myapp/css/web.css"]
# Web form specific assets
webform_include_js = {"My Web Form": ["public/js/webform_helper.js"]}
webform_include_css = {"My Web Form": ["public/css/webform.css"]}DocType Class Overrides
override_doctype_class = {
"Item": "myapp.myapp.custom.item.Item",
"BOM": "myapp.myapp.custom.bom.BOM",
}Document Event Hooks
doc_events = {
"Workday": {
"before_save": "myapp.api.workday_update",
"on_update": "myapp.api.workday_changed",
},
"*": { # All doctypes
"on_trash": "myapp.api.handle_trash",
},
}List View JS Overrides
doctype_list_js = {
"Job Card": "public/js/job_card.js",
}Post-Migration Hook
after_migrate = "myapp.migrate.after_migrate"Bundle Naming Convention
Frappe's asset builder recognizes files by naming pattern:
| Pattern | Behavior |
|---|---|
*.bundle.js | Processed through Frappe's esbuild bundler (resolves imports) |
*.bundle.css | Processed through Frappe's CSS bundler |
app.bundle.js | Special: auto-included when app_include_js references it |
Place bundles in <app>/public/js/ and reference them in hooks:
app_include_js = "myapp.app.bundle.js" # → public/js/app.bundle.jsThe app.bundle.js convention is particularly useful for injecting global behavior (custom views, field controllers) without explicitly including them per page.
Post-Migration Script
migrate.py runs after bench migrate completes:
from frappe.utils.nestedset import rebuild_tree
from frappe.utils.scheduler import activate_scheduler
def after_migrate() -> None:
rebuild_tree("Item Group") # Rebuild nested set tree
activate_scheduler() # Ensure scheduler is runningRegister in hooks: after_migrate = "myapp.migrate.after_migrate".
This article is in the public domain (CC0 1.0), code samples included. Use it however helps you.