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 writing Python Document Controllers for ERPNext/Frappe DocTypes. Covers lifecycle hooks (validate, on_update, on_submit), controller override, submittable documents, autoname patterns, UUID naming (v16), and the flags system. Keywords: document controller, lifecycle
$ npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-syntax-controllers --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/frappe-syntax-controllersContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when writing Python Document Controllers for ERPNext/Frappe DocTypes. Covers lifecycle hooks (validate, on_update, on_submit), controller override, submittable documents, autoname patterns, UUID naming (v16), and the flags system. Keywords: document controller, lifecycle
name: frappe-syntax-controllers description: > Use when writing Python Document Controllers for ERPNext/Frappe DocTypes. Covers lifecycle hooks (validate, on_update, on_submit), controller override, submittable documents, autoname patterns, UUID naming (v16), and the flags system. Keywords: document controller, lifecycle hook, validate, on_update, on_submit, autoname, naming series, flags, v14-v16, controller example, lifecycle hook order, when to use validate, Python DocType class. license: MIT compatibility: "Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16." metadata: author: OpenAEC-Foundation version: "2.0"
Document Controllers are Python classes that define all server-side logic for a DocType. EVERY DocType has exactly one controller file. The controller class extends `frappe.model.document.Document`.
import frappe
from frappe import _
from frappe.model.document import Document
class SalesOrder(Document):
def autoname(self):
"""Custom naming logic. Sets self.name."""
self.name = f"SO-{self.customer_code}-{frappe.utils.now_datetime().year}"
def validate(self):
"""MAIN validation — runs on EVERY save (insert and update).
Changes to self ARE saved to database."""
if not self.items:
frappe.throw(_("Items are required"))
self.total = sum(item.amount for item in self.items)
def on_update(self):
"""After save — changes to self are NOT saved.
Use frappe.db.set_value() for post-save field changes."""
self.notify_linked_docs()
def on_submit(self):
"""After submit (docstatus 0 -> 1). Create ledger entries here."""
self.create_gl_entries()
def on_cancel(self):
"""After cancel (docstatus 1 -> 2). Reverse ledger entries here."""
self.reverse_gl_entries()
@frappe.whitelist()
def recalculate(self):
"""Exposed to client JS via frm.call('recalculate')."""
self.total = sum(item.amount for item in self.items)
return {"total": self.total}| DocType Name | Class Name | File Path | |---|---|---| | Sales Order | `SalesOrder` | `selling/doctype/sales_order/sales_order.py` | | My Custom Doc | `MyCustomDoc` | `module/doctype/my_custom_doc/my_custom_doc.py` |
**Rule**: DocType name -> PascalCase class -> snake_case filename. ALWAYS match exactly.
---
before_insert -> before_naming -> autoname -> before_validate -> validate -> before_save -> [db_insert] -> after_insert -> on_update -> on_change
before_validate -> validate -> before_save -> [db_update] -> on_update -> on_change
before_validate -> validate -> before_submit -> [db_update] -> on_submit -> on_update -> on_change
before_cancel -> [db_update] -> on_cancel -> on_change
before_update_after_submit -> [db_update] -> on_update_after_submit -> on_change
on_trash -> [db_delete] -> after_delete
before_discard -> [db_set docstatus=2] -> on_discard
**Complete hook reference with parameters**: See [lifecycle-methods.md](references/lifecycle-methods.md)
---
What do you need to do? | +-- Validate data or calculate fields? | +-- validate (changes to self ARE saved) | +-- Action AFTER save (emails, sync, linked docs)? | +-- on_update (changes to self are NOT saved) | +-- Only for NEW documents? | +-- after_insert (runs once on first save only) | +-- Custom document name? | +-- autoname (set self.name) | +-- Before/after SUBMIT? | +-- Validate before submit? -> before_submit | +-- Create entries after submit? -> on_submit | +-- Before/after CANCEL? | +-- Check linked docs? -> before_cancel | +-- Reverse entries? -> on_cancel | +-- Cleanup before delete? | +-- on_trash | +-- React to ANY value change (including db_set)? | +-- on_change (MUST be idempotent)
---
# WRONG - change is lost after on_update
def on_update(self):
self.status = "Completed" # NOT saved to database
# CORRECT - use db_set or frappe.db.set_value
def on_update(self):
self.db_set("status", "Completed")# WRONG - breaks Frappe transaction management
def validate(self):
frappe.db.commit() # Can cause partial updates on error
# CORRECT - Frappe commits automatically at end of request
def validate(self):
self.update_related() # No commit needed# WRONG - parent validation is skipped entirely
def validate(self):
self.custom_check()
# CORRECT - parent logic preserved
def validate(self):
super().validate()
self.custom_check()def on_update(self):
if self.flags.get("from_linked_doc"):
return
linked = frappe.get_doc("Linked Doc", self.linked_doc)
linked.flags.from_linked_doc = True
linked.save()# WRONG - document is already saved when this throws
def on_update(self):
if self.total < 0:
frappe.throw("Invalid total") # Too late!
# CORRECT - validate BEFORE save
def validate(self):
if self.total < 0:
frappe.throw("Invalid total") # Blocks save---
| Method | Example | Result | Version | |---|---|---|---| | `field:fieldname` | `field:customer_name` | `ABC Company` | All | | `naming_series:` | `naming_series:` | `SO-2024-00001` | All | | Expression | `PRE-.#####` | `PRE-00001` | All | | Old-style format | `INV-{YYYY}-{####}` | `INV
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…