agentsclimarketplace

Frappe desk customization

Skill lubusIN/frappe-skills/frappe-desk-customization

Frappe Agent Skills gives AI coding assistants structured Frappe knowledge for DocTypes, APIs, Desk customization, frontend apps, reports, testing, and production patterns.

Install
npx -y skills add lubusIN/frappe-skills --skill frappe-desk-customization

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

What its author says it does

Copied from the file, not written here

Customize Frappe Desk UI with form scripts, list view scripts, report scripts, dialogs, and client-side JavaScript APIs. Use when building interactive Desk experiences, adding custom buttons, or scripting form behavior.

SKILL.md

9.6 KB, as published. Nobody here has run it

Frappe Desk Customization

Customize the Frappe Desk admin UI with form scripts, list views, dialogs, and client-side APIs.

When to use

  • Adding custom buttons or actions to forms
  • Filtering Link fields dynamically
  • Toggling field visibility based on conditions
  • Customizing list view indicators and bulk actions
  • Building interactive dialogs and prompts
  • Adding client-side validation before save
  • Injecting scripts into other apps' DocTypes via hooks

Inputs required

  • Target DocType for customization
  • Whether script is app-level (version controlled) or Client Script (site-specific)
  • Events to hook into (refresh, validate, field change, etc.)
  • UI behavior requirements (buttons, filters, visibility)

Procedure

0) Choose script type

TypeLocationVersion ControlledUse Case
App-level form script<app>/<module>/doctype/<doctype>/<doctype>.jsYesStandard app behavior
Client ScriptDocType: Client ScriptNo (DB)Site-specific customization
Hook-injected scriptVia doctype_js in hooks.pyYesExtend other apps' DocTypes

1) Write form scripts

frappe.ui.form.on("My DocType", {
    // Called once during form setup
    setup(frm) {
        frm.set_query("customer", function() {
            return {
                filters: { "status": "Active" }
            };
        });
    },

    // Called every time form loads or refreshes
    refresh(frm) {
        if (frm.doc.status === "Draft") {
            frm.add_custom_button(__("Submit for Review"), function() {
                frappe.call({
                    method: "my_app.api.submit_for_review",
                    args: { name: frm.doc.name },
                    callback(r) {
                        frm.reload_doc();
                    }
                });
            }, __("Actions"));
        }

        // Toggle field visibility
        frm.toggle_display("discount_section", frm.doc.grand_total > 1000);

        // Set field properties
        frm.set_df_property("notes", "read_only", frm.doc.docstatus === 1);
    },

    // Called before save — return false to cancel
    validate(frm) {
        if (frm.doc.end_date < frm.doc.start_date) {
            frappe.msgprint(__("End date must be after start date"));
            frappe.validated = false;
        }
    },

    // Field change handler (use fieldname as key)
    customer(frm) {
        if (frm.doc.customer) {
            frappe.db.get_value("Customer", frm.doc.customer, "territory",
                function(r) {
                    frm.set_value("territory", r.territory);
                }
            );
        }
    },

    // Before save hook
    before_save(frm) {
        frm.doc.full_name = `${frm.doc.first_name} ${frm.doc.last_name}`;
    },

    // After save hook
    after_save(frm) {
        frappe.show_alert({
            message: __("Document saved successfully"),
            indicator: "green"
        });
    }
});

// Child table events
frappe.ui.form.on("My DocType Item", {
    qty(frm, cdt, cdn) {
        let row = locals[cdt][cdn];
        frappe.model.set_value(cdt, cdn, "amount", row.qty * row.rate);
        calculate_total(frm);
    },

    items_remove(frm) {
        calculate_total(frm);
    }
});

function calculate_total(frm) {
    let total = 0;
    (frm.doc.items || []).forEach(row => {
        total += row.amount || 0;
    });
    frm.set_value("grand_total", total);
}

2) Build dialogs and prompts

// Simple prompt
frappe.prompt(
    { fieldname: "reason", fieldtype: "Small Text", label: "Reason", reqd: 1 },
    function(values) {
        frappe.call({
            method: "my_app.api.reject",
            args: { name: frm.doc.name, reason: values.reason }
        });
    },
    __("Rejection Reason"),
    __("Reject")
);

// Multi-field dialog
let d = new frappe.ui.Dialog({
    title: __("Configure Settings"),
    fields: [
        { fieldname: "email", fieldtype: "Data", options: "Email", label: "Email", reqd: 1 },
        { fieldname: "frequency", fieldtype: "Select", options: "Daily\nWeekly\nMonthly", label: "Frequency" },
        { fieldname: "active", fieldtype: "Check", label: "Active", default: 1 }
    ],
    primary_action_label: __("Save"),
    primary_action(values) {
        frappe.call({
            method: "my_app.api.save_settings",
            args: values,
            callback() {
                d.hide();
                frappe.show_alert({ message: __("Settings saved"), indicator: "green" });
            }
        });
    }
});
d.show();

// Confirmation dialog
frappe.confirm(
    __("Are you sure you want to delete this?"),
    function() { /* Yes */ },
    function() { /* No */ }
);

3) Make server calls

// Standard call (callback)
frappe.call({
    method: "my_app.api.get_stats",
    args: { customer: frm.doc.customer },
    freeze: true,
    freeze_message: __("Loading..."),
    callback(r) {
        if (r.message) {
            frm.set_value("total_orders", r.message.total);
        }
    }
});

// Promise-based call
let result = await frappe.xcall("my_app.api.get_stats", {
    customer: frm.doc.customer
});

4) Customize list views

// my_app/public/js/sample_doc_list.js
// or via hooks: doctype_list_js = {"Sample Doc": "public/js/sample_doc_list.js"}

frappe.listview_settings["Sample Doc"] = {
    // Status indicator colors
    get_indicator(doc) {
        if (doc.status === "Open") return [__("Open"), "orange", "status,=,Open"];
        if (doc.status === "Closed") return [__("Closed"), "green", "status,=,Closed"];
        return [__("Draft"), "grey", "status,=,Draft"];
    },

    // Add bulk actions
    onload(listview) {
        listview.page.add_action_item(__("Mark as Closed"), function() {
            let names = listview.get_checked_items(true);
            frappe.call({
                method: "my_app.api.bulk_close",
                args: { names },
                callback() { listview.refresh(); }
            });
        });
    },

    // Hide default "New" button
    hide_name_column: true
};

5) Use realtime events

// Listen for server-side events
frappe.realtime.on("export_complete", function(data) {
    frappe.show_alert({
        message: __("Export complete: {0} records", [data.count]),
        indicator: "green"
    });
});

6) Inject scripts via hooks

To extend a DocType from another app without modifying it:

# hooks.py
doctype_js = {
    "Sales Order": "public/js/sales_order_custom.js"
}

doctype_list_js = {
    "Sales Order": "public/js/sales_order_list_custom.js"
}
# Rebuild assets after adding hook scripts
bench build --app my_app

7) Navigation and routing

// Navigate to a document
frappe.set_route("Form", "Sales Order", "SO-001");

// Navigate to list with filters
frappe.route_options = { "status": "Open" };
frappe.set_route("List", "Sales Order");

// Get current route
let route = frappe.get_route();

Verification

  • Form script loads without JS console errors
  • Custom buttons appear in correct conditions
  • Field visibility toggles work
  • Link field filters return correct options
  • Validation prevents invalid saves
  • List view indicators display correctly
  • Dialogs open, collect input, and submit

Failure modes / debugging

  • Script not loading: Check file path matches DocType; run bench build
  • Button not appearing: Check condition logic in refresh; verify frm.doc.docstatus
  • Event not firing: Verify event name matches exactly (case-sensitive)
  • Hook script ignored: Check hooks.py path; rebuild assets
  • frappe.call failing: Check method path; verify @frappe.whitelist() on server

Escalation

  • For server-side controller logic → frappe-doctype-development
  • For RPC endpoint implementation → frappe-api-development
  • For Frappe UI (Vue 3) frontends → frappe-frontend-development

References

Guardrails

  • Use frm.doc not doc directly: Always access document via frm.doc for consistency and reactivity
  • Validate before save: Use frm.validate() in validate event, not before_save
  • Async awareness: frappe.call() is async; use callbacks or async/await for sequential operations
  • Refresh after field changes: Call frm.refresh_field() or frm.refresh_fields() after programmatic changes
  • Check frm.is_new() appropriately: Some operations only make sense on saved documents

Common Mistakes

MistakeWhy It FailsFix
Missing frm.refresh_field() after set_valueUI doesn't updateCall frm.refresh_field('fieldname') after frm.set_value()
Wrong event hook nameEvent never firesUse exact names: refresh, validate, onload, before_save
Blocking UI with sync callsPage freezesUse frappe.call() with async: true (default)
Using cur_frm instead of frmBreaks in dialogs/multiple formsAlways use the frm parameter passed to handlers
Not checking frm.doc.docstatusButtons appear on submitted docsCheck frm.doc.docstatus == 0 before showing edit actions
console.log(frm.doc) showing stale dataDebugging confusionUse frm.reload_doc() or check network responses

Keep looking

Skills are one crate of 328,083. 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.