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.md
source content

Document Lifecycle Hooks (doc_events)

Quick Reference: Event Execution Order

Insert (new document)

OrderEventPurposeCan Raise?
1
before_insert
Set defaults before namingYES
2
before_naming
Modify naming logicYES
3
autoname
Set the
name
property
YES
4
before_validate
Auto-set missing valuesYES
5
validate
Validation logic — throw to abortYES
6
before_save
Final mutations before DB writeYES
7
db_insert
Internal — writes row to DB
8
after_insert
Post-insert logic (runs once ever)YES
9
on_update
Post-save logic (runs on every save)YES
10
on_change
Fires if any field value changedYES

Save (existing document)

OrderEventPurpose
1
before_validate
Auto-set missing values
2
validate
Validation logic — throw to abort
3
before_save
Final mutations before DB write
4
db_update
Internal — updates row in DB
5
on_update
Post-save logic
6
on_change
Fires if any field value changed

Submit

OrderEventPurpose
1
before_validate
Auto-set missing values
2
validate
Validation logic
3
before_save
Final mutations before DB write
4
before_submit
Pre-submit logic — throw to abort
5
db_update
Internal — updates row in DB
6
on_submit
Post-submit logic (GL entries etc)
7
on_update
Post-save logic
8
on_change
Fires if any field value changed

Cancel

OrderEventPurpose
1
before_cancel
Pre-cancel validation
2
db_update
Internal — updates row in DB
3
on_cancel
Post-cancel logic (reverse GL etc)
4
on_change
Fires if any field value changed

Delete

OrderEventPurpose
1
on_trash
Pre-delete cleanup
2
after_delete
Post-delete logic

Other Operations

OperationEvents (in order)
Rename
before_rename
after_rename
Amend
before_insert
chain runs on the new amended doc
Update After Submit
before_update_after_submit
db_update
on_update_after_submit
on_change

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.

AspectController (class method)doc_events (hooks.py)
Location
{doctype}.py
controller file
hooks.py
in your app
Use whenYou OWN the DocTypeYou are EXTENDING another app's DocType
ExecutionRuns first (controller)Runs after controller method
Multiple appsOnly one controller per DocTypeMultiple 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
):

  1. Controller method runs first (
    def validate(self)
    )
  2. doc_events
    handlers run in app installation order
  3. 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
    extend_doctype_class
    over
    override_doctype_class
    in v16+ when multiple apps may extend the same DocType.
  • 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
    validate
    ) run as part of the controller, NOT as separate doc_events handlers.

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:

  1. Handlers execute in app installation order (as listed in
    sites/{site}/site_config.json
    installed_apps
    ).
  2. The order can be changed via Setup > Installed Applications > Update Hooks Resolution Order.
  3. For
    override_doctype_class
    , the last-installed app wins (only one override applies).
  4. For
    extend_doctype_class
    (v16+), all extensions stack cumulatively.

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
    ,
    on_cancel
    — all run BEFORE the transaction commits.
  • The transaction commits only AFTER all events complete successfully.
  • after_delete
    runs after the DELETE statement but still within the request transaction.

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

  1. ALWAYS use
    frappe.throw()
    to abort operations — NEVER use
    raise Exception
    .
  2. NEVER modify
    doc.name
    outside of
    autoname
    or
    before_naming
    .
  3. ALWAYS call
    super().{event}()
    when overriding controller methods in subclasses.
  4. NEVER use
    doc.save()
    inside
    validate
    or
    before_save
    — this causes infinite recursion.
  5. ALWAYS use
    doc.flags.ignore_permissions = True
    explicitly if your hook needs to bypass permissions — NEVER assume hooks run as Administrator.
  6. NEVER put slow operations (API calls, file I/O) in
    validate
    — use
    after_insert
    or
    on_update
    with
    frappe.enqueue()
    instead.
  7. ALWAYS use
    doc.flags
    to communicate between events in the same request (e.g.,
    doc.flags.skip_notification = True
    ).
  8. NEVER rely on
    on_change
    for critical logic — it only fires when values actually differ from the database state.

See Also