agentsclimarketplace

Frappe impl hooks

Skill Impertio-Studio/Frappe_Claude_Skill_Package/skills/source/impl/frappe-impl-hooks

Use when implementing hooks.py configurations in a Frappe custom app. Covers step-by-step workflows for doc_events, scheduler_events, override/extend_doctype_class, permission hooks, extend_bootinfo, fixtures, asset injection, website hooks, and doctype_js. Prevents broken transactions, missed migrations, and multi-app conflicts. Keywords: hooks.py, doc_events, scheduler_events, override doctype,, how to add hook, when to use doc_events, scheduler setup, override existing behavior. extend doctype class, permission hook, scheduler job, fixtures, doctype_js, extend_bootinfo, website hooks.From its SKILL.md

Install
npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-impl-hooks

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

12.0 KB, ~2.9k tokens by cl100k_base, as published. Nobody here has run it

Frappe Hooks Implementation Workflow

Step-by-step workflows for implementing hooks.py configurations. For API syntax reference, see frappe-syntax-hooks.

Version: v14/v15/v16 (V16-specific features noted)


Master Decision: What Are You Implementing?

WHAT DO YOU WANT TO ACHIEVE?
│
├─► React to document lifecycle events?
│   ├─► On OTHER app's DocTypes → doc_events in hooks.py
│   ├─► On YOUR OWN DocTypes → controller methods (preferred)
│   └─► On ALL DocTypes → doc_events with "*" wildcard
│
├─► Run code on a schedule?
│   └─► scheduler_events (daily, hourly, cron, etc.)
│
├─► Modify an existing DocType's behavior?
│   ├─► V16+: extend_doctype_class (RECOMMENDED)
│   └─► V14/V15: override_doctype_class (last app wins!)
│
├─► Override an existing API endpoint?
│   └─► override_whitelisted_methods
│
├─► Add custom permission logic?
│   ├─► List filtering → permission_query_conditions
│   └─► Document-level → has_permission
│
├─► Send config data to client on page load?
│   └─► extend_bootinfo
│
├─► Export/import configuration?
│   └─► fixtures
│
├─► Add JS/CSS to desk or portal?
│   ├─► Desk-wide → app_include_js / app_include_css
│   ├─► Portal-wide → web_include_js / web_include_css
│   └─► Specific form → doctype_js
│
├─► Customize website/portal behavior?
│   └─► website_context, portal_menu_items, website_route_rules
│
└─► Hook into session/auth lifecycle?
    └─► on_login, on_session_creation, on_logout

Workflow 1: Implementing doc_events

When to Use

Use doc_events when you need to react to document lifecycle events on DocTypes owned by OTHER apps (ERPNext, Frappe core). For YOUR OWN DocTypes, ALWAYS prefer controller methods.

Step-by-Step

Step 1: Choose the right event (see references/decision-tree.md)

BEFORE save: validate (every save), before_insert (new only)
AFTER save:  after_insert (new only), on_update (every save), on_change (any change)
SUBMIT flow: before_submit → on_submit → on_change
CANCEL flow: before_cancel → on_cancel → on_change
DELETE:      on_trash (before), after_delete (after)
RENAME:      before_rename, after_rename

Step 2: Add to hooks.py

# myapp/hooks.py
doc_events = {
    "Sales Invoice": {
        "validate": "myapp.events.sales_invoice.validate",
        "on_submit": "myapp.events.sales_invoice.on_submit"
    }
}

Step 3: Create handler module

# myapp/events/sales_invoice.py
import frappe

def validate(doc, method=None):
    """Changes to doc ARE saved (before-save event)."""
    if doc.grand_total < 0:
        frappe.throw("Total cannot be negative")

def on_submit(doc, method=None):
    """Document already saved. Use db_set_value for changes."""
    frappe.db.set_value("Sales Invoice", doc.name,
                        "custom_external_id", create_external(doc))

Step 4: Deploy

bench --site sitename migrate

Step 5: Test

bench --site sitename execute myapp.events.sales_invoice.validate --kwargs '{"doc_name": "INV-001"}'
# Or in bench console:
# doc = frappe.get_doc("Sales Invoice", "INV-001"); doc.save()

Critical Rules for doc_events

  • NEVER call frappe.db.commit() inside a doc_event handler — Frappe manages the transaction
  • NEVER modify doc fields in on_update — changes are lost; use frappe.db.set_value() instead
  • ALWAYS accept method=None as second parameter in handler signature
  • ALWAYS use rename signature: def handler(doc, method, old, new, merge)
  • ALWAYS run bench --site sitename migrate after changing hooks.py

Workflow 2: Implementing scheduler_events

Step-by-Step

Step 1: Choose frequency

FrequencyShort (< 5 min)Long (5-25 min)
Every tickall—
Hourlyhourlyhourly_long
Dailydailydaily_long
Weeklyweeklyweekly_long
Monthlymonthlymonthly_long
Customcroncron (use long queue manually)

Step 2: Add to hooks.py

scheduler_events = {
    "daily": ["myapp.tasks.daily_cleanup"],
    "daily_long": ["myapp.tasks.heavy_sync"],
    "cron": {
        "0 9 * * 1-5": ["myapp.tasks.weekday_report"]
    }
}

Step 3: Implement task (NO arguments)

# myapp/tasks.py
import frappe

def daily_cleanup():
    """Scheduler calls with NO arguments."""
    frappe.db.delete("Error Log", {
        "creation": ["<", frappe.utils.add_days(None, -30)]
    })
    frappe.db.commit()

def heavy_sync():
    """Long task — commit periodically."""
    records = get_records_to_sync()
    for i, record in enumerate(records):
        process(record)
        if i % 100 == 0:
            frappe.db.commit()
    frappe.db.commit()

Step 4: Deploy and verify

bench --site sitename migrate
bench --site sitename scheduler enable
bench --site sitename scheduler status
# Test manually:
bench --site sitename execute myapp.tasks.daily_cleanup

Critical Rules for Scheduler

  • NEVER add parameters to scheduler task functions — the scheduler passes none
  • ALWAYS use _long variants for tasks exceeding 5 minutes (default queue timeout is 5 min)
  • ALWAYS commit periodically in long tasks to save progress
  • Tasks > 25 minutes: split into chunks or use frappe.enqueue()

Workflow 3: Implementing extend_doctype_class (V16+)

Step-by-Step

Step 1: Add to hooks.py

extend_doctype_class = {
    "Sales Invoice": ["myapp.extensions.sales_invoice.SalesInvoiceMixin"]
}

Step 2: Create mixin class

# myapp/extensions/sales_invoice.py
import frappe
from frappe.model.document import Document

class SalesInvoiceMixin(Document):
    def validate(self):
        super().validate()  # ALWAYS call super() FIRST
        self.custom_validation()

    def custom_validation(self):
        if self.grand_total > 1000000:
            frappe.msgprint("High-value invoice", indicator="orange")

Step 3: Deploy — bench --site sitename migrate

When to Use extend vs override

  • ALWAYS prefer extend_doctype_class on V16+ — multiple apps can extend safely
  • ONLY use override_doctype_class when you must completely replace controller logic
  • On V14/V15, override_doctype_class is the only option — last installed app wins

Workflow 4: Implementing Permission Hooks

Step-by-Step

Step 1: Add to hooks.py

permission_query_conditions = {
    "Sales Invoice": "myapp.permissions.si_query"
}
has_permission = {
    "Sales Invoice": "myapp.permissions.si_permission"
}

Step 2: Implement handlers

# myapp/permissions.py
import frappe

def si_query(user):
    """Returns SQL WHERE clause for list filtering."""
    if not user:
        user = frappe.session.user
    if "Sales Manager" in frappe.get_roles(user):
        return ""  # See all
    return f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"

def si_permission(doc, user=None, permission_type=None):
    """Returns True (allow), False (deny), or None (use default)."""
    if not user:
        user = frappe.session.user
    if permission_type == "write" and doc.status == "Closed":
        return False
    return None

Critical Rules for Permission Hooks

  • permission_query_conditions ONLY works with get_list, NEVER with get_all
  • has_permission can ONLY deny access — returning True does NOT grant additional permissions
  • ALWAYS handle user=None by defaulting to frappe.session.user

Workflow 5: Asset Injection and doctype_js

Adding Global JS/CSS

# hooks.py
app_include_js = "/assets/myapp/js/myapp.min.js"       # Desk
app_include_css = "/assets/myapp/css/myapp.min.css"     # Desk
web_include_js = "/assets/myapp/js/portal.min.js"       # Portal
web_include_css = "/assets/myapp/css/portal.min.css"    # Portal

Extending a Specific Form

# hooks.py
doctype_js = {
    "Sales Invoice": "public/js/sales_invoice.js"
}
// myapp/public/js/sales_invoice.js
frappe.ui.form.on("Sales Invoice", {
    refresh(frm) {
        if (frm.doc.docstatus === 1) {
            frm.add_custom_button(__("Custom Action"), () => {
                frappe.call({
                    method: "myapp.api.custom_action",
                    args: { invoice: frm.doc.name },
                    freeze: true
                });
            }, __("Actions"));
        }
    }
});

ALWAYS run bench build --app myapp after changing JS/CSS files.


Workflow 6: Fixtures, Boot Info, and Website Hooks

Fixtures

fixtures = [
    {"dt": "Custom Field", "filters": [["module", "=", "My App"]]},
    {"dt": "Property Setter", "filters": [["module", "=", "My App"]]}
]

NEVER export fixtures without filters — it captures ALL apps' customizations.

extend_bootinfo

extend_bootinfo = "myapp.boot.extend_with_config"
def extend_with_config(bootinfo):
    bootinfo.my_app = {"feature_enabled": True}
    # NEVER send secrets — bootinfo is visible in browser DevTools

Website Hooks

website_route_rules = [
    {"from_route": "/shop/<category>", "to_route": "shop"}
]
portal_menu_items = [
    {"title": "My Orders", "route": "/my-orders", "role": "Customer"}
]
on_login = "myapp.handlers.on_login"
on_logout = "myapp.handlers.on_logout"

Migration: Moving Logic Between Hooks, Controllers, and Server Scripts

FromToSteps
Server Script → hooks.py1. Create Python handler, 2. Add doc_events, 3. Disable Server Script, 4. Migrate
hooks.py → Controller1. Move logic to doctype .py, 2. Remove doc_events entry, 3. Migrate
Controller → hooks.py1. Create events module, 2. Add doc_events, 3. Remove from controller, 4. Migrate

ALWAYS migrate after ANY hooks.py change: bench --site sitename migrate


Handler Signatures Quick Reference

HookSignature
doc_eventsdef handler(doc, method=None):
rename eventsdef handler(doc, method, old, new, merge):
scheduler_eventsdef handler(): (no args)
extend_bootinfodef handler(bootinfo):
permission_querydef handler(user): returns SQL string
has_permissiondef handler(doc, user=None, permission_type=None): returns True/False/None
on_logindef handler(login_manager):
on_logoutdef handler():

Version Differences

FeatureV14V15V16
doc_eventsYesYesYes
scheduler_eventsYesYesYes
override_doctype_classYesYesYes
extend_doctype_classNoNoYes
permission hooksYesYesYes
Scheduler tick interval~4 min~4 min~60 sec
auth_hooksNoYesYes

Reference Files

FileContents
decision-tree.mdComplete hook selection flowcharts
workflows.mdStep-by-step implementation patterns
examples.mdWorking code examples for all hook types

What ships with it: 4 files

58.7 KB alongside SKILL.md

references/

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.