Skip to content
AI & Agents
Skill

/frappe-impl-clientscripts

Use when implementing client-side form features in Frappe/ERPNext: field visibility, cascading filters, calculated fields, custom buttons, server calls, form validation, child table logic, debugging. Covers step-by-step workflows from Setup > Client Script through migration to

From plugin
frappe-claude-skill-package
17961 skills
Install
$ npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-impl-clientscripts --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-impl-clientscripts

Context preview

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

Use when implementing client-side form features in Frappe/ERPNext: field visibility, cascading filters, calculated fields, custom buttons, server calls, form validation, child table logic, debugging. Covers step-by-step workflows from Setup > Client Script through migration to

SKILL.md

frappe-impl-clientscripts.SKILL.md
name: frappe-impl-clientscripts
description: >
  Use when implementing client-side form features in Frappe/ERPNext:
  field visibility, cascading filters, calculated fields, custom buttons,
  server calls, form validation, child table logic, debugging.
  Covers step-by-step workflows from Setup > Client Script through
  migration to custom app JS. Keywords: how to implement client script,
  form logic workflow, dynamic UI, calculate fields, frm.call, frappe.call,
  frappe.xcall, client script testing, field dependency, custom button,
  how to hide field, show field based on value, add button to form, calculate total, dynamic form.
license: MIT
compatibility: "Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16."
metadata:
  author: OpenAEC-Foundation
  version: "2.0"

Client Scripts — Implementation Workflows

Step-by-step workflows for building client-side form features. For exact API syntax, see `frappe-syntax-clientscripts`.

**Version**: v14/v15/v16 | **Note**: v13 renamed "Custom Script" to "Client Script"

Quick Decision: Client or Server?

MUST the logic ALWAYS execute (imports, API, Data Import)?
├── YES → Server Script or Controller
└── NO  → What is the goal?
         ├── UI feedback / UX → Client Script
         ├── Show/hide fields → Client Script
         ├── Link filters → Client Script
         ├── Data validation → BOTH (client for UX, server for integrity)
         └── Calculations → Client for display, server for critical

**Rule**: ALWAYS use Client Scripts for UX. ALWAYS back critical logic with server-side validation.

Workflow 1: Create a Client Script via UI

1. Navigate to **Setup > Client Script** (or type "New Client Script" in awesomebar) 2. Select the target **DocType** 3. ALWAYS set **Enabled** checkbox 4. Write script using the `frappe.ui.form.on` pattern 5. Save — script is active immediately (no restart needed) 6. Open target DocType form → test behavior 7. Open browser DevTools Console (F12) for debugging

**When to migrate to custom app**: ALWAYS migrate when the script exceeds 50 lines, needs version control, or must be deployed across environments.

Workflow 2: Choose the Right Event

WHAT DO YOU WANT?
├── Set link filters         → setup (once, earliest lifecycle)
├── Add custom buttons       → refresh (re-added after each render)
├── Show/hide fields         → refresh + {fieldname} (BOTH needed)
├── Validate before save     → validate (frappe.throw stops save)
├── Action after save        → after_save
├── Calculate on change      → {fieldname} handler
├── Child row added          → {tablename}_add
├── Child row removed        → {tablename}_remove
├── Child field changed      → Child DocType: {fieldname}
├── One-time init            → setup or onload
└── After full DOM render    → onload_post_render

> See [references/decision-tree.md](references/decision-tree.md) for complete event timing matrix.

Workflow 3: Field Visibility Toggle

**Goal**: Show "delivery_date" only when "requires_delivery" is checked.

**Step 1**: Implement BOTH refresh and fieldname events:

frappe.ui.form.on('Sales Order', {
    refresh(frm) {
        frm.trigger('requires_delivery'); // Set initial state
    },
    requires_delivery(frm) {
        frm.toggle_display('delivery_date', frm.doc.requires_delivery);
        frm.toggle_reqd('delivery_date', frm.doc.requires_delivery);
    }
});

**Why both?** `refresh` sets state on form load. `{fieldname}` responds to user interaction. NEVER use only one — the form will show wrong state on load or on change.

Workflow 4: Cascading Link Filters

**Goal**: Filter "city" based on selected "country".

frappe.ui.form.on('Customer', {
    setup(frm) {
        // ALWAYS set filters in setup — ensures consistency
        frm.set_query('city', () => ({
            filters: { country: frm.doc.country || '' }
        }));
    },
    country(frm) {
        frm.set_value('city', ''); // ALWAYS clear dependent field
    }
});

**Rule**: ALWAYS put `set_query` in `setup`. ALWAYS clear child fields when parent changes.

Workflow 5: Calculated Fields (Child Table)

**Goal**: Calculate row amounts and document totals.

frappe.ui.form.on('Invoice Item', {
    qty(frm, cdt, cdn) { calculate_row(frm, cdt, cdn); },
    rate(frm, cdt, cdn) { calculate_row(frm, cdt, cdn); },
    amount(frm) { calculate_totals(frm); }
});

frappe.ui.form.on('Invoice', {
    items_remove(frm) { calculate_totals(frm); }
});

function calculate_row(frm, cdt, cdn) {
    let row = frappe.get_doc(cdt, cdn);
    frappe.model.set_value(cdt, cdn, 'amount',
        flt(row.qty) * flt(row.rate));
}

function calculate_totals(frm) {
    let total = (frm.doc.items || []).reduce(
        (sum, row) => sum + flt(row.amount), 0);
    frm.set_value('grand_total', flt(total, 2));
}

**Rules**:

  • ALWAYS use `flt()` for numeric operations (handles null/undefined)
  • ALWAYS handle `items_remove` — totals must recalculate on row deletion
  • NEVER call `refresh_field` after `set_value` — it triggers automatically

Workflow 6: Server Calls: Which Method to Use

NEED TO CALL THE SERVER?
├── Fetch a single value?
│   └── frappe.db.get_value(doctype, name, fields)
│       Returns: Promise — lightweight, no whitelist needed
│
├── Call a document's controller method?
│   └── frm.call(method, args)
│       Requires: @frappe.whitelist() on controller method
│       Auto-includes: doctype, docname, doc context
│
├── Call a standalone whitelisted function?
│   └── frappe.call({method: 'dotted.path', args: {}})
│       Requires: @frappe.whitelist() decorator
│       Returns: Promise with r.message
│
└── Need Promise-only (no callback)?
    └── frappe.xcall('dotted.path', args)
        Same as frappe.call but returns clean Promise

**Example — frm.call**:

frm.call('calculate_taxes').then(r => {
    frm.reload_doc();  // Refresh after server-side changes
});

**Example — frappe.

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
15d ago
Added

Repo: Impertio-Studio/Frappe_Claude_Skill_Package