$npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-impl-hooksUse when implementing hooks.py configurations in a Frappe custom app. Covers step-by-step workflows for doc_events, scheduler_events, override/extend_doctype_class, permission hooks, extend_bootinfo, fixtures, asset injection, website hooks, and doctype_js. Prevents broken transa
| 1 | # Frappe Hooks Implementation Workflow |
| 2 | |
| 3 | Step-by-step workflows for implementing hooks.py configurations. For API syntax reference, see `frappe-syntax-hooks`. |
| 4 | |
| 5 | **Version**: v14/v15/v16 (V16-specific features noted) |
| 6 | |
| 7 | --- |
| 8 | |
| 9 | ## Master Decision: What Are You Implementing? |
| 10 | |
| 11 | ``` |
| 12 | WHAT DO YOU WANT TO ACHIEVE? |
| 13 | │ |
| 14 | ├─► React to document lifecycle events? |
| 15 | │ ├─► On OTHER app's DocTypes → doc_events in hooks.py |
| 16 | │ ├─► On YOUR OWN DocTypes → controller methods (preferred) |
| 17 | │ └─► On ALL DocTypes → doc_events with "*" wildcard |
| 18 | │ |
| 19 | ├─► Run code on a schedule? |
| 20 | │ └─► scheduler_events (daily, hourly, cron, etc.) |
| 21 | │ |
| 22 | ├─► Modify an existing DocType's behavior? |
| 23 | │ ├─► V16+: extend_doctype_class (RECOMMENDED) |
| 24 | │ └─► V14/V15: override_doctype_class (last app wins!) |
| 25 | │ |
| 26 | ├─► Override an existing API endpoint? |
| 27 | │ └─► override_whitelisted_methods |
| 28 | │ |
| 29 | ├─► Add custom permission logic? |
| 30 | │ ├─► List filtering → permission_query_conditions |
| 31 | │ └─► Document-level → has_permission |
| 32 | │ |
| 33 | ├─► Send config data to client on page load? |
| 34 | │ └─► extend_bootinfo |
| 35 | │ |
| 36 | ├─► Export/import configuration? |
| 37 | │ └─► fixtures |
| 38 | │ |
| 39 | ├─► Add JS/CSS to desk or portal? |
| 40 | │ ├─► Desk-wide → app_include_js / app_include_css |
| 41 | │ ├─► Portal-wide → web_include_js / web_include_css |
| 42 | │ └─► Specific form → doctype_js |
| 43 | │ |
| 44 | ├─► Customize website/portal behavior? |
| 45 | │ └─► website_context, portal_menu_items, website_route_rules |
| 46 | │ |
| 47 | └─► Hook into session/auth lifecycle? |
| 48 | └─► on_login, on_session_creation, on_logout |
| 49 | ``` |
| 50 | |
| 51 | --- |
| 52 | |
| 53 | ## Workflow 1: Implementing doc_events |
| 54 | |
| 55 | ### When to Use |
| 56 | |
| 57 | Use doc_events when you need to react to document lifecycle events on DocTypes owned by OTHER apps (ERPNext, Frappe core). For YOUR OWN DocTypes, ALWAYS prefer controller methods. |
| 58 | |
| 59 | ### Step-by-Step |
| 60 | |
| 61 | **Step 1: Choose the right event** (see `references/decision-tree.md`) |
| 62 | |
| 63 | ``` |
| 64 | BEFORE save: validate (every save), before_insert (new only) |
| 65 | AFTER save: after_insert (new only), on_update (every save), on_change (any change) |
| 66 | SUBMIT flow: before_submit → on_submit → on_change |
| 67 | CANCEL flow: before_cancel → on_cancel → on_change |
| 68 | DELETE: on_trash (before), after_delete (after) |
| 69 | RENAME: before_rename, after_rename |
| 70 | ``` |
| 71 | |
| 72 | **Step 2: Add to hooks.py** |
| 73 | |
| 74 | ```python |
| 75 | # myapp/hooks.py |
| 76 | doc_events = { |
| 77 | "Sales Invoice": { |
| 78 | "validate": "myapp.events.sales_invoice.validate", |
| 79 | "on_submit": "myapp.events.sales_invoice.on_submit" |
| 80 | } |
| 81 | } |
| 82 | ``` |
| 83 | |
| 84 | **Step 3: Create handler module** |
| 85 | |
| 86 | ```python |
| 87 | # myapp/events/sales_invoice.py |
| 88 | import frappe |
| 89 | |
| 90 | def validate(doc, method=None): |
| 91 | """Changes to doc ARE saved (before-save event).""" |
| 92 | if doc.grand_total < 0: |
| 93 | frappe.throw("Total cannot be negative") |
| 94 | |
| 95 | def on_submit(doc, method=None): |
| 96 | """Document already saved. Use db_set_value for changes.""" |
| 97 | frappe.db.set_value("Sales Invoice", doc.name, |
| 98 | "custom_external_id", create_external(doc)) |
| 99 | ``` |
| 100 | |
| 101 | **Step 4: Deploy** |
| 102 | |
| 103 | ```bash |
| 104 | bench --site sitename migrate |
| 105 | ``` |
| 106 | |
| 107 | **Step 5: Test** |
| 108 | |
| 109 | ```bash |
| 110 | bench --site sitename execute myapp.events.sales_invoice.validate --kwargs '{"doc_name": "INV-001"}' |
| 111 | # Or in bench console: |
| 112 | # doc = frappe.get_doc("Sales Invoice", "INV-001"); doc.save() |
| 113 | ``` |
| 114 | |
| 115 | ### Critical Rules for doc_events |
| 116 | |
| 117 | - **NEVER** call `frappe.db.commit()` inside a doc_event handler — Frappe manages the transaction |
| 118 | - **NEVER** modify `doc` fields in `on_update` — changes are lost; use `frappe.db.set_value()` instead |
| 119 | - **ALWAYS** accept `method=None` as second parameter in handler signature |
| 120 | - **ALWAYS** use rename signature: `def handler(doc, method, old, new, merge)` |
| 121 | - **ALWAYS** run `bench --site sitename migrate` after changing hooks.py |
| 122 | |
| 123 | --- |
| 124 | |
| 125 | ## Workflow 2: Implementing scheduler_events |
| 126 | |
| 127 | ### Step-by-Step |
| 128 | |
| 129 | **Step 1: Choose frequency** |
| 130 | |
| 131 | | Frequency | Short (< 5 min) | Long (5-25 min) | |
| 132 | |-----------|-----------------|------------------| |
| 133 | | Every tick | `all` | — | |
| 134 | | Hourly | `hourly` | `hourly_long` | |
| 135 | | Daily | `daily` | `daily_long` | |
| 136 | | Weekly | `weekly` | `weekly_long` | |
| 137 | | Monthly | `monthly` | `monthly_long` | |
| 138 | | Custom | `cron` | `cron` (use long queue manually) | |
| 139 | |
| 140 | **Step 2: Add to hooks.py** |
| 141 | |
| 142 | ```python |
| 143 | scheduler_events = { |
| 144 | "daily": ["myapp.tasks.daily_cleanup"], |
| 145 | "daily_long": ["myapp.tasks.heavy_sync"], |
| 146 | "cron": { |