Document Lifecycle
Every Frappe document goes through a defined lifecycle. Hooks fire at each stage — apps can validate, react, or veto.
Stages
| Stage | When | Common use |
|---|---|---|
before_insert |
Just before first save (record doesn't exist yet) | Compute defaults from context |
after_insert |
After first save | Create related records |
validate |
Before every save (insert OR update) | Business rule checks |
before_save |
Before every save | Final adjustments |
on_update |
After every save | Sync to external systems |
before_submit |
Before submit | Final checks before locking |
on_submit |
After submit | Create GL Entries, trigger workflow |
before_cancel |
Before cancel | Verify cancellation allowed |
on_cancel |
After cancel | Reverse GL Entries |
on_trash |
Before delete | Cleanup |
Submittable documents
Documents like Sales Invoice, Stock Entry, Journal Entry are submittable:
| docstatus | Meaning | Editable? |
|---|---|---|
| 0 | Draft | Yes |
| 1 | Submitted (finalized; ledger entries created) | No — Cancel + Amend |
| 2 | Cancelled (reversed) | No |
You don't delete submitted documents — you Cancel and (optionally) Amend.
Hook resolution
For each lifecycle stage, Frappe runs:
- The DocType's controller method (e.g.,
before_save(self)) - App
doc_eventshooks (in install order)
Multiple apps can hook the same event. Errors from any hook abort the save — partial state never persists.
Idempotency
Don't assume hooks fire only once per "user action". A retry, a queued job, or a script can trigger the same event multiple times. Design hooks to be idempotent — re-running them shouldn't double-charge or double-create.
Last updated 3 days ago
Was this helpful?