Framework Git

Framework Git

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

Controllers & Lifecycle

A controller is the Python class behind a DocType. It lives in <doctype>.py, subclasses frappe.model.document.Document (whose read/write methods are covered in the Document API), and is where your server-side business logic goes. Frappe calls specially named methods on the controller at set points in a document's life. These are the lifecycle hooks.

The controller class

The class name is the PascalCase of the DocType name, and it is auto-discovered from the DocType's module.

import frappe
from frappe.model.document import Document

class Task(Document):
    def validate(self):
        if self.exp_end_date and self.exp_start_date:
            if self.exp_end_date < self.exp_start_date:
                frappe.throw("End date cannot be before start date")

To stop a save, call frappe.throw(message) from any of the pre-save hooks. It raises a ValidationError and rolls back the transaction.

Lifecycle hooks

Define any of these methods on your controller; Frappe calls them automatically. You never call them yourself. The order below is the exact sequence from frappe/model/document.py.

On insert (a new document)

When doc.insert() runs:

Hook Use it to
before_insert Set up values before anything else; runs only on new docs.
before_validate Normalise/clean data before validation.
validate Validate the document; throw to abort.
before_save Final tweaks just before writing to the DB.
(row written to DB)
after_insert React to the new record now that it has a name.
on_update React to the saved state (also runs on every later save).
on_change Runs after every change (save, submit, cancel, db_set).

On save (an existing document)

When doc.save() runs on a document that already exists:

Hook Use it to
before_validate Normalise data before validation.
validate Validate; throw to abort.
before_save Final tweaks before the DB write.
(row updated in DB)
on_update React to the saved changes.
on_change Runs after the change.

before_save and on_update are the save-time equivalents of before_insert/after_insert, but before_save/on_update run on both insert and update, while before_insert/after_insert run only on insert.

On submit

For submittable DocTypes, when doc.submit() runs (docstatus 0 → 1):

Hook Use it to
before_validate Normalise data.
validate Validate.
before_submit Last checks before the document becomes submitted.
(row updated, docstatus = 1)
on_update Runs on submit too.
on_submit Post the document's effects (ledger entries, stock, etc.).
on_change Runs after the change.

On cancel

When doc.cancel() runs (docstatus 1 → 2):

Hook Use it to
before_cancel Checks before cancelling.
(row updated, docstatus = 2)
on_cancel Reverse the effects created in on_submit.
on_change Runs after the change.

After on_cancel, Frappe verifies no other active documents link to this one before completing.

Update after submit

Submitted documents are read-only except for fields marked allow_on_submit. Editing such a field and saving (docstatus stays 1) triggers:

Hook Use it to
before_update_after_submit Validate the limited edit.
on_update_after_submit React to the post-submit change.

On delete

When doc.delete() / frappe.delete_doc() runs:

Hook Use it to
on_trash Clean up related data before the row is removed.
after_delete Final cleanup after deletion.

On load

Hook Use it to
onload Prepare data for the form when a document is opened (e.g. self.set_onload("key", value)).

Discard (drafts)

A draft can be discarded (doc.discard()), which fires before_discard then on_discard. See Docstatus for why this is the sanctioned Draft → Cancelled path.

Naming and rename hooks

Hook Use it to
before_naming Adjust or validate data before the name is generated. Runs first in set_new_name, before any naming rule.
before_rename Validate or transform a rename before it happens. Return {"new": name} to change the target name.
after_rename React once the rename, and all its link updates, are complete.

before_rename and after_rename fire from frappe.rename_doc / doc.rename(), not from insert or save. See Naming for the full naming resolution order.

Direct database updates: db_set

doc.db_set(fieldname, value) writes a field straight to the database, bypassing validate and the save hooks. It still fires before_change just before the write and on_change just after, the same on_change that also runs after save, submit and cancel.

Quick reference: order of common operations

insert:  before_insert → before_validate → validate → before_save
         → [DB insert] → after_insert → on_update → on_change

save:    before_validate → validate → before_save
         → [DB update] → on_update → on_change

submit:  before_validate → validate → before_submit
         → [DB update] → on_update → on_submit → on_change

cancel:  before_cancel → [DB update] → on_cancel → on_change

delete:  on_trash → [DB delete] → after_delete

Reacting to changed values

Inside validate, on_update or on_change you can compare against the previously saved state:

class Task(Document):
    def on_update(self):
        if self.has_value_changed("status"):
            old = self.get_value_before_save("status")
            frappe.msgprint(f"Status changed from {old} to {self.status}")

has_value_changed(fieldname) returns True if the value differs from the database copy; get_value_before_save(fieldname) returns the previous value. These only work in a save context.

Extending another app's controller

Hooks defined directly on the class are for the DocType's own app. To run logic on a DocType owned by another app, register a handler via doc_events in hooks.py rather than editing its controller. See Hooks.

# hooks.py
doc_events = {
    "Task": {
        "on_update": "your_app.tasks.notify_on_update",
    }
}
# your_app/tasks.py
def notify_on_update(doc, method):
    # `doc` is the Task document, `method` is "on_update"
    ...

See also

  • Docstatus: the submit/cancel state machine.
  • Hooks: hooking into other apps' DocTypes.
  • Document API: insert, save, submit, db_set, etc.
Last updated 1 hour ago
Was this helpful?
Thanks!