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.