$npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-errors-hooksUse when debugging hooks.py errors in Frappe/ERPNext. Covers hook not firing (typo, wrong dict structure), circular imports, app_include_js path errors, scheduler_events not running, doc_events on wrong DocType, permission_query_conditions SQL errors, override_doctype_class impor
| 1 | # Frappe Hooks Error Diagnosis & Resolution |
| 2 | |
| 3 | Cross-ref: `frappe-syntax-hooks` (syntax), `frappe-impl-hooks` (workflows), `frappe-errors-controllers` (controller errors). |
| 4 | |
| 5 | --- |
| 6 | |
| 7 | ## Error-to-Fix Mapping Table |
| 8 | |
| 9 | | Error / Symptom | Cause | Fix | |
| 10 | |-----------------|-------|-----| |
| 11 | | Hook not firing at all | Typo in dotted path | Verify module path matches actual file location | |
| 12 | | `ImportError` on bench start | Wrong module path or circular import | Fix import path; break circular dependency | |
| 13 | | `AttributeError: module has no attribute` | Function name typo in hooks.py | Match function name exactly to Python definition | |
| 14 | | `app_include_js` not loading | Path missing `assets/` prefix or wrong extension | Use `"assets/myapp/js/file.js"` format | |
| 15 | | scheduler_events not running | Scheduler disabled or workers down | `bench scheduler enable`, check `bench doctor` | |
| 16 | | doc_events handler never called | DocType name misspelled in dict key | Use exact DocType name with spaces: `"Sales Invoice"` | |
| 17 | | `permission_query_conditions` breaks list view | SQL syntax error or frappe.throw() in handler | Return valid SQL string; NEVER throw | |
| 18 | | `override_doctype_class` import failure | Parent class import path changed between versions | Pin import to correct module path for target version | |
| 19 | | `extend_doctype_class` [v16+] method conflict | Two extensions define same method name | Rename conflicting methods; check hook resolution order | |
| 20 | | Fixtures not loading on install | Wrong `dt` key or DocType doesn't exist on target | Verify DocType exists before export; check filter syntax | |
| 21 | | `extend_bootinfo` breaks login | Unhandled exception in boot handler | Wrap ALL bootinfo code in try/except | |
| 22 | | Wildcard `"*"` handler breaks all saves | Unhandled exception in wildcard doc_events | ALWAYS wrap wildcard handlers in try/except | |
| 23 | | Hook fires but changes lost | Missing `frappe.db.commit()` in scheduler | Add explicit commit in scheduler/background tasks | |
| 24 | | Multiple handler chain broken | First handler throws, others never run | Isolate non-critical ops in try/except | |
| 25 | |
| 26 | --- |
| 27 | |
| 28 | ## Hook Registration Errors |
| 29 | |
| 30 | ### Hook Not Firing: Diagnosis Checklist |
| 31 | |
| 32 | ``` |
| 33 | IS YOUR HOOK NOT FIRING? |
| 34 | │ |
| 35 | ├─► Check 1: Is the dotted path correct? |
| 36 | │ hooks.py: "myapp.events.sales.validate" |
| 37 | │ File: myapp/events/sales.py → def validate(doc, method=None): |
| 38 | │ COMMON MISTAKE: "myapp.events.sales_invoice.validate" when file is sales.py |
| 39 | │ |
| 40 | ├─► Check 2: Is the dict structure correct? |
| 41 | │ doc_events uses NESTED dict: {"Sales Invoice": {"validate": "path"}} |
| 42 | │ scheduler_events uses LIST: {"daily": ["path1", "path2"]} |
| 43 | │ permission_query uses FLAT dict: {"Sales Invoice": "path"} |
| 44 | │ |
| 45 | ├─► Check 3: Is bench restarted after hooks.py change? |
| 46 | │ ALWAYS run: bench restart (or bench clear-cache for dev) |
| 47 | │ |
| 48 | ├─► Check 4: Is the DocType name exact? |
| 49 | │ "Sales Invoice" NOT "SalesInvoice" NOT "sales_invoice" |
| 50 | │ Use exact DocType name as shown in Frappe UI |
| 51 | │ |
| 52 | └─► Check 5: Is the app installed on the site? |
| 53 | bench --site mysite list-apps |
| 54 | ``` |
| 55 | |
| 56 | ### Circular Import Errors |
| 57 | |
| 58 | ```python |
| 59 | # ❌ CAUSES ImportError — circular dependency |
| 60 | # myapp/hooks.py imports from myapp.events |
| 61 | # myapp/events/sales.py imports from myapp.hooks |
| 62 | |
| 63 | # ✅ CORRECT — break the cycle |
| 64 | # Move shared constants to myapp/constants.py |
| 65 | # Import from constants in both hooks.py and events/ |
| 66 | ``` |
| 67 | |
| 68 | **Rule**: NEVER import from hooks.py in your event handlers. hooks.py is read by the framework, not imported by your code. |
| 69 | |
| 70 | ### Wrong Dict Structure by Hook Type |
| 71 | |
| 72 | ```python |
| 73 | # ❌ WRONG — doc_events needs nested dict, not flat |
| 74 | doc_events = { |
| 75 | "Sales Invoice": "myapp.events.validate" # WRONG: string, not dict |
| 76 | } |
| 77 | |
| 78 | # ✅ CORRECT |
| 79 | doc_events = { |
| 80 | "Sales Invoice": { |
| 81 | "validate": "myapp.events.sales.validate" |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | # ❌ WRONG — scheduler_events daily needs list |
| 86 | scheduler_events = { |
| 87 | "daily": "myapp.tasks.daily_sync" # WRONG: string, not list |
| 88 | } |
| 89 | |
| 90 | # ✅ CORRECT |
| 91 | scheduler_events = { |
| 92 | "daily": ["myapp.tasks.daily_sync"] |
| 93 | } |
| 94 | |
| 95 | # ❌ WRONG — cron needs nested dict with list values |
| 96 | scheduler_events = { |
| 97 | "cron": ["0 9 * * *", "myapp.tasks.morning"] # WRONG structure |
| 98 | } |
| 99 | |
| 100 | # ✅ CORRECT |
| 101 | scheduler_events = { |
| 102 | "cron": { |
| 103 | "0 |