> 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 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.
npx skills add https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-impl-clientscripts
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"
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.
frappe.ui.form.on patternWhen to migrate to custom app: ALWAYS migrate when the script exceeds 50 lines, needs version control, or must be deployed across environments.
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 for complete event timing matrix.
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.
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.
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:
flt() for numeric operations (handles null/undefined)items_remove — totals must recalculate on row deletionrefresh_field after set_value — it triggers automaticallyNEED 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.xcall:
let result = await frappe.xcall(
'myapp.api.check_credit', { customer: frm.doc.customer });
frappe.ui.form.on('Sales Order', {
refresh(frm) {
// ALWAYS check conditions before adding buttons
if (!frm.is_new() && frm.doc.docstatus === 1) {
frm.add_custom_button(__('Create Invoice'), () => {
create_invoice(frm);
}, __('Create')); // Group label
}
}
});
Rules:
refresh — they are cleared on each renderfrm.is_new() — buttons on unsaved docs cause errors__() for translationsetup or onload — UI not readyfrappe.ui.form.on('Sales Order', {
async validate(frm) {
if (!frm.doc.customer || !frm.doc.grand_total) return;
let r = await frappe.call({
method: 'myapp.api.check_credit',
args: {
customer: frm.doc.customer,
amount: frm.doc.grand_total
}
});
if (r.message && !r.message.allowed) {
frappe.throw(__('Credit limit exceeded. Available: {0}',
[r.message.available]));
}
}
});
Rules:
async/await for server calls in validatefrappe.throw() to stop save — msgprint does NOT stop itvalidate without user expectationconsole.log(frm.doc) in your event handlercur_frm in Console to inspect current form statefrappe.call requestsfrappe.ui.form.handlers to see registered event handlersDebug pattern:
frappe.ui.form.on('MyDocType', {
my_field(frm) {
console.log('Field changed:', frm.doc.my_field);
// ... actual logic
}
});
myapp/myapp/public/js/sales_order.jsfrappe.ui.form.on wrapper)hooks.py: doctype_js = {
"Sales Order": "public/js/sales_order.js"
}
bench build (or bench watch for development)ALWAYS migrate when: version control needed, multi-environment deployment, script > 50 lines, team collaboration required.
| Rule | Why |
|------|-----|
| set_query in setup only | Prevents re-registration on every refresh |
| Batch set_value calls | frm.set_value({a: 1, b: 2}) — one update, not two |
| Cache server responses | Store in frm._cache_key to avoid repeat calls |
| NEVER query in loops | Fetch all data once, build lookup map |
| Use frappe.db.get_value | Lighter than frappe.call for simple lookups |
frappe-syntax-clientscripts — Exact API syntax and method signaturesfrappe-errors-clientscripts — Error handling and common pitfallsfrappe-syntax-whitelisted — Server methods callable from clientfrappe-core-database — frappe.db.* client-side APIfrappe-impl-serverscripts — When to move logic server-side> See references/decision-tree.md for event selection.
> See references/workflows.md for extended patterns.
> See references/examples.md for 10+ complete examples.
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Use when implementing any feature or bugfix, before writing implementation code
Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
Expert guidance for systematic backtesting of trading strategies. Use when developing, testing, stress-testing, or validating quantitative trading strategies. Covers "beating ideas to death" methodology, parameter robustness testing, slippage modeling, bias prevention, and interpreting backtest results. Applicable when user asks about backtesting, strategy validation, robustness testing, avoiding overfitting, or systematic trading development.
Cloud laboratory platform for automated protein testing and validation. Use when designing proteins and needing experimental validation including binding assays, expression testing, thermostability measurements, enzyme activity assays, or protein sequence optimization. Also use for submitting experiments via API, tracking experiment status, downloading results, optimizing protein sequences for better expression using computational tools (NetSolP, SoluProt, SolubleMPNN, ESM), or managing protein design workflows with wet-lab validation.
This skill should be used for time series machine learning tasks including classification, regression, clustering, forecasting, anomaly detection, segmentation, and similarity search. Use when working with temporal data, sequential patterns, or time-indexed observations requiring specialized algorithms beyond standard ML approaches. Particularly suited for univariate and multivariate time series analysis with scikit-learn compatible APIs.
Take impertio-studio/frappe-impl-clientscripts from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.