
Frappe Ops App Lifecycle
- 23 installs
- 159 repo stars
- Updated July 8, 2026
- openaec-foundation/frappe_claude_skill_package
Helps with ai & agent building tasks.
About
frappe-ops-app-lifecycle is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- frappe-ops-app-lifecycle
- AI & Agent Building
- AI-coding skill
Frappe Ops App Lifecycle by the numbers
- 23 all-time installs (skills.sh)
- Ranked #10,032 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/frappe_claude_skill_package --skill frappe-ops-app-lifecycleAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 23 |
|---|---|
| repo stars | ★ 159 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/frappe_claude_skill_package ↗ |
What it does
Helps with ai & agent building tasks.
Files
App Lifecycle Management
Quick Reference
| Command | Purpose | When to Use |
|---|---|---|
bench new-app | Scaffold new app | Starting a new project |
bench get-app URL | Clone from Git | Installing existing app |
bench --site SITE install-app | Install on site | After get-app or new-app |
bench --site SITE remove-app | Uninstall from site | Removing app from site |
bench remove-app | Remove from bench | Removing app entirely |
bench --site SITE migrate | Run patches + sync | After code changes |
bench build | Compile assets | After JS/CSS changes |
bench --site SITE console | Python REPL | Debugging |
bench start | Start dev server | Development |
bench setup production | Configure nginx+supervisor | Deploying to production |
1. Scaffolding: bench new-app
bench new-app my_custom_appInteractive prompts:
- App Title → Human-readable name
- App Description → One-line summary
- App Publisher → Company/author name
- App Email → Contact email
- App Icon → Default:
octicon octicon-file-directory - App Color → Default:
grey - App License → Default:
MIT
Generated Directory Structure
apps/my_custom_app/
├── MANIFEST.in # Files included in Python package
├── README.md # Project readme
├── license.txt # License file
├── requirements.txt # Python dependencies
├── dev-requirements.txt # Dev-only Python deps (v15+)
├── package.json # Node.js dependencies
├── setup.py # Python package config (v14)
├── pyproject.toml # Python package config (v15+)
├── my_custom_app/
│ ├── __init__.py # App version string
│ ├── hooks.py # Framework integration hooks
│ ├── modules.txt # List of app modules
│ ├── patches.txt # Migration patches list
│ ├── config/
│ │ ├── __init__.py
│ │ ├── desktop.py # Desktop/workspace config
│ │ └── docs.py # Documentation config
│ ├── public/ # Static assets → /assets/my_custom_app/
│ │ ├── css/
│ │ └── js/
│ ├── templates/ # Jinja templates
│ └── www/ # Portal pages (URL = path)What Each Core File Does
| File | Purpose | NEVER Forget |
|---|---|---|
__init__.py | Defines __version__ | ALWAYS update before release |
hooks.py | ALL framework integration | Entry point for everything |
modules.txt | Declares app modules | ALWAYS add new modules here |
patches.txt | Migration patch registry | ALWAYS add patches in order |
requirements.txt | Python deps installed on setup | Add pip packages here |
public/ | Static files served by nginx | Accessible at /assets/app_name/ |
www/ | Portal pages | Filename = URL path |
2. Development Cycle
Code → Migrate → Build → Test → CommitStep-by-Step
# 1. Make code changes (DocTypes, reports, APIs, etc.)
# 2. Migrate — sync DocType schema + run patches
bench --site mysite migrate
# 3. Build — compile JS/CSS assets
bench build --app my_custom_app
# 4. Test — run Python tests
bench --site mysite run-tests --app my_custom_app
# 5. Commit
git -C apps/my_custom_app add -A && git -C apps/my_custom_app commit -m "feat: add feature"ALWAYS run bench migrate after modifying DocType JSON files. ALWAYS run bench build after modifying JS/CSS files.
3. Getting Apps from Git
# Public repo
bench get-app https://github.com/org/my_app
# Specific branch
bench get-app https://github.com/org/my_app --branch develop
# Private repo via SSH
bench get-app git@github.com:org/private_app.git
# Private repo via token (v15+)
bench get-app https://TOKEN@github.com/org/private_app.gitAfter get-app, ALWAYS install on the target site:
bench --site mysite install-app my_appget-app clones to apps/ and adds to apps.txt. install-app creates database tables and runs after_install hooks.
4. Installing and Removing Apps
Installation Order Matters
Apps are installed in order listed in apps.txt. If App B depends on App A, App A MUST be listed first.
# Install
bench --site mysite install-app my_app
# Verify
bench --site mysite list-apps
# Output: frappe, erpnext, my_app
# Remove from site (keeps code in apps/)
bench --site mysite remove-app my_app
# Remove from bench entirely (deletes code)
bench remove-app my_appApp Dependencies (v14+)
Declare in hooks.py:
required_apps = ["frappe", "erpnext"]Frappe ALWAYS checks required_apps during installation and blocks if dependencies are missing.
5. Debugging with bench console
bench --site mysite consoleOpens an IPython REPL with Frappe context:
# Query data
frappe.db.sql("SELECT name, status FROM `tabSales Invoice` LIMIT 5", as_dict=True)
# Get a document
doc = frappe.get_doc("Sales Invoice", "SINV-00001")
print(doc.grand_total)
# Test a whitelisted method
from my_app.api import my_function
result = my_function(param="value")
# Check configuration
frappe.get_site_config()
# Auto-reload on code changes (v15+)
# Start with: bench --site mysite console --autoreloadALWAYS use bench console for debugging — NEVER modify production data with raw SQL.
6. Development Mode vs Production Mode
Development Mode
# Enable
bench set-config -g developer_mode 1
# Start dev server (Procfile: web + worker + redis + socketio)
bench startDevelopment mode enables:
- DocType editing in Desk
- "Is Standard" option for reports/scripts
- Auto-reload on Python file changes
- Detailed error tracebacks in browser
dev-requirements.txtdependencies installed
Production Mode
# Disable developer mode
bench set-config -g developer_mode 0
# Setup production (nginx + supervisor)
sudo bench setup production USERNAME
# Restart
sudo supervisorctl restart all
# or
sudo systemctl restart supervisorProduction mode:
- Serves via nginx (port 80/443)
- Background workers via supervisor
- Static files served directly by nginx
- Errors logged to files, not browser
- NEVER enable
developer_modeon production sites
7. Asset Building
v15+ (esbuild)
# Build all apps
bench build
# Build specific app
bench build --app my_custom_app
# Watch mode (auto-rebuild on changes)
bench watchv14 (build.json)
v14 uses build.json in the app root to map source files to bundles:
{
"css/my_app.css": [
"public/css/style.css"
],
"js/my_app.js": [
"public/js/main.js"
]
}Asset Include in hooks.py
# Desk (backend UI)
app_include_js = "my_app.bundle.js" # v15+ bundle syntax
app_include_css = "my_app.bundle.css"
# Portal (website)
web_include_js = "my_app_web.bundle.js"
web_include_css = "my_app_web.bundle.css"
# v14 legacy syntax
app_include_js = "/assets/my_app/js/my_app.js"
app_include_css = "/assets/my_app/css/my_app.css"ALWAYS run bench build after changing JS/CSS files. ALWAYS run bench clear-cache if assets are not updating.
8. App Versioning
Version String in __init__.py
# my_custom_app/__init__.py
__version__ = "1.2.0"ALWAYS use semantic versioning: MAJOR.MINOR.PATCH
- MAJOR: Breaking changes
- MINOR: New features (backward compatible)
- PATCH: Bug fixes
The version is read by bench version, displayed in Desk, and used by the Marketplace.
Checking Versions
bench version
# frappe 15.23.0
# erpnext 15.18.0
# my_custom_app 1.2.09. Patches: Data Migrations
Writing a Patch
# my_app/patches/v1_2/update_customer_status.py
import frappe
def execute():
frappe.reload_doc("module_name", "doctype", "customer_extension")
frappe.db.sql("""
UPDATE `tabCustomer Extension`
SET status = 'Active'
WHERE status IS NULL
""")
frappe.db.commit()Registering in patches.txt
# patches.txt — v14+ supports sections
[pre_model_sync]
my_app.patches.v1_1.fix_old_data
my_app.patches.v1_2.rename_field_before_schema
[post_model_sync]
my_app.patches.v1_2.update_customer_status
my_app.patches.v1_2.migrate_settingsSection timing (v14+):
[pre_model_sync]— Runs BEFORE DocType schema changes are applied[post_model_sync]— Runs AFTER schema changes (new fields available)- No section header — Runs in
[pre_model_sync]by default
Patch Rules
- ALWAYS add new patches at the END of their section
- Patches run ONCE — tracked in
tabPatch Log - To re-run a patch, append a comment:
my_app.patches.v1_2.fix #2025-03-20 - ALWAYS call
frappe.reload_doc()before accessing new/modified DocTypes - ALWAYS use
[post_model_sync]for patches that need new fields - One-liner patches:
execute:frappe.delete_doc("Page", "old-page", ignore_missing=True)
Testing a Patch
# Run all pending patches
bench --site mysite migrate
# Run a specific patch manually in console
bench --site mysite console
>>> from my_app.patches.v1_2.update_customer_status import execute
>>> execute()
>>> frappe.db.commit()10. Publishing to Frappe Marketplace
Prerequisites Checklist
1. App hosted on public GitHub repository 2. setup.py or pyproject.toml with correct metadata 3. Valid __version__ in __init__.py 4. README.md with installation instructions 5. All tests passing
setup.py (v14)
from setuptools import setup, find_packages
setup(
name="my_custom_app",
version="1.0.0",
description="My Custom App for ERPNext",
author="Your Name",
author_email="you@example.com",
packages=find_packages(),
zip_safe=False,
include_package_data=True,
install_requires=["frappe"],
)pyproject.toml (v15+)
[project]
name = "my_custom_app"
dynamic = ["version"]
requires-python = ">=3.10,<3.13"
dependencies = ["frappe"]
[build-system]
requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core:buildapi"Publishing Steps
1. Create account at https://frappecloud.com/marketplace 2. Add your GitHub repository 3. Configure supported versions (v14, v15) 4. Submit for review 5. After approval, app appears in Marketplace
11. App Update Lifecycle on Client Sites
# Pull latest code
bench update --pull
# Or update specific app
cd apps/my_custom_app && git pull origin main && cd ../..
# Then migrate (runs patches + syncs schema)
bench --site mysite migrate
# Rebuild assets
bench build --app my_custom_app
# Restart workers
bench restartThe bench update command wraps: backup → pull → requirements → migrate → build → restart.
ALWAYS take a backup before running bench update on production. ALWAYS test updates on staging before applying to production.
See Also
- references/examples.md — Complete app scaffolding examples
- references/anti-patterns.md — Common mistakes
- references/workflows.md — Step-by-step workflows
- references/module-workspace-shipping.md — Module Def, modules.txt, and workspace shipping
frappe-syntax-hooks— Complete hooks.py referencefrappe-core-database— Database and migration patternsfrappe-impl-workspace— Workspace builder, components, and customization
App Lifecycle Anti-Patterns
AP-1: Forgetting to Add Module to modules.txt
WRONG: Create a new module directory but skip modules.txt:
my_app/
├── custom_reports/
│ ├── __init__.py
│ └── report/...
├── modules.txt # ← "Custom Reports" not listed hereRIGHT:
# modules.txt
My Custom App
Custom ReportsWhy: Frappe reads modules.txt to register modules. Missing modules cause "Module not found" errors during migration and DocType creation.
AP-2: Not Declaring required_apps
WRONG:
# hooks.py — no dependency declaration
app_name = "my_app"RIGHT:
# hooks.py
app_name = "my_app"
required_apps = ["frappe", "erpnext"]Why: Without required_apps, users can install your app without ERPNext, causing import errors and broken DocType links. ALWAYS declare all app dependencies.
AP-3: Running migrate Without Backup on Production
WRONG:
bench --site production-site migrateRIGHT:
bench --site production-site backup
bench --site production-site migrateWhy: Migrations can fail mid-way, leaving the database in an inconsistent state. ALWAYS backup before migrating production sites.
AP-4: Editing DocTypes Without Developer Mode
WRONG: Manually editing DocType JSON files and running migrate.
RIGHT:
# Enable developer mode first
bench set-config -g developer_mode 1
# Edit DocTypes in Desk UI
# Frappe auto-generates JSON files
bench --site mysite migrateWhy: Frappe generates DocType JSON from the UI. Manual edits may have incorrect checksums and be overwritten. ALWAYS use the Desk UI in developer mode.
AP-5: Putting Patches in Wrong Section
WRONG — accessing a new field in pre_model_sync:
[pre_model_sync]
my_app.patches.v1_2.fill_new_field # New field doesn't exist yet!RIGHT:
[post_model_sync]
my_app.patches.v1_2.fill_new_field # Schema already synced, field existsWhy: [pre_model_sync] patches run BEFORE schema changes. If your patch needs a field that was just added to the DocType JSON, it MUST be in [post_model_sync].
AP-6: Forgetting frappe.reload_doc in Pre-Sync Patches
WRONG:
def execute():
# Tries to access new field, but DocType meta is stale
frappe.db.set_value("My DocType", "doc1", "new_field", "value")RIGHT:
def execute():
frappe.reload_doc("module_name", "doctype", "my_doctype")
frappe.db.set_value("My DocType", "doc1", "new_field", "value")Why: In [pre_model_sync], the DocType meta is from the OLD schema. Call frappe.reload_doc() to load the new JSON before accessing new fields.
AP-7: Hardcoding Site Name in Scripts
WRONG:
site_config = frappe.get_site_config("mysite.localhost")RIGHT:
site_config = frappe.get_site_config() # Uses current site contextWhy: Site name differs between dev, staging, and production. NEVER hardcode site names. Frappe ALWAYS knows the current site from context.
AP-8: Not Running bench build After JS Changes
WRONG:
# Edit JS file
vim apps/my_app/my_app/public/js/custom.js
# Expect changes to appear immediatelyRIGHT:
# Edit JS file, then build
bench build --app my_app
# Or use watch mode during development
bench watchWhy: JS/CSS files must be compiled into bundles. Without bench build, Desk serves stale cached assets. Use bench watch during development for auto-rebuild.
AP-9: Using bench update Without Testing on Staging
WRONG:
# Directly on production
bench updateRIGHT:
# On staging first
bench update
bench --site staging run-tests
# Verify in browser
# Then on production
bench --site production backup
bench updateWhy: bench update pulls ALL app updates, runs ALL pending migrations, and rebuilds ALL assets. A breaking change in any app can take down the site. ALWAYS test on staging first.
AP-10: Committing __pycache__ and .pyc Files
WRONG:
git add -A # Includes __pycache__/ directoriesRIGHT — ensure .gitignore contains:
__pycache__/
*.pyc
*.pyoWhy: Compiled Python files are platform-specific and cause merge conflicts. ALWAYS add __pycache__/ to .gitignore before the first commit.
App Lifecycle Examples
Complete hooks.py for a Custom App
# my_custom_app/hooks.py
app_name = "my_custom_app"
app_title = "My Custom App"
app_publisher = "Your Company"
app_description = "Custom ERPNext extensions for inventory management"
app_email = "dev@yourcompany.com"
app_license = "MIT"
app_version = "1.0.0"
# App dependencies — ALWAYS declare these
required_apps = ["frappe", "erpnext"]
# Asset injection
app_include_js = "my_custom_app.bundle.js" # v15+
app_include_css = "my_custom_app.bundle.css" # v15+
# Lifecycle hooks
after_install = "my_custom_app.setup.after_install"
after_migrate = "my_custom_app.setup.after_migrate"
# DocType overrides
doctype_js = {
"Sales Invoice": "public/js/sales_invoice.js",
"Customer": "public/js/customer.js"
}
# Document events
doc_events = {
"Sales Invoice": {
"on_submit": "my_custom_app.events.sales_invoice.on_submit",
"on_cancel": "my_custom_app.events.sales_invoice.on_cancel"
},
"*": {
"after_insert": "my_custom_app.events.common.log_creation"
}
}
# Scheduled tasks
scheduler_events = {
"daily": [
"my_custom_app.tasks.daily_sync"
],
"hourly": [
"my_custom_app.tasks.check_stock_levels"
],
"cron": {
"0 9 * * 1": [
"my_custom_app.tasks.weekly_report"
]
}
}
# Permissions
permission_query_conditions = {
"Custom DocType": "my_custom_app.permissions.get_conditions"
}
has_permission = {
"Custom DocType": "my_custom_app.permissions.has_permission"
}
# Fixtures — export these DocTypes as JSON
fixtures = [
"Custom Field",
{"dt": "Property Setter", "filters": [["module", "=", "My Custom App"]]},
{"dt": "Role", "filters": [["name", "in", ["Inventory Analyst"]]]},
]Complete __init__.py
# my_custom_app/__init__.py
__version__ = "1.0.0"Complete patches.txt
# my_custom_app/patches.txt
[pre_model_sync]
# v1.0 — Initial release patches
my_custom_app.patches.v1_0.create_custom_roles
[post_model_sync]
# v1.0 — Data migration after schema sync
my_custom_app.patches.v1_0.set_default_values
my_custom_app.patches.v1_0.migrate_legacy_data
# v1.1 — Feature additions
my_custom_app.patches.v1_1.add_inventory_categories
my_custom_app.patches.v1_1.update_existing_itemsExample Patch: Pre-Model-Sync
# my_custom_app/patches/v1_0/create_custom_roles.py
import frappe
def execute():
"""Create custom roles before DocType sync so permissions are ready."""
if not frappe.db.exists("Role", "Inventory Analyst"):
doc = frappe.new_doc("Role")
doc.role_name = "Inventory Analyst"
doc.desk_access = 1
doc.insert(ignore_permissions=True)
frappe.db.commit()Example Patch: Post-Model-Sync
# my_custom_app/patches/v1_1/update_existing_items.py
import frappe
def execute():
"""Set default category for items that don't have one (new field added in v1.1)."""
# No need for reload_doc — post_model_sync already applied schema
frappe.db.sql("""
UPDATE `tabItem`
SET custom_category = 'General'
WHERE custom_category IS NULL OR custom_category = ''
""")
frappe.db.commit()Example after_install Setup
# my_custom_app/setup.py (not the package setup.py)
import frappe
def after_install():
"""Run after bench --site SITE install-app my_custom_app."""
create_default_settings()
setup_email_templates()
def create_default_settings():
if not frappe.db.exists("My App Settings", "My App Settings"):
doc = frappe.new_doc("My App Settings")
doc.enabled = 1
doc.sync_interval = 60
doc.insert(ignore_permissions=True)
frappe.db.commit()
def setup_email_templates():
templates = [
{"name": "Stock Alert", "subject": "Low Stock Alert: {item_name}",
"response": "Item {item_name} has fallen below minimum stock level."}
]
for tmpl in templates:
if not frappe.db.exists("Email Template", tmpl["name"]):
doc = frappe.new_doc("Email Template")
doc.update(tmpl)
doc.insert(ignore_permissions=True)
frappe.db.commit()modules.txt Example
My Custom App
Inventory Extensions
Custom ReportsALWAYS add a new line to modules.txt when creating a new module. Frappe reads this file to register modules.
pyproject.toml (v15+ Full Example)
[project]
name = "my_custom_app"
dynamic = ["version"]
description = "Custom ERPNext extensions for inventory management"
authors = [
{name = "Your Company", email = "dev@yourcompany.com"}
]
requires-python = ">=3.10,<3.13"
readme = "README.md"
license = {text = "MIT"}
dependencies = []
[build-system]
requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core:buildapi"
[tool.bench.dev-dependencies]
pytest = "~=7.4"
coverage = "~=7.3"setup.py (v14 Full Example)
from setuptools import setup, find_packages
with open("requirements.txt") as f:
install_requires = f.read().strip().split("\n")
setup(
name="my_custom_app",
version="1.0.0",
description="Custom ERPNext extensions for inventory management",
author="Your Company",
author_email="dev@yourcompany.com",
packages=find_packages(),
zip_safe=False,
include_package_data=True,
install_requires=install_requires,
python_requires=">=3.8",
)Module Definition & Workspace Shipping
How to configure modules within a Frappe app and ship workspaces as part of the app distribution.
For workspace builder UI, components, and customization, see skill frappe-impl-workspace.This reference focuses on the ops side: module registration, directory layout, and shipping.
---
1. Module Def DocType
Every Frappe module is represented by a Module Def document. When you create a module in developer mode, Frappe creates a module.json file in the module directory.
Module Def Fields
| Field | Type | Purpose |
|---|---|---|
module_name | Data | Human-readable name (e.g., "Inventory Management") |
app_name | Data | The app this module belongs to (e.g., "my_custom_app") |
custom | Check | 0 for standard modules shipped with an app, 1 for user-created |
restrict_to_domain | Link → Domain | Limit module visibility to a specific domain |
module.json Example
{
"app_name": "my_custom_app",
"category": "",
"color": "",
"custom": 0,
"docstatus": 0,
"doctype": "Module Def",
"icon": "",
"idx": 0,
"module_name": "Inventory Management",
"name": "Inventory Management",
"restrict_to_domain": ""
}This file lives at: my_custom_app/inventory_management/module.json
---
2. modules.txt — Module Registration
The modules.txt file in the app's inner package directory is the single source of truth for which modules an app provides. Frappe reads this file during installation to create Module Def documents.
Location
apps/my_custom_app/my_custom_app/modules.txtFormat
One module name per line, matching the module_name field exactly:
Inventory Management
Warehouse Operations
Custom ReportsRules
- ALWAYS add a new line to
modules.txtwhen creating a new module - The name in
modules.txtMUST match themodule_nameinmodule.json - The directory name is the snake_case version of the module name (e.g.,
inventory_management/) - Order in
modules.txtdetermines the order modules appear in the module list - NEVER remove a module from
modules.txtif DocTypes depend on it — this breaks installations
What Happens on Install
When bench --site mysite install-app my_custom_app runs:
1. Frappe reads modules.txt 2. For each module name, creates a Module Def document (if it does not exist) 3. Sets app_name to the installing app 4. Sets custom = 0 (standard module)
---
3. Module Directory Structure
Each module declared in modules.txt MUST have a corresponding directory:
my_custom_app/
├── modules.txt
├── inventory_management/
│ ├── __init__.py
│ ├── module.json # Module Def export
│ ├── doctype/ # DocTypes belonging to this module
│ │ ├── warehouse_item/
│ │ │ ├── warehouse_item.json
│ │ │ ├── warehouse_item.py
│ │ │ └── warehouse_item.js
│ │ └── stock_entry_custom/
│ │ └── ...
│ ├── report/ # Reports belonging to this module
│ │ └── stock_summary/
│ │ └── ...
│ ├── workspace/ # Workspaces for this module
│ │ └── inventory_management/
│ │ └── inventory_management.json
│ ├── page/ # Custom pages
│ ├── dashboard_chart/ # Dashboard chart definitions
│ └── number_card/ # Number card definitionsDocType ↔ Module Association
Every DocType belongs to exactly one module. The module field in the DocType JSON determines this:
{
"doctype": "DocType",
"name": "Warehouse Item",
"module": "Inventory Management",
...
}ALWAYS set the module field to a module declared in your app's modules.txt. If you set it to another app's module (e.g., "Stock"), the DocType will be exported to that app's directory instead of yours.
---
4. Module Icons and Colors
Module Def supports icon and color fields, but these are primarily cosmetic in v14+ where Workspace icons have replaced module-based navigation.
Setting via module.json
{
"module_name": "Inventory Management",
"icon": "stock",
"color": "#3498db"
}Where Icons Appear
- Workspace sidebar: The workspace's own
iconfield takes precedence - Module-based views (legacy): Uses Module Def icon
- Search results: Module icon shown next to DocType results
In practice, ALWAYS set the icon on the Workspace document rather than relying on Module Def. The workspace icon is what users actually see in the sidebar.
---
5. Module-Based Permissions
Modules participate in the permission system through Domain restrictions and Module visibility:
Domain Restriction
{
"module_name": "Manufacturing",
"restrict_to_domain": "Manufacturing"
}When a domain is deactivated, all modules restricted to that domain become invisible. Their DocTypes and workspaces are hidden from navigation.
Module Visibility per User
Users can enable/disable modules in their user settings (Setup > User > Module Access). This controls:
- Which modules appear in the sidebar
- Which workspaces are visible
- Which module-specific search results appear
This does NOT override DocType-level permissions. A user with read access to a DocType can still access it via URL even if the module is hidden.
Role-Based Module Access
Module visibility can also be controlled through the Block Module DocType:
# Programmatically block a module for a user
frappe.get_doc({
"doctype": "Block Module",
"parent": "user@example.com",
"parenttype": "User",
"parentfield": "block_modules",
"module": "Inventory Management"
}).insert()---
6. Workspace Shipping — Ops Perspective
This section covers the operational aspects of shipping workspaces. For workspace builder UI details, see frappe-impl-workspace and its reference references/shipping-with-app.md.
Directory Convention
{app_name}/{module_snake_case}/workspace/{workspace_snake_case}/{workspace_snake_case}.jsonExample:
my_custom_app/inventory_management/workspace/inventory_dashboard/inventory_dashboard.jsonAuto-Export in Developer Mode
When developer_mode = 1:
1. Edit the workspace in the Workspace Builder UI 2. Click Save 3. Frappe writes the JSON to the app directory based on the workspace's module field 4. The file path is: {app}/{module}/workspace/{name}/{name}.json
ALWAYS verify the module field belongs to YOUR app. If it points to another app's module, the JSON exports to that app's directory.
Manual Export
# In bench console
bench --site mysite console
>>> ws = frappe.get_doc("Workspace", "Inventory Dashboard")
>>> ws.export_doc()Shipping Dependent Documents
Workspace JSON references Number Cards, Dashboard Charts, and Custom HTML Blocks by name. These documents do NOT auto-export with the workspace.
ALWAYS ship dependencies using fixtures in hooks.py:
# hooks.py
fixtures = [
{
"dt": "Number Card",
"filters": [["module", "=", "Inventory Management"]]
},
{
"dt": "Dashboard Chart",
"filters": [["module", "=", "Inventory Management"]]
},
{
"dt": "Custom HTML Block",
"filters": [["name", "in", [
"Inventory Overview Widget",
"Stock Alert Panel"
]]]
}
]Then export:
bench --site mysite export-fixtures
# Creates: my_custom_app/fixtures/number_card.json
# Creates: my_custom_app/fixtures/dashboard_chart.json
# Creates: my_custom_app/fixtures/custom_html_block.jsonInstallation Order
Frappe processes documents in this order during install-app:
1. Module Def documents (from modules.txt and module.json) 2. DocTypes and their schemas 3. Fixtures (Number Cards, Charts, Custom Blocks from hooks.py) 4. Workspaces (from workspace/ directories)
This order guarantees dependencies exist before the workspace references them.
ALWAYS Run After Changes
# After modifying workspace or its dependencies
bench --site mysite export-fixtures # Export fixture changes
bench build --app my_custom_app # Rebuild if JS assets changed
git -C apps/my_custom_app add -A # Stage all changesCommon Pitfalls
| Mistake | Result | Fix |
|---|---|---|
Module not in modules.txt | Workspace has no parent module, export fails | Add module to modules.txt, run bench migrate |
Forgot export-fixtures | Number Cards/Charts missing on target site | Run bench export-fixtures and commit JSON files |
Workspace module points to another app | JSON exported to wrong app directory | Change workspace's module to your own module |
for_user set in workspace JSON | Workspace is private, invisible to other users | Remove for_user field from the JSON |
| Fixture filter too broad | Exports other apps' Number Cards/Charts | Use module or name-based filters |
---
7. Complete Module + Workspace Shipping Checklist
- [ ] Module name added to
modules.txt - [ ] Module directory exists with
__init__.pyandmodule.json - [ ] All DocTypes have correct
modulefield pointing to your module - [ ] Workspace JSON exists at
{module}/workspace/{name}/{name}.json - [ ] Workspace
modulefield matches your module - [ ] Workspace
for_useris NOT set - [ ] All Number Cards referenced by workspace are in fixtures
- [ ] All Dashboard Charts referenced by workspace are in fixtures
- [ ] All Custom HTML Blocks referenced by workspace are in fixtures
- [ ]
bench export-fixtureshas been run - [ ] Fixture JSON files are committed
- [ ] Tested on a fresh site:
bench new-site test && bench install-app my_custom_app - [ ] Workspace renders correctly after
bench migrateon the test site
App Lifecycle Workflows
Workflow 1: Create a New Frappe App from Scratch
Step 1 — Scaffold
cd ~/frappe-bench
bench new-app my_inventory_app
# Answer prompts: title, description, publisher, email, licenseStep 2 — Enable Developer Mode
bench set-config -g developer_mode 1
bench startStep 3 — Create First Module
1. Open Desk > Module Def > New 2. Set Module Name = "Inventory Extensions" 3. Save
Verify modules.txt now contains "Inventory Extensions".
Step 4 — Create First DocType
1. Open Desk > DocType > New 2. Set Name, Module = "Inventory Extensions" 3. Add fields, set naming, permissions 4. Save (generates JSON in my_inventory_app/inventory_extensions/doctype/)
Step 5 — Install on Site
bench --site mysite install-app my_inventory_appStep 6 — Initialize Git
cd apps/my_inventory_app
git init
echo "__pycache__/\n*.pyc\nnode_modules/\n.eggs/" > .gitignore
git add -A
git commit -m "feat: initial app scaffold"Step 7 — Declare Dependencies
Edit hooks.py:
required_apps = ["frappe", "erpnext"]---
Workflow 2: Install an Existing App from GitHub
Step 1 — Get the App
# Public repo
bench get-app https://github.com/org/custom_app
# Private repo (SSH key must be configured)
bench get-app git@github.com:org/private_app.git
# Specific branch
bench get-app https://github.com/org/custom_app --branch v15Step 2 — Install on Site
bench --site mysite install-app custom_appStep 3 — Build Assets
bench build --app custom_appStep 4 — Verify
bench --site mysite list-apps
# Should show: frappe, erpnext, custom_app---
Workflow 3: Write and Deploy a Patch
Step 1 — Create Patch File
mkdir -p apps/my_app/my_app/patches/v1_2/Create apps/my_app/my_app/patches/v1_2/update_item_defaults.py:
import frappe
def execute():
frappe.reload_doc("inventory_extensions", "doctype", "custom_item_settings")
items = frappe.get_all("Custom Item Settings",
filters={"default_warehouse": ["is", "not set"]},
pluck="name")
for item in items:
frappe.db.set_value("Custom Item Settings", item,
"default_warehouse", "Main Warehouse - CO")
frappe.db.commit()Step 2 — Register in patches.txt
Add to patches.txt:
[post_model_sync]
my_app.patches.v1_2.update_item_defaultsStep 3 — Test Locally
bench --site mysite migrate
# Check logs for errorsStep 4 — Verify in Console
bench --site mysite console
>>> frappe.get_all("Patch Log", filters={"patch": ["like", "%update_item_defaults%"]})
# Should return one entry confirming the patch ran---
Workflow 4: Set Up Production Deployment
Step 1 — Disable Developer Mode
bench set-config -g developer_mode 0Step 2 — Build Production Assets
bench buildStep 3 — Setup Production Services
sudo bench setup production LINUX_USERNAME
# This configures:
# - nginx (reverse proxy, SSL, static files)
# - supervisor (gunicorn workers, background workers, socketio)
# - fail2ban (optional, brute-force protection)Step 4 — Enable SSL (Optional)
sudo bench setup lets-encrypt mysite.comStep 5 — Verify
sudo supervisorctl status
# All processes should show RUNNING
curl -I https://mysite.com
# Should return 200 OK---
Workflow 5: Update an App on Production
Step 1 — Backup
bench --site mysite backup --with-filesStep 2 — Pull Updates
# Update all apps
bench update --pull
# Or update specific app only
cd apps/my_custom_app
git pull origin main
cd ../..Step 3 — Install Requirements
bench setup requirements --python
bench setup requirements --nodeStep 4 — Migrate
bench --site mysite migrateStep 5 — Build and Restart
bench build
bench restartStep 6 — Verify
bench version
bench --site mysite list-apps---
Workflow 6: Debug a Failed Migration
Step 1 — Read the Error
bench --site mysite migrate 2>&1 | tail -50
# Look for the specific patch or DocType that failedStep 2 — Open Console
bench --site mysite consoleStep 3 — Test the Failing Patch
from my_app.patches.v1_2.problematic_patch import execute
try:
execute()
frappe.db.commit()
except Exception as e:
frappe.db.rollback()
print(f"Error: {e}")Step 4 — Fix and Re-run
After fixing the patch code:
bench --site mysite migrateIf the patch already logged as "run" but failed, re-run by appending a version comment:
# In patches.txt, change:
my_app.patches.v1_2.problematic_patch
# To:
my_app.patches.v1_2.problematic_patch #2025-03-20-fix---
Workflow 7: Publish App to Frappe Marketplace
Step 1 — Verify App Quality
# All tests pass
bench --site mysite run-tests --app my_custom_app
# Version is set
python -c "import my_custom_app; print(my_custom_app.__version__)"
# README exists
cat apps/my_custom_app/README.mdStep 2 — Push to GitHub
cd apps/my_custom_app
git remote add origin https://github.com/org/my_custom_app.git
git push -u origin mainStep 3 — Create Release Tag
git tag -a v1.0.0 -m "v1.0.0: Initial release"
git push origin v1.0.0Step 4 — Submit to Marketplace
1. Go to https://frappecloud.com/marketplace 2. Log in or create account 3. Click "Publish New App" 4. Enter GitHub repository URL 5. Select supported Frappe versions 6. Submit for review
Step 5 — Maintain
- ALWAYS bump
__version__in__init__.pybefore creating a new tag - ALWAYS update
patches.txtfor data migrations - ALWAYS test on a clean site:
bench new-site test && bench --site test install-app my_custom_app