frappe-impl-customapp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited frappe-impl-customapp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Workflow for building a custom Frappe app from scratch. For exact syntax, see frappe-syntax-customapp.
Version: v14/v15/v16 compatible
WHAT CHANGES DO YOU NEED?
|
+-- Add fields to existing DocType?
| +-- NO APP NEEDED: Custom Field + Property Setter
|
+-- Simple automation/validation (<50 lines)?
| +-- NO APP NEEDED: Server Script or Client Script
|
+-- Complex business logic, new DocTypes, or Python code?
| +-- YES: Create custom app
|
+-- Integration with external system (needs imports)?
| +-- YES: Custom app REQUIRED (Server Scripts block imports)
|
+-- Custom reports with complex queries?
| +-- Script Report (no app) vs Query Report (app optional)Rule: ALWAYS start with the simplest solution. Server Scripts + Custom Fields solve 70% of needs without a custom app.
cd ~/frappe-bench
bench new-app my_app
# Prompts: Title, Description, Publisher, Email, LicenseALWAYS verify immediately:
# my_app/my_app/__init__.py MUST have:
__version__ = "0.0.1"[build-system]
requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core.buildapi"
[project]
name = "my_app"
authors = [{ name = "Your Company", email = "[email protected]" }]
description = "Your app description"
requires-python = ">=3.10"
readme = "README.md"
dynamic = ["version"]
dependencies = [
"requests>=2.28.0" # Only PyPI packages here
]
[tool.bench.frappe-dependencies]
frappe = ">=15.0.0,<16.0.0"
# erpnext = ">=15.0.0,<16.0.0" # Only if neededRule: NEVER put frappe or erpnext in [project].dependencies -- they are NOT on PyPI.
app_name = "my_app"
app_title = "My App"
app_publisher = "Your Company"
app_description = "Description"
app_email = "[email protected]"
app_license = "MIT"
required_apps = ["frappe"] # Or ["frappe", "erpnext"]
fixtures = [] # Configured laterRule: ALWAYS declare required_apps with all dependencies.
# my_app/my_app/modules.txt
My App| App Size | Module Strategy |
|---|---|
| 1-5 DocTypes | ONE module with app name |
| 6-15 DocTypes | 2-4 modules by functional area |
| 15+ DocTypes | Modules by business domain |
Rule: Each DocType belongs to EXACTLY one module. Module name in modules.txt maps to directory: My Custom App --> my_custom_app/.
mkdir -p my_app/my_app/new_module/doctype
touch my_app/my_app/new_module/__init__.py
# Add "New Module" to modules.txt
bench --site mysite migrate# Install app on site
bench --site mysite install-app my_app
# Create DocType (via UI recommended, or CLI)
bench --site mysite new-doctype "My Document" --module "My App"This creates:
my_app/my_app/doctype/my_document/
+-- my_document.json # DocType definition
+-- my_document.py # Controller
+-- my_document.js # Client script
+-- test_my_document.py # Testsdoc_events = {
"Sales Invoice": {
"validate": "my_app.events.sales_invoice.validate",
"on_submit": "my_app.events.sales_invoice.on_submit"
}
}extend_doctype_class = {
"Sales Invoice": "my_app.overrides.sales_invoice.CustomSalesInvoice"
}Rule: ALWAYS call super().method() when overriding lifecycle methods in v16.
scheduler_events = {
"daily": ["my_app.tasks.daily_cleanup"],
"cron": {"0 9 * * 1-5": ["my_app.tasks.morning_report"]}
}See frappe-impl-hooks and frappe-impl-scheduler for complete patterns.
mkdir -p my_app/my_app/patches/v1_0
touch my_app/my_app/patches/__init__.py
touch my_app/my_app/patches/v1_0/__init__.py# my_app/my_app/patches/v1_0/populate_defaults.py
import frappe
def execute():
if not frappe.db.has_column("My DocType", "target_field"):
return # Skip if not applicable
batch_size = 1000
offset = 0
while True:
records = frappe.get_all("My DocType",
limit_page_length=batch_size, limit_start=offset)
if not records:
break
for r in records:
frappe.db.set_value("My DocType", r.name,
"target_field", "default", update_modified=False)
frappe.db.commit()
offset += batch_size[pre_model_sync]
# Patches that run BEFORE schema changes (backup data from deleted fields)
[post_model_sync]
# Patches that run AFTER schema changes (populate new fields)
my_app.patches.v1_0.populate_defaultsRules:
fixtures = [
{"dt": "Custom Field", "filters": [["module", "=", "My App"]]},
{"dt": "Property Setter", "filters": [["module", "=", "My App"]]},
{"dt": "Role", "filters": [["name", "in", ["My App User", "My App Manager"]]]},
{"dt": "Workflow", "filters": [["document_type", "=", "My DocType"]]},
"My Category", # All records of your own config DocType
]bench --site mysite export-fixtures --app my_app
ls my_app/my_app/fixtures/
# custom_field.json, property_setter.json, etc.Rules:
bench migrate# After schema changes (DocType fields, hooks.py, patches)
bench --site mysite migrate
# After JS/CSS changes
bench build --app my_app
# After Python changes (controllers, events)
bench --site mysite clear-cache
# Full restart (production)
bench restart
# Watch mode (development)
bench watch # Auto-rebuilds on file changes1. Edit code/DocType
2. bench --site mysite migrate (if schema changed)
3. bench build --app my_app (if JS/CSS changed)
4. bench --site mysite clear-cache (if Python changed)
5. Test in browser
6. Repeat# Run all tests
bench --site mysite run-tests --app my_app
# Run specific test
bench --site mysite run-tests --module my_app.my_module.doctype.my_doctype.test_my_doctype
# Run with verbose output
bench --site mysite run-tests --app my_app -vSee frappe-testing-unit for writing test cases.
cd apps/my_app
git init && git add . && git commit -m "Initial commit"
git remote add origin https://github.com/org/my_app.git
git push -u origin main# On target bench
bench get-app https://github.com/org/my_app.git
bench --site target-site install-app my_app
bench --site target-site migrate# my_app/my_app/__init__.py
__version__ = "1.0.0" # Semantic versioning: MAJOR.MINOR.PATCH| Change Type | Version Bump | Example |
|---|---|---|
| Breaking changes | MAJOR | 1.x -> 2.0.0 |
| New features | MINOR | 1.1.x -> 1.2.0 |
| Bug fixes | PATCH | 1.2.0 -> 1.2.1 |
# hooks.py
required_apps = ["frappe", "erpnext"] # Install order matters# pyproject.toml
[tool.bench.frappe-dependencies]
frappe = ">=15.0.0,<16.0.0"
erpnext = ">=15.0.0,<16.0.0"[project]
dependencies = ["requests>=2.28.0", "pandas>=1.5.0"]Rule: NEVER create circular dependencies between apps.
| Aspect | v14 | v15 | v16 |
|---|---|---|---|
| Build config | setup.py | pyproject.toml | pyproject.toml |
| DocType extension | doc_events | doc_events | extend_doctype_class preferred |
| Python minimum | 3.10 | 3.10 | 3.11 |
| Patch format | INI sections | INI sections | INI sections |
extend_doctype_class hook: Cleaner extension via mixinsbench new-app - NEVER create structure manually__version__ in __init__.pydynamic = ["version"] in pyproject.toml[project].dependenciesfrappe.db.commit() in large patches| File | Contents |
|---|---|
| workflows.md | 8 step-by-step implementation guides |
| decision-tree.md | Complete decision flowcharts |
| examples.md | 5 complete working app examples |
| anti-patterns.md | Common mistakes to avoid |
frappe-syntax-customapp - Exact syntax referencefrappe-syntax-hooks - Hooks configuration syntaxfrappe-impl-hooks - Hook implementation patternsfrappe-core-database - Database operations for patchesfrappe-impl-scheduler - Scheduled task implementationfrappe-ops-bench - Bench commands referencefrappe-ops-app-lifecycle - App versioning and release managementfrappe-testing-unit - Writing tests for your appfrappe-testing-cicd - CI/CD pipeline for app testing~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.