Skip to main content
ClaudeWave
Install in Claude Code
Copy
git clone --depth 1 https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package /tmp/frappe-errors-hooks && cp -r /tmp/frappe-errors-hooks/skills/source/errors/frappe-errors-hooks ~/.claude/skills/frappe-errors-hooks
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# Frappe Hooks Error Diagnosis & Resolution

Cross-ref: `frappe-syntax-hooks` (syntax), `frappe-impl-hooks` (workflows), `frappe-errors-controllers` (controller errors).

---

## Error-to-Fix Mapping Table

| Error / Symptom | Cause | Fix |
|-----------------|-------|-----|
| Hook not firing at all | Typo in dotted path | Verify module path matches actual file location |
| `ImportError` on bench start | Wrong module path or circular import | Fix import path; break circular dependency |
| `AttributeError: module has no attribute` | Function name typo in hooks.py | Match function name exactly to Python definition |
| `app_include_js` not loading | Path missing `assets/` prefix or wrong extension | Use `"assets/myapp/js/file.js"` format |
| scheduler_events not running | Scheduler disabled or workers down | `bench scheduler enable`, check `bench doctor` |
| doc_events handler never called | DocType name misspelled in dict key | Use exact DocType name with spaces: `"Sales Invoice"` |
| `permission_query_conditions` breaks list view | SQL syntax error or frappe.throw() in handler | Return valid SQL string; NEVER throw |
| `override_doctype_class` import failure | Parent class import path changed between versions | Pin import to correct module path for target version |
| `extend_doctype_class` [v16+] method conflict | Two extensions define same method name | Rename conflicting methods; check hook resolution order |
| Fixtures not loading on install | Wrong `dt` key or DocType doesn't exist on target | Verify DocType exists before export; check filter syntax |
| `extend_bootinfo` breaks login | Unhandled exception in boot handler | Wrap ALL bootinfo code in try/except |
| Wildcard `"*"` handler breaks all saves | Unhandled exception in wildcard doc_events | ALWAYS wrap wildcard handlers in try/except |
| Hook fires but changes lost | Missing `frappe.db.commit()` in scheduler | Add explicit commit in scheduler/background tasks |
| Multiple handler chain broken | First handler throws, others never run | Isolate non-critical ops in try/except |

---

## Hook Registration Errors

### Hook Not Firing: Diagnosis Checklist

```
IS YOUR HOOK NOT FIRING?
│
├─► Check 1: Is the dotted path correct?
│   hooks.py: "myapp.events.sales.validate"
│   File:     myapp/events/sales.py → def validate(doc, method=None):
│   COMMON MISTAKE: "myapp.events.sales_invoice.validate" when file is sales.py
│
├─► Check 2: Is the dict structure correct?
│   doc_events uses NESTED dict: {"Sales Invoice": {"validate": "path"}}
│   scheduler_events uses LIST: {"daily": ["path1", "path2"]}
│   permission_query uses FLAT dict: {"Sales Invoice": "path"}
│
├─► Check 3: Is bench restarted after hooks.py change?
│   ALWAYS run: bench restart (or bench clear-cache for dev)
│
├─► Check 4: Is the DocType name exact?
│   "Sales Invoice" NOT "SalesInvoice" NOT "sales_invoice"
│   Use exact DocType name as shown in Frappe UI
│
└─► Check 5: Is the app installed on the site?
    bench --site mysite list-apps
```

### Circular Import Errors

```python
# ❌ CAUSES ImportError — circular dependency
# myapp/hooks.py imports from myapp.events
# myapp/events/sales.py imports from myapp.hooks

# ✅ CORRECT — break the cycle
# Move shared constants to myapp/constants.py
# Import from constants in both hooks.py and events/
```

**Rule**: NEVER import from hooks.py in your event handlers. hooks.py is read by the framework, not imported by your code.

### Wrong Dict Structure by Hook Type

```python
# ❌ WRONG — doc_events needs nested dict, not flat
doc_events = {
    "Sales Invoice": "myapp.events.validate"  # WRONG: string, not dict
}

# ✅ CORRECT
doc_events = {
    "Sales Invoice": {
        "validate": "myapp.events.sales.validate"
    }
}

# ❌ WRONG — scheduler_events daily needs list
scheduler_events = {
    "daily": "myapp.tasks.daily_sync"  # WRONG: string, not list
}

# ✅ CORRECT
scheduler_events = {
    "daily": ["myapp.tasks.daily_sync"]
}

# ❌ WRONG — cron needs nested dict with list values
scheduler_events = {
    "cron": ["0 9 * * *", "myapp.tasks.morning"]  # WRONG structure
}

# ✅ CORRECT
scheduler_events = {
    "cron": {
        "0 9 * * 1-5": ["myapp.tasks.morning_report"]
    }
}
```

---

## app_include_js / app_include_css Errors

```python
# ❌ WRONG — missing assets/ prefix
app_include_js = "js/myapp.js"

# ❌ WRONG — using Python module path instead of file path
app_include_js = "myapp.public.js.myapp"

# ✅ CORRECT — full asset path
app_include_js = "assets/myapp/js/myapp.js"

# ✅ CORRECT — multiple files as list
app_include_js = ["assets/myapp/js/app.js", "assets/myapp/js/utils.js"]
app_include_css = "assets/myapp/css/myapp.css"
```

**Diagnosis**: If JS/CSS not loading, check browser DevTools Network tab for 404. Run `bench build` after adding new files. ALWAYS verify the file exists at `myapp/public/js/myapp.js`.

---

## scheduler_events Not Running

### Diagnosis Steps

```bash
# Step 1: Is scheduler enabled?
bench scheduler status
# If disabled: bench scheduler enable

# Step 2: Are workers running?
bench doctor
# Look for: "Workers online: X"
# If 0: bench start (dev) or supervisorctl restart all (prod)

# Step 3: Check Scheduled Job Log
# In Frappe UI: /api/method/frappe.client.get_list?doctype=Scheduled Job Log&limit=5

# Step 4: Check Error Log for task failures
# In Frappe UI: /app/error-log

# Step 5: Is the task registered?
bench execute frappe.utils.scheduler.get_all_tasks
```

### Common Scheduler Failures

```python
# ❌ PROBLEM: Task runs but changes not persisted
def daily_sync():
    for item in frappe.get_all("Item", limit=100):
        frappe.db.set_value("Item", item.name, "synced", 1)
    # MISSING: frappe.db.commit() — ALL changes lost!

# ✅ FIX: ALWAYS commit in scheduler tasks
def daily_sync():
    for item in frappe.get_all("Item", limit=100):
        frappe.db.set_value("Item", item.name, "synced", 1)
    frappe.db.commit()

# ❌ PROBLEM: Task fails silently — no debugging possible
def daily_task():
    t