Frappe_Claude_Skill_Package frappe-syntax-clientscripts
install
source · Clone the upstream repo
git clone https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/source/syntax/frappe-syntax-clientscripts" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-syntax-clientscripts && rm -rf "$T"
manifest:
skills/source/syntax/frappe-syntax-clientscripts/SKILL.mdsource content
Frappe Client Scripts Syntax
Client Scripts run in the browser and control all UI interactions in Frappe/ERPNext. Create them via Setup > Client Script or in custom apps under
public/js/.
CRITICAL: Client Script validations ONLY apply in the browser form view. API calls and System Console bypass them. ALWAYS pair with Server Scripts for security-critical validation.
Quick Reference
| Action | Code |
|---|---|
| Set value | |
| Get value | |
| Hide field | |
| Make mandatory | |
| Make read-only | |
| Set field property | |
| Filter Link field | |
| Call server | |
| Call doc method | |
| Prevent save | |
| Add button | |
| Add child row | |
| Show alert | |
| Translate string | or |
Event Decision Tree
What do you need to do? │ ├─ One-time setup (queries, formatters)? │ └─ ALWAYS use setup — runs once per form instance │ ├─ Show/hide fields, add buttons, update UI? │ └─ ALWAYS use refresh — fires after every load/reload │ ├─ Validate data before save? │ └─ ALWAYS use validate — use frappe.throw() to block save │ ├─ Modify data right before server save? │ └─ Use before_save — last chance to change values │ ├─ Run logic after successful save? │ └─ Use after_save — document is persisted │ ├─ React to a field value change? │ └─ Use the fieldname as the event name │ ├─ Intercept workflow state change? │ └─ Use before_workflow_action / after_workflow_action │ └─ Manipulate DOM after full render? └─ Use onload_post_render — NEVER use jQuery selectors directly
See references/events.md for complete event list and execution order.
Form Event Registration
// Parent form events frappe.ui.form.on('Sales Order', { setup(frm) { }, // Once per form instance refresh(frm) { }, // After every load/reload validate(frm) { }, // Before save — throw to block fieldname(frm) { } // On field value change }); // Child table events — ALWAYS register on the CHILD doctype frappe.ui.form.on('Sales Order Item', { qty(frm, cdt, cdn) { let row = frappe.get_doc(cdt, cdn); frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.rate); }, items_add(frm, cdt, cdn) { }, // Row added items_remove(frm) { }, // Row removed (no cdt/cdn) items_move(frm) { } // Row reordered });
Value Manipulation
// ALWAYS use frm.set_value() — NEVER assign frm.doc.field directly frm.set_value('status', 'Approved'); // Single frm.set_value({status: 'Approved', priority: 'High'}); // Multiple // Read values (read-only — NEVER write via frm.doc) let val = frm.doc.fieldname; let items = frm.doc.items; // Child table array
Field Properties
// Show/hide (accepts single field or array) frm.toggle_display(['priority', 'due_date'], frm.doc.status === 'Open'); // Mandatory toggle frm.toggle_reqd('due_date', true); // Read-only toggle frm.toggle_enable('amount', false); // false = read-only // Arbitrary property change frm.set_df_property('status', 'options', ['New', 'Open', 'Closed']); frm.set_df_property('amount', 'read_only', 1); frm.set_df_property('notes', 'label', 'Internal Notes'); // Intro message at form top frm.set_intro('This document is pending review', 'orange');
Link Field Filters
// ALWAYS set queries in setup event — NEVER in refresh frappe.ui.form.on('Sales Order', { setup(frm) { // Simple filter frm.set_query('customer', () => ({ filters: { disabled: 0 } })); // Child table filter frm.set_query('item_code', 'items', (doc, cdt, cdn) => { let row = locals[cdt][cdn]; return { filters: { is_sales_item: 1 } }; }); // Server-side query for complex logic frm.set_query('customer', () => ({ query: 'myapp.queries.get_filtered_customers', filters: { region: frm.doc.region } })); } });
Server Communication
// frappe.call — whitelisted Python method let r = await frappe.call({ method: 'myapp.api.process_data', args: { customer: frm.doc.customer }, freeze: true, freeze_message: __('Processing...') }); if (r.message) { /* use r.message */ } // frm.call — document controller method let result = await frm.call('calculate_taxes', { include_shipping: true }); // frappe.db shortcuts let val = await frappe.db.get_value('Customer', name, 'credit_limit'); let list = await frappe.db.get_list('Sales Order', { filters: { customer: frm.doc.customer }, fields: ['name', 'grand_total'], order_by: 'creation desc', limit: 10 });
Child Table Operations
// Add row — ALWAYS call refresh_field after let row = frm.add_child('items', { item_code: 'ITEM-001', qty: 5 }); frm.refresh_field('items'); // Clear all rows frm.clear_table('items'); frm.refresh_field('items'); // Modify existing rows — refresh_field ONCE after loop frm.doc.items.forEach(row => { row.discount = row.qty > 10 ? 5 : 0; }); frm.refresh_field('items'); // Set child row value (inside child event handler) frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.rate); // Mark form dirty after programmatic changes frm.dirty();
Custom Buttons
refresh(frm) { if (frm.doc.docstatus === 1) { // Grouped dropdown frm.add_custom_button(__('Invoice'), () => { frappe.model.open_mapped_doc({ method: 'erpnext.selling.doctype.sales_order.sales_order.make_sales_invoice', frm: frm }); }, __('Create')); // Primary action frm.page.set_primary_action(__('Process'), () => { frm.call('process').then(() => frm.reload_doc()); }); } // ALWAYS guard buttons with state checks if (!frm.is_new() && frm.doc.docstatus === 0) { frm.add_custom_button(__('Validate'), () => { /* ... */ }); } }
List View Customization
frappe.listview_settings['Task'] = { add_fields: ['status', 'priority'], filters: [['status', '!=', 'Cancelled']], hide_name_column: true, get_indicator(doc) { // ALWAYS return [label, color, filter_field + ',' + filter_value] if (doc.status === 'Open') return [__('Open'), 'orange', 'status,=,Open']; if (doc.status === 'Closed') return [__('Closed'), 'green', 'status,=,Closed']; }, button: { show(doc) { return doc.status === 'Open'; }, get_label() { return __('Close'); }, action(doc) { frappe.call({method: 'myapp.api.close', args: {name: doc.name}}); } }, formatters: { priority(val) { return val === 'High' ? `<b>${val}</b>` : val; } }, onload(listview) { /* runs once */ }, refresh(listview) { /* runs on every refresh */ } };
Dialogs and Prompts
// Quick prompt frappe.prompt({label: 'Reason', fieldname: 'reason', fieldtype: 'Data'}, (values) => { console.log(values.reason); }, __('Enter Reason') ); // Full dialog let d = new frappe.ui.Dialog({ title: __('Enter Details'), fields: [ {label: 'Name', fieldname: 'name', fieldtype: 'Data', reqd: 1}, {label: 'Date', fieldname: 'date', fieldtype: 'Date'} ], size: 'small', primary_action_label: __('Submit'), primary_action(values) { d.hide(); /* use values */ } }); d.show(); // Progress indicator frappe.show_progress(__('Importing'), 45, 100, __('Please wait'));
Critical Rules
- ALWAYS call
after ANY child table modificationfrm.refresh_field('table') - NEVER assign
— ALWAYS usefrm.doc.field = valuefrm.set_value() - ALWAYS use
for every user-facing string__('text') - ALWAYS place
inset_query
— NEVER insetuprefresh - NEVER use
— it freezes the browserasync: false - ALWAYS check
before adding action buttonsfrm.is_new() - NEVER use direct jQuery selectors for field manipulation — use Frappe API
- NEVER store state in global variables — attach to
object insteadfrm - ALWAYS check
before using server call responsesr.message - ALWAYS use
insidefrappe.throw()
to block save — NEVERvalidate
in async handlersreturn false
See references/methods.md for complete API reference. See references/examples.md for real-world patterns. See references/anti-patterns.md for common mistakes.
Related Skills
— Implementation workflows and decision treesfrappe-impl-clientscripts
— Error handling and debugging patternsfrappe-errors-clientscripts
— Server-side methods called from client scriptsfrappe-syntax-whitelisted
— DocType field definitions referenced in scriptsfrappe-syntax-doctypes