Skip to content
AI & Agents
Skill

/frappe-syntax-controllers

Use when writing Python Document Controllers for ERPNext/Frappe DocTypes. Covers lifecycle hooks (validate, on_update, on_submit), controller override, submittable documents, autoname patterns, UUID naming (v16), and the flags system. Keywords: document controller, lifecycle

From plugin
frappe-claude-skill-package
17961 skills
Install
$ npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-syntax-controllers --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/frappe-syntax-controllers

Context preview

The summary Claude sees to decide when to auto-load this skill.

Use when writing Python Document Controllers for ERPNext/Frappe DocTypes. Covers lifecycle hooks (validate, on_update, on_submit), controller override, submittable documents, autoname patterns, UUID naming (v16), and the flags system. Keywords: document controller, lifecycle

SKILL.md

frappe-syntax-controllers.SKILL.md
name: frappe-syntax-controllers
description: >
  Use when writing Python Document Controllers for ERPNext/Frappe DocTypes.
  Covers lifecycle hooks (validate, on_update, on_submit), controller
  override, submittable documents, autoname patterns, UUID naming (v16),
  and the flags system. Keywords: document controller, lifecycle hook,
  validate, on_update, on_submit, autoname, naming series, flags, v14-v16,
  controller example, lifecycle hook order, when to use validate, Python DocType class.
license: MIT
compatibility: "Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16."
metadata:
  author: OpenAEC-Foundation
  version: "2.0"

Frappe Syntax: Document Controllers

Document Controllers are Python classes that define all server-side logic for a DocType. EVERY DocType has exactly one controller file. The controller class extends `frappe.model.document.Document`.

Quick Reference

import frappe
from frappe import _
from frappe.model.document import Document

class SalesOrder(Document):
    def autoname(self):
        """Custom naming logic. Sets self.name."""
        self.name = f"SO-{self.customer_code}-{frappe.utils.now_datetime().year}"

    def validate(self):
        """MAIN validation — runs on EVERY save (insert and update).
        Changes to self ARE saved to database."""
        if not self.items:
            frappe.throw(_("Items are required"))
        self.total = sum(item.amount for item in self.items)

    def on_update(self):
        """After save — changes to self are NOT saved.
        Use frappe.db.set_value() for post-save field changes."""
        self.notify_linked_docs()

    def on_submit(self):
        """After submit (docstatus 0 -> 1). Create ledger entries here."""
        self.create_gl_entries()

    def on_cancel(self):
        """After cancel (docstatus 1 -> 2). Reverse ledger entries here."""
        self.reverse_gl_entries()

    @frappe.whitelist()
    def recalculate(self):
        """Exposed to client JS via frm.call('recalculate')."""
        self.total = sum(item.amount for item in self.items)
        return {"total": self.total}

File Location and Naming

| DocType Name | Class Name | File Path | |---|---|---| | Sales Order | `SalesOrder` | `selling/doctype/sales_order/sales_order.py` | | My Custom Doc | `MyCustomDoc` | `module/doctype/my_custom_doc/my_custom_doc.py` |

**Rule**: DocType name -> PascalCase class -> snake_case filename. ALWAYS match exactly.

---

Lifecycle Hook Execution Order

INSERT (new document)

before_insert -> before_naming -> autoname -> before_validate -> validate
-> before_save -> [db_insert] -> after_insert -> on_update -> on_change

SAVE (existing document)

before_validate -> validate -> before_save -> [db_update]
-> on_update -> on_change

SUBMIT (docstatus 0 -> 1)

before_validate -> validate -> before_submit -> [db_update]
-> on_submit -> on_update -> on_change

CANCEL (docstatus 1 -> 2)

before_cancel -> [db_update] -> on_cancel -> on_change

UPDATE AFTER SUBMIT

before_update_after_submit -> [db_update]
-> on_update_after_submit -> on_change

DELETE

on_trash -> [db_delete] -> after_delete

DISCARD [v15+]

before_discard -> [db_set docstatus=2] -> on_discard

**Complete hook reference with parameters**: See [lifecycle-methods.md](references/lifecycle-methods.md)

---

Hook Selection Decision Tree

What do you need to do?
|
+-- Validate data or calculate fields?
|   +-- validate (changes to self ARE saved)
|
+-- Action AFTER save (emails, sync, linked docs)?
|   +-- on_update (changes to self are NOT saved)
|
+-- Only for NEW documents?
|   +-- after_insert (runs once on first save only)
|
+-- Custom document name?
|   +-- autoname (set self.name)
|
+-- Before/after SUBMIT?
|   +-- Validate before submit? -> before_submit
|   +-- Create entries after submit? -> on_submit
|
+-- Before/after CANCEL?
|   +-- Check linked docs? -> before_cancel
|   +-- Reverse entries? -> on_cancel
|
+-- Cleanup before delete?
|   +-- on_trash
|
+-- React to ANY value change (including db_set)?
|   +-- on_change (MUST be idempotent)

---

Critical Rules

1. Changes after on_update are NOT saved

# WRONG - change is lost after on_update
def on_update(self):
    self.status = "Completed"  # NOT saved to database

# CORRECT - use db_set or frappe.db.set_value
def on_update(self):
    self.db_set("status", "Completed")

2. NEVER call frappe.db.commit() in controllers

# WRONG - breaks Frappe transaction management
def validate(self):
    frappe.db.commit()  # Can cause partial updates on error

# CORRECT - Frappe commits automatically at end of request
def validate(self):
    self.update_related()  # No commit needed

3. ALWAYS call super() when overriding

# WRONG - parent validation is skipped entirely
def validate(self):
    self.custom_check()

# CORRECT - parent logic preserved
def validate(self):
    super().validate()
    self.custom_check()

4. Use flags for recursion prevention

def on_update(self):
    if self.flags.get("from_linked_doc"):
        return
    linked = frappe.get_doc("Linked Doc", self.linked_doc)
    linked.flags.from_linked_doc = True
    linked.save()

5. NEVER put validation logic in on_update

# WRONG - document is already saved when this throws
def on_update(self):
    if self.total < 0:
        frappe.throw("Invalid total")  # Too late!

# CORRECT - validate BEFORE save
def validate(self):
    if self.total < 0:
        frappe.throw("Invalid total")  # Blocks save

---

Document Naming (autoname)

| Method | Example | Result | Version | |---|---|---|---| | `field:fieldname` | `field:customer_name` | `ABC Company` | All | | `naming_series:` | `naming_series:` | `SO-2024-00001` | All | | Expression | `PRE-.#####` | `PRE-00001` | All | | Old-style format | `INV-{YYYY}-{####}` | `INV

Read more
Ships withfrappe-claude-skill-package

60 deterministic Claude AI skills for Frappe Framework & ERPNext v14-v16 development and operations

Get the whole plugin
Stats
178
Stars
53
Forks
Maintained
Maintenance
Python
Language
2mo ago
Last commit
8mo ago
Created
14d ago
Added

Repo: Impertio-Studio/Frappe_Claude_Skill_Package