Frappe_Claude_Skill_Package frappe-syntax-hooks-events
install
source · Clone the upstream repo
git clone https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/source/syntax/frappe-syntax-hooks-events" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-syntax-hooks-events && rm -rf "$T"
manifest:
skills/source/syntax/frappe-syntax-hooks-events/SKILL.mdsource content
Document Lifecycle Hooks (doc_events)
Quick Reference: Event Execution Order
Insert (new document)
| Order | Event | Purpose | Can Raise? |
|---|---|---|---|
| 1 | | Set defaults before naming | YES |
| 2 | | Modify naming logic | YES |
| 3 | | Set the property | YES |
| 4 | | Auto-set missing values | YES |
| 5 | | Validation logic — throw to abort | YES |
| 6 | | Final mutations before DB write | YES |
| 7 | | Internal — writes row to DB | — |
| 8 | | Post-insert logic (runs once ever) | YES |
| 9 | | Post-save logic (runs on every save) | YES |
| 10 | | Fires if any field value changed | YES |
Save (existing document)
| Order | Event | Purpose |
|---|---|---|
| 1 | | Auto-set missing values |
| 2 | | Validation logic — throw to abort |
| 3 | | Final mutations before DB write |
| 4 | | Internal — updates row in DB |
| 5 | | Post-save logic |
| 6 | | Fires if any field value changed |
Submit
| Order | Event | Purpose |
|---|---|---|
| 1 | | Auto-set missing values |
| 2 | | Validation logic |
| 3 | | Final mutations before DB write |
| 4 | | Pre-submit logic — throw to abort |
| 5 | | Internal — updates row in DB |
| 6 | | Post-submit logic (GL entries etc) |
| 7 | | Post-save logic |
| 8 | | Fires if any field value changed |
Cancel
| Order | Event | Purpose |
|---|---|---|
| 1 | | Pre-cancel validation |
| 2 | | Internal — updates row in DB |
| 3 | | Post-cancel logic (reverse GL etc) |
| 4 | | Fires if any field value changed |
Delete
| Order | Event | Purpose |
|---|---|---|
| 1 | | Pre-delete cleanup |
| 2 | | Post-delete logic |
Other Operations
| Operation | Events (in order) |
|---|---|
| Rename | → |
| Amend | chain runs on the new amended doc |
| Update After Submit | → → → |
doc_events in hooks.py: Syntax
Basic Structure
# hooks.py doc_events = { "Sales Invoice": { "on_submit": "myapp.events.sales_invoice.on_submit", "on_cancel": "myapp.events.sales_invoice.on_cancel", }, "Purchase Order": { "validate": "myapp.events.purchase_order.validate", } }
Wildcard: Apply to ALL DocTypes
doc_events = { "*": { "after_insert": "myapp.events.global_handler.after_insert_all", "on_update": "myapp.events.global_handler.track_changes", } }
ALWAYS use
"*" (string with asterisk) as the key. This fires the handler for every DocType.
Multiple Handlers per Event
doc_events = { "Sales Invoice": { "on_submit": [ "myapp.events.accounting.create_gl_entries", "myapp.events.notifications.send_invoice_email", ] } }
Handler Function Signature
# myapp/events/sales_invoice.py def on_submit(doc, method=None): """ doc — the Document instance (e.g., Sales Invoice) method — string name of the event (e.g., "on_submit"), or None """ if doc.grand_total > 10000: frappe.sendmail(...)
ALWAYS accept
method as the second parameter (with default None). Frappe passes it automatically.
Decision Tree: Which Event to Use
"I need to validate data before saving"
→ Use
validate. ALWAYS raise frappe.throw() here to block invalid saves.
"I need to set default values automatically"
→ Use
before_validate. This runs before validate, so your defaults are set before validation checks.
"I need to run logic only on first creation"
→ Use
after_insert. This fires ONLY on insert, NEVER on subsequent saves.
"I need to run logic on every save (insert + update)"
→ Use
on_update. This fires on both insert and save operations.
"I need to create linked documents after submit"
→ Use
on_submit. NEVER create linked docs in validate — the document is not yet committed.
"I need to reverse linked documents on cancel"
→ Use
on_cancel. ALWAYS clean up GL entries, stock ledger entries, and linked docs here.
"I need to modify the document name"
→ Use
autoname in the controller, or before_naming for conditional logic.
"I need to prevent deletion under certain conditions"
→ Use
on_trash. Raise frappe.throw() to block deletion.
"I need to update a submitted document's fields"
→ Use
before_update_after_submit for validation and on_update_after_submit for side effects.
"I need logic that runs only when values actually changed"
→ Use
on_change. This fires only when at least one field value differs from the DB state.
doc_events vs Controller Events
Both mechanisms trigger the SAME events. The difference is WHERE you register them.
| Aspect | Controller (class method) | doc_events (hooks.py) |
|---|---|---|
| Location | controller file | in your app |
| Use when | You OWN the DocType | You are EXTENDING another app's DocType |
| Execution | Runs first (controller) | Runs after controller method |
| Multiple apps | Only one controller per DocType | Multiple apps can register handlers |
ALWAYS use
doc_events when hooking into a DocType you do NOT own. NEVER modify another app's controller file directly.
Execution Order Within a Single Event
For a given event (e.g.,
validate):
- Controller method runs first (
)def validate(self)
handlers run in app installation orderdoc_events- Wildcard
handlers run after specific DocType handlers"*"
extend_doctype_class [v16+]
In Frappe v16+,
extend_doctype_class provides a cleaner alternative to doc_events for adding methods to existing DocTypes.
hooks.py
extend_doctype_class = { "Sales Invoice": [ "myapp.overrides.sales_invoice.SalesInvoiceExtension" ] }
Extension Class (Mixin)
# myapp/overrides/sales_invoice.py import frappe class SalesInvoiceExtension: def validate(self): """This is called as part of the controller chain.""" if self.grand_total < 0: frappe.throw("Grand total cannot be negative") def custom_method(self): """Custom methods are also available on the doc instance.""" return self.items
Key Rules
- ALWAYS use
overextend_doctype_class
in v16+ when multiple apps may extend the same DocType.override_doctype_class - Multiple apps can extend the same DocType — extensions stack via MRO.
- Class resolution order follows hooks priority:
.class Final(App2Mixin, App1Mixin, Original) - Extension methods (like
) run as part of the controller, NOT as separate doc_events handlers.validate
override_doctype_class [v14+]
Completely replaces the controller class. Use with extreme caution.
# hooks.py override_doctype_class = { "ToDo": "myapp.overrides.todo.CustomToDo" }
# myapp/overrides/todo.py from frappe.desk.doctype.todo.todo import ToDo class CustomToDo(ToDo): def validate(self): super().validate() # ALWAYS call super() to preserve original logic # Your additions here
NEVER use
override_doctype_class if extend_doctype_class is available (v16+). Only ONE app can override a DocType — last-installed app wins, silently breaking other apps.
Multi-App Event Ordering
When multiple apps register
doc_events for the same DocType and event:
- Handlers execute in app installation order (as listed in
→sites/{site}/site_config.json
).installed_apps - The order can be changed via Setup > Installed Applications > Update Hooks Resolution Order.
- For
, the last-installed app wins (only one override applies).override_doctype_class - For
(v16+), all extensions stack cumulatively.extend_doctype_class
Transaction Behavior
All document events from
before_validate through on_change run inside a single database transaction.
- If ANY event raises an exception, the ENTIRE operation rolls back (including
/db_insert
).db_update
,after_insert
,on_update
,on_submit
— all run BEFORE the transaction commits.on_cancel- The transaction commits only AFTER all events complete successfully.
runs after the DELETE statement but still within the request transaction.after_delete
NEVER assume data is committed to DB inside any event handler. Other concurrent requests will NOT see your changes until the full request completes.
Critical Rules
- ALWAYS use
to abort operations — NEVER usefrappe.throw()
.raise Exception - NEVER modify
outside ofdoc.name
orautoname
.before_naming - ALWAYS call
when overriding controller methods in subclasses.super().{event}() - NEVER use
insidedoc.save()
orvalidate
— this causes infinite recursion.before_save - ALWAYS use
explicitly if your hook needs to bypass permissions — NEVER assume hooks run as Administrator.doc.flags.ignore_permissions = True - NEVER put slow operations (API calls, file I/O) in
— usevalidate
orafter_insert
withon_update
instead.frappe.enqueue() - ALWAYS use
to communicate between events in the same request (e.g.,doc.flags
).doc.flags.skip_notification = True - NEVER rely on
for critical logic — it only fires when values actually differ from the database state.on_change
See Also
- Event Execution Order — Detailed Diagrams
- Working Examples for Common Patterns
- Anti-Patterns and Common Mistakes
— App-level hooks (scheduler, fixtures, permissions)frappe-syntax-hooks-config- Official docs: https://docs.frappe.io/framework/user/en/basics/doctypes/controllers