Skip to main content

Chapter 2 of 7 · What runs when

What runs on save, submit and cancel

Every hook in order, and what it can still change.

Reviewed 14 Oct 20263 min readFrappe v16.52.0

Save, submit and cancel are not three methods. They are one method, save(), called with a different docstatus: 0 stays 0, 0 becomes 1, 1 becomes 2. The hooks you know are steps inside it.

Pick an action below and step through what runs, in order. Switch on the things you have set up, and throw an error at any step to see what is left behind.

Try it

Step through a document’s save

Pick an action, step through what runs, and throw an error to see what is left behind.

Action

Needs: Write and Submit. docstatus goes 0 → 1: the same save, with the submit hooks.

Show what you have set up

At each hook Frappe runs the controller’s own method first, then these, in this order: doc_events, Notification, Webhook, Server Script, Automation.

Throw an error at

Step 0 of 14
  1. Write permissionNext

    Permission document.py:585

  2. Timestamp check; docstatus 0 → 1 makes the action “submit”

    Check document.py:589, 1131–1136

  3. Submit permission

    Permission document.py:1136

  4. Values you may not write at permission level 1+ are reset

    Check document.py:593, 1023–1046

  5. Linked documents exist and none is cancelled

    Check document.py:594, 1210–1227

  6. before_validate

    Hook document.py:1407–1408

    doc_events handlers

  7. validate

    Hook document.py:1416–1417

    doc_events handlers

  8. before_submit

    Hook document.py:1418

    doc_events handlers

  9. Mandatory fields, selects, lengths and any workflow rule

    Check document.py:598, 829–841

    Runs after validate, so validate can still fill a required field.

  10. Row updated with docstatus = 1

    Write document.py:609–611

  11. on_update

    Hook document.py:1458–1459

    Fires on submit too, before on_submit.doc_events handlers

  12. on_submit

    Hook document.py:1460

    doc_events handlers

  13. Cache cleared, realtime update sent, search index and version saved

    After document.py:1467–1475

  14. on_change

    Hook document.py:1477

    Runs last, after the version is saved.doc_events handlers

Frappe v16.52.0. The same order holds on v15.123.0, except: no Automations, no discard, and doc_events handlers may commit.

What runs on Save?

First the permission check: Write for an existing document, Create for a new one. Then Frappe compares the modified timestamp, so a save made from an out-of-date copy is refused. It resets values in fields the user may not write, checks that every link exists, and runs before_validate, validate and before_save. Only then does it check mandatory fields, which is why validate can still fill a required field. After that it writes the row, runs on_update, clears the cache, saves the version, and runs on_change last. A new document also runs before_insert, picks its name, and runs after_insert before on_update.

What does Submit add?

The same save, plus the Submit permission. After validate it runs before_submit. After the write it runs on_update and then on_submit. So a handler on on_update runs when the document is submitted as well as on every draft save. If the DocType has a Workflow, its transition rule is checked together with the mandatory fields.

What does Cancel skip?

Most of it. Cancel runs before_cancel and on_cancel and nothing else from the save path: no before_validate, no validate, no mandatory check. After on_cancel, Frappe refuses the cancel if an active document still links to this one. An amendment is a new document, named after the cancelled one with -1 added, and it goes through Save and Submit again.

Where does your code go?

For each event Frappe runs, in this order: the method on the DocType's controller, the doc_events handlers for that DocType, the handlers for *, Notification rules, Webhooks, Server Scripts and, on v16, Automations. Handlers from several apps run in the order of the site's installed apps. A Server Script's "Before Save" is the validate hook, and there is no Server Script event for before_save.

What happens when something fails?

A request is one database transaction. If anything raises, Frappe rolls the whole request back, including a row that the save has already written. That is why a frappe.throw in on_submit undoes the submit. Webhooks wait in a list and are sent after the commit, so a failed save sends none, and a document saved twice in one request fires a webhook once, with the last version.

Two limits. Your own code that calls frappe.db.commit() keeps whatever it committed. And a background job you start before the commit may read the old data unless you pass enqueue_after_commit=True.

What changed from v15 to v16?

The order above is the same on v15.123.0: the insert, save, before-save and post-save hooks and the docstatus transitions were compared function by function. v16 adds discard() with before_discard and on_discard, the Automations step, and the handler(doc) form without a method argument. In v16, doc_events handlers also cannot commit.

Where this comes from

Read at Frappe v16.52.0 and ERPNext v16.50.0. “(v15)” marks Frappe v15.123.0 and “(ERPNext v15)” marks ERPNext v15.122.0. Each link opens that file at that tag, on that line.

Official page: Controllers, read 14 October 2026. Pattern: the same payment booked twice.

Your next step

See it live

Run a sprint

Finish a task in this week's sprint, log the hour it took, and see the sprint's burndown, all on ERPNext's own records.

You end with hours booked on a task · 2 screens · about 3 min

No sign-up, no card, no account.

Next step · Architecture guide

Now see how we build a Frappe app

A separate app in your own Git, so upgrades keep working.