Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
openaec-foundation avatar

Frappe Syntax Hooks

  • 24 installs
  • 159 repo stars
  • Updated July 8, 2026
  • openaec-foundation/frappe_claude_skill_package

Helps with ai & agent building tasks.

About

frappe-syntax-hooks is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.

  • frappe-syntax-hooks
  • AI & Agent Building
  • AI-coding skill

Frappe Syntax Hooks by the numbers

  • 24 all-time installs (skills.sh)
  • Ranked #9,912 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-syntax-hooks

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs24
repo stars159
Last updatedJuly 8, 2026
Repositoryopenaec-foundation/frappe_claude_skill_package

What it does

Helps with ai & agent building tasks.

Files

SKILL.mdMarkdownGitHub ↗

Frappe Configuration Hooks (hooks.py)

Configuration hooks in hooks.py enable custom apps to extend Frappe/ERPNext behavior. This skill covers ALL non-document-event hooks. For doc_events (validate, on_submit, on_update, etc.), see frappe-syntax-hooks-events.

Quick Reference: Hook Categories

CategoryKey HooksReference
App metadataapp_name, app_title, required_appsBelow
Frontend assetsapp_include_js/css, web_include_js/cssBelow
Install/migratebefore_install, after_install, after_migrateBelow
Schedulerhourly, daily, cron, *_longscheduler-events.md
Session/authon_login, on_logout, auth_hooksbootinfo.md
Request middlewarebefore_request, after_requestrequest-lifecycle.md
Permissionspermission_query_conditions, has_permissionpermissions.md
DocType overridesoverride_doctype_class, doctype_jsoverrides.md
Website/portalwebsite_route_rules, portal_menu_itemsrequest-lifecycle.md
File handlingbefore_write_file, write_fileBelow
Emailoverride_email_send, default_mail_footerBelow
PDFpdf_header_html, pdf_footer_htmlBelow
Jinjajinja.methods, jinja.filtersBelow
Boot/client dataextend_bootinfo, notification_configbootinfo.md
Data/fixturesfixtures, global_search_doctypesBelow
Method overridesoverride_whitelisted_methods, standard_queriesoverrides.md

---

Decision Tree: Which Hook Do I Need?

What do you want to achieve?
|
+-- ADD JS/CSS to desk or portal?
|   +-- Desk --> app_include_js / app_include_css
|   +-- Portal --> web_include_js / web_include_css
|   +-- Specific form --> doctype_js
|   +-- List view --> doctype_list_js
|
+-- RUN periodic background tasks?
|   +-- < 5 min execution --> hourly / daily / weekly / monthly
|   +-- 5-25 min execution --> hourly_long / daily_long / etc.
|   +-- Exact time needed --> cron
|   See: frappe-syntax-hooks > scheduler-events.md
|
+-- SEND data to client at page load?
|   +-- extend_bootinfo
|
+-- MODIFY controller of existing DocType?
|   +-- v16+ --> extend_doctype_class (RECOMMENDED)
|   +-- v14/v15 --> override_doctype_class (last app wins)
|
+-- MODIFY API endpoint?
|   +-- override_whitelisted_methods
|
+-- CUSTOMIZE permissions?
|   +-- List filtering --> permission_query_conditions
|   +-- Document-level --> has_permission
|
+-- REACT to document save/submit/delete?
|   +-- See frappe-syntax-hooks-events skill
|
+-- EXPORT/IMPORT configuration?
|   +-- fixtures
|
+-- SETUP on install or migrate?
|   +-- after_install / after_migrate
|
+-- ADD custom Jinja functions?
|   +-- jinja.methods / jinja.filters
|
+-- CUSTOMIZE website routing?
|   +-- website_route_rules
|   See: request-lifecycle.md for full routing pipeline
|
+-- INTERCEPT every request/response?
|   +-- before_request / after_request
|   See: request-lifecycle.md for lifecycle flow
|
+-- CUSTOM page rendering?
|   +-- page_renderer hook
|   See: request-lifecycle.md for renderer architecture

---

1. App Metadata Hooks

ALWAYS include these in every hooks.py:

app_name = "myapp"
app_title = "My App"
app_publisher = "My Company"
app_description = "Custom ERPNext extensions"
app_email = "info@mycompany.com"
app_license = "MIT"
required_apps = ["erpnext"]  # Declare dependencies

---

2. Frontend Asset Injection

# Desk (backend UI) assets — loaded on EVERY desk page
app_include_js = "/assets/myapp/js/myapp.min.js"       # string or list
app_include_css = "/assets/myapp/css/myapp.min.css"

# Website/portal assets — loaded on EVERY web page
web_include_js = "/assets/myapp/js/web.min.js"
web_include_css = "/assets/myapp/css/web.min.css"

# Web form specific assets
webform_include_js = {"My Web Form": "public/js/my_webform.js"}
webform_include_css = {"My Web Form": "public/css/my_webform.css"}

# Form script extensions (extend OTHER apps' forms)
doctype_js = {"Sales Invoice": "public/js/sales_invoice.js"}

# List view script extensions
doctype_list_js = {"Sales Invoice": "public/js/sales_invoice_list.js"}

# Custom sounds
sounds = [{"name": "alert", "src": "/assets/myapp/sounds/alert.mp3", "volume": 0.5}]

NEVER put heavy libraries in app_include_js — they load on every page.

---

3. Installation & Migration Lifecycle

before_install = "myapp.setup.before_install"
after_install = "myapp.setup.after_install"
after_sync = "myapp.setup.after_sync"            # After fixture sync
before_migrate = "myapp.setup.before_migrate"
after_migrate = "myapp.setup.after_migrate"
before_uninstall = "myapp.setup.before_uninstall"
after_uninstall = "myapp.setup.after_uninstall"
before_tests = "myapp.setup.seed_test_data"

All accept a single dotted-path string. The function receives no arguments.

---

4. Scheduler Events

See scheduler-events.md for full reference.

scheduler_events = {
    "all": ["myapp.tasks.every_minute"],            # ~60s interval
    "hourly": ["myapp.tasks.hourly_check"],         # default queue, 5 min timeout
    "daily": ["myapp.tasks.daily_report"],
    "weekly": ["myapp.tasks.weekly_cleanup"],
    "monthly": ["myapp.tasks.monthly_summary"],
    "daily_long": ["myapp.tasks.heavy_sync"],       # long queue, 25 min timeout
    "cron": {
        "0 9 * * 1-5": ["myapp.tasks.weekday_morning"]  # cron expression
    }
}

ALWAYS run bench --site sitename migrate after changing scheduler_events. NEVER define task functions with arguments — they receive none.

---

5. Session & Authentication Hooks

on_login = "myapp.auth.on_login"                     # Receives login_manager
on_logout = "myapp.auth.on_logout"                   # No arguments
on_session_creation = "myapp.auth.on_session_creation"  # No arguments
auth_hooks = ["myapp.auth.validate_request"]          # List of validators

Execution order: on_login --> session created --> on_session_creation --> extend_bootinfo.

---

6. Request/Response Middleware

See request-lifecycle.md for the full request lifecycle flow, page renderer architecture, and router API.

before_request = ["myapp.middleware.before_request"]   # List of dotted paths
after_request = ["myapp.middleware.after_request"]
before_job = ["myapp.middleware.before_job"]            # Before background job
after_job = ["myapp.middleware.after_job"]              # After background job

---

7. Permission Hooks

See permissions.md for full reference.

permission_query_conditions = {
    "Sales Invoice": "myapp.permissions.si_query_conditions"
}
has_permission = {
    "Sales Invoice": "myapp.permissions.si_has_permission"
}

ALWAYS check if not user: user = frappe.session.user in handlers. ALWAYS use frappe.db.escape(user) in SQL — NEVER string interpolation. permission_query_conditions works ONLY with get_list, NOT get_all.

---

8. DocType Class Overrides

See overrides.md for full reference.

# v14+ — Full replacement (LAST installed app wins)
override_doctype_class = {
    "Sales Invoice": "myapp.overrides.CustomSalesInvoice"
}

# v16+ — Mixin-based extension (ALL apps coexist) [RECOMMENDED]
extend_doctype_class = {
    "Address": ["myapp.extensions.AddressMixin"]
}

ALWAYS call super().method() in overrides. Forgetting super() breaks core logic.

---

9. Website & Portal Hooks

# URL routing
website_route_rules = [
    {"from_route": "/custom-page/<name>", "to_route": "Custom Page"}
]
website_redirects = [
    {"source": "/old-url", "target": "/new-url"}
]
website_catch_all = "myapp.www.custom_404"

# Homepage
homepage = "my-custom-home"
role_home_page = {"Sales User": "sales-dashboard"}
get_website_user_home_page = "myapp.utils.get_home_page"

# Portal sidebar
portal_menu_items = [{"title": "My Orders", "route": "/orders", "role": "Customer"}]
standard_portal_menu_items = [{"title": "My Items", "route": "/my-items"}]

# Template overrides
base_template = "myapp/templates/base.html"
website_context = {"brand_html": "<b>My Brand</b>"}
update_website_context = "myapp.context.update_context"

---

10. File Handling Hooks

before_write_file = "myapp.files.before_write"      # Pre-save hook
write_file = "myapp.files.custom_write"              # Replace file storage (e.g., S3/CDN)
delete_file_data_content = "myapp.files.custom_delete"  # Replace file deletion

Use write_file to redirect file storage to cloud providers (S3, GCS, Azure Blob).

---

11. Email Hooks

override_email_send = "myapp.email.custom_send"       # Replace email backend
get_sender_details = "myapp.email.get_sender"          # Override From address
default_mail_footer = "myapp.email.get_footer"         # HTML footer for all emails

---

12. PDF Hooks

pdf_header_html = "myapp.pdf.get_header"               # Custom PDF header
pdf_body_html = "myapp.pdf.get_body"                    # Custom PDF body wrapper
pdf_footer_html = "myapp.pdf.get_footer"                # Custom PDF footer
# pdf_generator = "myapp.pdf.generate"                  # [v16+] Replace PDF engine

---

13. Jinja Hooks

# Add custom methods available in Jinja templates
jinja = {
    "methods": ["myapp.jinja_utils.get_balance"],
    "filters": ["myapp.jinja_utils.format_iban"]
}
# myapp/jinja_utils.py
def get_balance(customer):
    """Usage in template: {{ get_balance(doc.customer) }}"""
    return frappe.db.get_value("Customer", customer, "outstanding_amount") or 0

def format_iban(value):
    """Usage in template: {{ bank_account|format_iban }}"""
    if not value: return ""
    return " ".join([value[i:i+4] for i in range(0, len(value), 4)])

---

14. Boot & Client Data

See bootinfo.md for full reference.

extend_bootinfo = "myapp.boot.extend_boot"
notification_config = "myapp.notifications.get_config"

NEVER put secrets/API keys in bootinfo — it is sent to the browser. NEVER run heavy queries in bootinfo — it runs on EVERY page load.

---

15. Data & Fixtures

fixtures = [
    {"dt": "Custom Field", "filters": [["module", "=", "My App"]]},
    {"dt": "Property Setter", "filters": [["module", "=", "My App"]]},
    {"dt": "Role", "filters": [["name", "like", "MyApp%"]]}
]

global_search_doctypes = {"My DocType": {"index": 10}}
ignore_links_on_delete = ["Communication", "Activity Log"]
calendars = ["My Event DocType"]
clear_cache = "myapp.cache.clear_custom_cache"

ALWAYS use filters in fixtures — NEVER export unfiltered (exports everything). NEVER put transactional data (Sales Invoice, Stock Entry) in fixtures.

---

16. Method Overrides

See overrides.md for full reference.

override_whitelisted_methods = {
    "frappe.client.get_count": "myapp.overrides.custom_get_count"
}
standard_queries = {
    "Customer": "myapp.queries.customer_query"
}

ALWAYS match the original method signature exactly when overriding.

---

Version Differences

Hookv14v15v16+
extend_doctype_class----NEW
extend_bootinfoYesYesYes
auth_hooksYesYesYes
after_syncYesYesYes
before_uninstall--YesYes
after_uninstall--YesYes
website_path_resolver--YesYes
All other hooksYesYesYes

---

Critical Rules

1. ALWAYS run bench --site sitename migrate after ANY hooks.py change 2. NEVER import frappe at module level in hooks.py — it runs before init 3. ALWAYS use dotted paths ("myapp.module.function") — NEVER lambdas 4. NEVER commit in hook handlers — Frappe manages transactions 5. ALWAYS test hooks in a dev environment before deploying

---

Anti-Patterns Summary

WrongCorrect
No filters in fixturesALWAYS filter by module/app
Secrets in bootinfoONLY public config in bootinfo
Heavy queries in bootinfoCache or minimize data
get_all with permission hooksUse get_list for permission filtering
Override without super()ALWAYS call super().method() first
Scheduler tasks with argsTasks receive NO arguments
Skip bench migrateALWAYS migrate after hook changes

Full anti-patterns: anti-patterns.md

---

Reference Files

FileContents
hooks.mdComplete hooks catalog by category
scheduler-events.mdScheduler frequencies, cron syntax, timeouts
permissions.mdPermission hooks in detail
overrides.mdDocType class override patterns
bootinfo.mdextend_bootinfo, session hooks, notification_config
examples.mdWorking hooks.py examples for each category
request-lifecycle.mdRequest lifecycle, routing pipeline, page renderers, router API
anti-patterns.mdCommon hook mistakes and corrections

For document lifecycle events (doc_events), see: frappe-syntax-hooks-events

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.