frappe-agent-architect
Use when designing multi-app Frappe architectures, deciding whether to split functionality into separate apps, or implementing cross-app communication…
Use when debugging or preventing errors in Frappe Document Controllers. Prevents autoname failures, validate loops, on_submit without is_submittable, wrong lifecycle hook choice, get_list permission errors, NestedSet errors, extend_doctype_class conflicts, missing super() calls,
$ npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-errors-controllers --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/frappe-errors-controllersContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when debugging or preventing errors in Frappe Document Controllers. Prevents autoname failures, validate loops, on_submit without is_submittable, wrong lifecycle hook choice, get_list permission errors, NestedSet errors, extend_doctype_class conflicts, missing super() calls,
name: frappe-errors-controllers description: > Use when debugging or preventing errors in Frappe Document Controllers. Prevents autoname failures, validate loops, on_submit without is_submittable, wrong lifecycle hook choice, get_list permission errors, NestedSet errors, extend_doctype_class conflicts, missing super() calls, and recursion without flags. Covers error diagnosis by lifecycle phase for v14/v15/v16. Keywords: controller error, autoname, validate loop, on_submit, is_submittable,, save fails, validate error, on_submit not working, autoname broken, controller crash. get_list, NestedSet, extend_doctype_class, super, flags, recursion guard. license: MIT compatibility: "Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16." metadata: author: OpenAEC-Foundation version: "2.0"
Cross-refs: `frappe-syntax-controllers` (syntax), `frappe-impl-controllers` (workflows), `frappe-errors-serverscripts` (server scripts).
---
CONTROLLER ERROR
│
├─► NAMING PHASE (autoname / before_naming)
│ ├─► NamingSeries not set → Add naming_series field or autoname property
│ ├─► DuplicateEntryError → Name collision, check uniqueness
│ └─► "name cannot be set directly" → Use autoname method, not self.name = x
│
├─► VALIDATION PHASE (before_validate / validate / before_save)
│ ├─► Infinite recursion → doc.save() called inside validate
│ ├─► Validation skipped → Missing super().validate() in override
│ └─► Wrong error timing → Use validate, not on_update, to block save
│
├─► SAVE PHASE (before_save / on_update / after_insert)
│ ├─► Changes lost in on_update → Use db_set(), not self.field = x
│ ├─► Infinite loop → self.save() in on_update triggers on_update again
│ └─► Transaction broken → frappe.db.commit() in controller (DON'T)
│
├─► SUBMIT PHASE (before_submit / on_submit)
│ ├─► "Not allowed to submit" → DocType missing is_submittable = 1
│ ├─► Partial state → Validation in on_submit (too late, already submitted)
│ └─► Stock/GL failures → Entries fail but docstatus already = 1
│
├─► CANCEL PHASE (before_cancel / on_cancel)
│ ├─► "Cannot cancel: linked docs" → Check and handle linked documents
│ └─► Partial cleanup → One reversal fails, rest skipped
│
└─► PERMISSION PHASE (has_permission / get_list)
├─► "Not permitted" → has_permission returns None (should be True/False)
├─► get_list returns nothing → permission_query_conditions SQL error
└─► SQL injection → User input in conditions without escape---
| Error Message | Cause | Fix | |---------------|-------|-----| | `NamingSeries is not set` | DocType uses naming_series but field is missing | Add `naming_series` field to DocType or set `autoname` in controller | | `DuplicateEntryError` | `autoname` generated non-unique name | Use `naming_series` with counter, or add hash suffix | | `Maximum recursion depth exceeded` | `self.save()` called in validate/on_update | NEVER call `self.save()` in hooks; use `self.db_set()` in on_update | | `Not allowed to submit` | DocType lacks `is_submittable = 1` | Enable "Is Submittable" in DocType settings | | `Cannot cancel: linked docs exist` | Submitted linked documents block cancellation | Cancel linked docs first, or use `before_cancel` to check | | `AttributeError: super()` | Missing `super()` call in overridden hook | ALWAYS call `super().method_name()` first in overrides | | `Value missing for: field` | Controller validate skipped parent logic | Ensure `super().validate()` is called | | `frappe.db.commit() breaks transactions` | Manual commit in controller hook | NEVER call `frappe.db.commit()` in controllers | | `Changes lost in on_update` | Set `self.field = x` instead of `self.db_set()` | Use `self.db_set("field", value)` after save hooks | | `NestedSet: root cannot be child` | Parent set to itself or circular reference | Validate parent != self in validate, check `lft`/`rgt` | | `extend_doctype_class conflict` [v16+] | Multiple apps extend same class with conflicting methods | Use MRO-aware design, check method resolution order | | `has_permission returns wrong result` | Function returns None instead of True/False | ALWAYS return explicit True or False | | `permission_query_conditions SQL error` | Malformed WHERE clause fragment | Test conditions string independently, use `frappe.db.escape()` |
---
# ❌ WRONG — Setting name directly fails
class CustomDoc(Document):
def autoname(self):
self.name = f"DOC-{self.customer}" # May cause DuplicateEntryError
# ✅ CORRECT — Use naming utilities
class CustomDoc(Document):
def autoname(self):
# Option 1: Naming series
from frappe.model.naming import set_name_by_naming_series
set_name_by_naming_series(self)
# Option 2: Safe format with counter
self.name = frappe.model.naming.make_autoname(
f"DOC-.{self.customer}.-.####"
)
# Option 3: Hash for guaranteed uniqueness
# Set autoname = "hash" in DocType JSON instead**Autoname options**: `naming_series`, `field:fieldname`, `format:PREFIX-{fieldname}-.####`, `hash`, `Prompt`, or custom `autoname()` method.
# ❌ WRONG — Infinite recursion
class SalesOrder(Document):
def validate(self):
self.calculate_totals()
self.save() # Triggers validate again → infinite loop!
def on_update(self):
self.status = "Updated"
self.save() # Triggers on_update again → infinite loop!
# ✅ CORRECT — Framework handles save; use db_set after save
class SalesOrder(Document):
def validate(self):
self.calculate_totals()
# No save() — framework saves after validate completes
def on_update(self):
self.db_set("status", "Updated") # Direct DB write, no trigger###
60 deterministic Claude AI skills for Frappe Framework & ERPNext v14-v16 development and operations
Repo: Impertio-Studio/Frappe_Claude_Skill_Package
Use when designing multi-app Frappe architectures, deciding whether to split functionality into separate apps, or implementing cross-app communication…
Use when debugging Frappe errors, using bench console for live inspection, analyzing tracebacks, or reading Frappe log files. Prevents wasted debugging time…
Use when receiving vague or unclear ERPNext/Frappe development requests that need interpretation. Transforms requirements like 'make invoice auto-calculate' or…
Use when migrating a Frappe app between major versions, detecting breaking API changes, or resolving post-migration errors. Prevents failed migrations from…
Use when reviewing or validating Frappe/ERPNext code against best practices and common pitfalls. Checks generated code before deployment, validates against all…
Use when building ERPNext/Frappe API integrations (v14/v15/v16) including REST API, RPC API, authentication, webhooks, and rate limiting. Covers external API…