Skip to content
AI & Agents
Skill

/frappe-impl-controllers

Use when building Document Controllers in a custom Frappe app: file creation, lifecycle hooks, validation, autoname, submittable workflows, controller override, child table controllers, flags system, migration from hooks.py and Server Scripts. Keywords: how to implement

From plugin
frappe-claude-skill-package
20061 skills

Claude Code

$ npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-impl-controllers --agent claude-code

SKILL.md

frappe-impl-controllers.SKILL.md
name: frappe-impl-controllers
description: >
  Use when building Document Controllers in a custom Frappe app:
  file creation, lifecycle hooks, validation, autoname, submittable
  workflows, controller override, child table controllers, flags system,
  migration from hooks.py and Server Scripts. Keywords: how to implement
  controller, which hook to use, validate vs on_update, override controller,
  submittable document, autoname, flags, extend_doctype_class, controller
  testing, child table controller,
  which hook to use, when does validate run, how to override save, document lifecycle.
license: MIT
compatibility: "Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16."
metadata:
  author: OpenAEC-Foundation
  version: "2.0"

Document Controllers — Implementation Workflows

Step-by-step workflows for building server-side DocType logic with full Python power. For exact syntax, see `frappe-syntax-controllers`.

**Version**: v14/v15/v16 | **v15+**: Supports auto-generated type annotations

Quick Decision: Controller vs Server Script?

NEED full Python (imports, classes, generators)?     → Controller
NEED external libraries (requests, pandas)?          → Controller
NEED try/except with rollback?                       → Controller
NEED frappe.enqueue() for background jobs?           → Controller
NEED to extend standard ERPNext DocType?             → Controller
Quick validation without custom app?                 → Server Script
Simple auto-fill or notification?                    → Server Script

**Rule**: ALWAYS use Controllers when you need a custom app. ALWAYS use Server Scripts for no-code prototyping.

Workflow 1: Create a New Controller

**Step 1**: Create DocType via Frappe UI or `bench new-doctype`

**Step 2**: File is auto-generated at:

apps/myapp/myapp/{module}/doctype/{doctype_name}/{doctype_name}.py

**Step 3**: Implement the controller class:

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

class MyDocType(Document):
    def validate(self):
        self.validate_dates()
        self.calculate_totals()

    def validate_dates(self):
        if self.from_date and self.to_date and self.from_date > self.to_date:
            frappe.throw(_("From Date cannot be after To Date"))

    def calculate_totals(self):
        self.total = sum(item.amount for item in self.items)

**Step 4**: Run `bench restart` (or `bench watch` for hot-reload in dev)

**Naming convention**: DocType "Sales Order" → class `SalesOrder`, file `sales_order.py`

Workflow 2: Choose the Right Hook

WHAT DO YOU WANT?
├── Validate data / calculate fields before save?
│   └── validate — changes to self ARE saved
│
├── Action AFTER save (emails, linked docs, logs)?
│   └── on_update — changes to self NOT saved (use db_set)
│
├── Only for NEW documents?
│   └── after_insert
│
├── Before/after SUBMIT?
│   ├── Check before submit → before_submit
│   └── Ledger entries after → on_submit
│
├── Before/after CANCEL?
│   ├── Prevent cancel → before_cancel
│   └── Reverse entries → on_cancel
│
├── Before DELETE?
│   └── on_trash (throw to prevent)
│
├── Custom document naming?
│   └── autoname
│
└── Detect ANY change (including db_set)?
    └── on_change

> See [references/decision-tree.md](references/decision-tree.md) for all hooks with execution order.

CRITICAL: validate vs on_update

| Aspect | `validate` | `on_update` | |--------|-----------|-------------| | When | Before DB write | After DB write | | `self.x = y` saved? | YES | **NO** — use `db_set` | | Can abort with throw? | YES | Already saved | | `get_doc_before_save()` | Available | Available | | Use for | Validation, calculations | Notifications, linked docs |

# WRONG — changes in on_update are NOT saved
def on_update(self):
    self.status = "Completed"  # LOST!

# CORRECT — use db_set
def on_update(self):
    frappe.db.set_value(self.doctype, self.name, "status", "Completed")

Workflow 3: Validation with Error Collection

def validate(self):
    errors = []
    if not self.items:
        errors.append(_("At least one item is required"))
    for item in self.items:
        if item.qty <= 0:
            errors.append(_("Row {0}: Qty must be positive").format(item.idx))
    if self.from_date > self.to_date:
        errors.append(_("From Date cannot be after To Date"))
    if errors:
        frappe.throw("<br>".join(errors))

Workflow 4: Detect Field Changes

def validate(self):
    old = self.get_doc_before_save()
    if old and old.status != self.status:
        self.flags.status_changed = True
        self.status_changed_on = frappe.utils.now()

def on_update(self):
    if self.flags.get('status_changed'):
        self.notify_status_change()

**Rule**: ALWAYS use `self.flags` to pass data between hooks. NEVER rely on external state.

Workflow 5: Custom Naming (autoname)

from frappe.model.naming import getseries

def autoname(self):
    # Format: PRJ-CUST-2025-001
    code = (self.customer or "GEN")[:4].upper()
    year = frappe.utils.getdate(self.start_date or frappe.utils.today()).year
    prefix = f"PRJ-{code}-{year}-"
    self.name = getseries(prefix, 3)

**Alternative — before_naming**:

def before_naming(self):
    if self.is_priority:
        self.naming_series = "PRIORITY-.#####"
    else:
        self.naming_series = "STD-.#####"

Workflow 6: Submittable Document

DRAFT (docstatus=0) → submit() → SUBMITTED (docstatus=1) → cancel() → CANCELLED (docstatus=2)

submit():  validate → before_submit → [DB: docstatus=1] → on_update → on_submit
cancel():  before_cancel → [DB: docstatus=2] → on_cancel
class PurchaseOrder(Document):
    def validate(self):
        self.validate_items()
        self.calculate_totals()

    def before_submit(self):
        # ONLY submit-specific checks here
        if self.total > 100000 and not self.manager_approval:
            frappe.thro
Read more
@impertio-studio
Ships withfrappe-claude-skill-package

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

Stars on GitHub

Momentum

stars in total
200
Star history of Impertio-Studio/Frappe_Claude_Skill_Package on GitHub, a chart by star-history.com

More skills from other plugins.

1password

1password

Set up and use 1Password CLI for sign-in, desktop integration, and reading or injecting secrets.

openclaw2w393k
@openclaw@openclawInstall Skill

Questions about frappe-impl-controllers.

Which coding agents can use frappe-impl-controllers?
frappe-impl-controllers installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.