5,438,331 AI resources indexedWorking toward the world’s largest AI resource index.

PDF

Frappe Client Script Generator

Generate JavaScript client-side form scripts for Frappe DocTypes. Use when creating form customizations, field validations, custom buttons, or client-side logic for Frappe/ERPNext forms.

Installable skillMITBy vyogotechUpdated Oct 30, 2026
INTENDED FORClaude CodeInstall in a listed agent.

instructions.md

---
name: frappe-client-script-generator-8622a7d0d7d0
description: Generate JavaScript client-side form scripts for Frappe DocTypes. Use when creating form customizations, field validations, custom buttons, or client-side logic for Frappe/ERPNext forms.
---

# Frappe Client Script Generator

Generate production-ready JavaScript form scripts for Frappe DocTypes with proper event handlers, validations, and custom functionality.

## When to Use This Skill

Claude should invoke this skill when:
- User wants to add client-side form customizations
- User needs field validations or calculations
- User requests custom buttons or actions on forms
- User wants to filter or fetch data dynamically
- User mentions form scripts, client scripts, or JavaScript for DocTypes
- User wants to show/hide fields conditionally
- User needs to set field values based on other fields

## Capabilities

### 1. Form Event Handlers

Generate event handlers for DocType forms following Frappe patterns from core apps.

**Refresh Event** (runs when form loads):
```javascript
// Pattern from: erpnext/accounts/doctype/sales_invoice/sales_invoice.js
frappe.ui.form.on('Sales Invoice', {
    refresh: function(frm) {
        // Add custom buttons
        if (frm.doc.docstatus === 1) {
            frm.add_custom_button(__('Create Payment'), function() {
                frm.events.make_payment_entry(frm);
            });
        }

        // Set field properties
        frm.set_df_property('customer', 'reqd', 1);

        // Show/hide fields
        frm.toggle_display('discount_section', frm.doc.apply_discount);
    }
});
```

**Setup Event** (runs once when form is created):
```javascript
// Pattern from: erpnext/stock/doctype/stock_entry/stock_entry.js
frappe.ui.form.on('Stock Entry', {
    setup: function(frm) {
        // Set query filters for Link fields
        frm.set_query('item_code', 'items', function() {
            return {
                filters: {
                    'is_stock_item': 1,
                    'has_serial_no': 0
                }
            };
        });
    }
});
```

**Onload Event** (runs on form load, before refresh):
```javascript
// Pattern from: erpnext/accounts/doctype/payment_entry/payment_entry.js
frappe.ui.form.on('Payment Entry', {
    onload: function(frm) {
        if (frm.is_new()) {
            frm.set_value('posting_date', frappe.datetime.get_today());
        }
    }
});
```

### 2. Field Change Handlers

**Single Field Change**:
```javascript
// Pattern from: erpnext/selling/doctype/sales_order/sales_order.js
frappe.ui.form.on('Sales Order', {
    customer: function(frm) {
        if (frm.doc.customer) {
            // Fetch customer details
            frappe.db.get_value('Customer', frm.doc.customer, 'customer_group')
                .then(r => {
                    if (r.message) {
                        frm.set_value('customer_group', r.message.customer_group);
                    }
                });
        }
    }
});
```

**Multiple Field Dependencies**:
```javascript
// Pattern from: erpnext/accounts/doctype/sales_invoice/sales_invoice.js
frappe.ui.form.on('Sales Invoice', {
    customer: function(frm) {
        frm.events.set_dynamic_field_label(frm);
    },
    currency: function(frm) {
        frm.events.set_dynamic_field_label(frm);
    },
    set_dynamic_field_label: function(frm) {
        if (frm.doc.currency) {
            frm.set_currency_labels(['total', 'grand_total'], frm.doc.currency);
        }
    }
});
```

### 3. Child Table (Grid) Events

**Child Table Row Events**:
```javascript
// Pattern from: erpnext/accounts/doctype/sales_invoice/sales_invoice_item.js
frappe.ui.form.on('Sales Invoice Item', {
    item_code: function(frm, cdt, cdn) {
        let row = locals[cdt][cdn];
        if (row.item_code) {
            frappe.call({
                method: 'erpnext.stock.get_item_details.get_item_details',
                args: {
                    item_code: row.item_code,
                    company: frm.doc.company
                },
                callback: function(r) {
                    if (r.message) {
                        frappe.model.set_value(cdt, cdn, 'rate', r.message.price_list_rate);
                        frappe.model.set_value(cdt, cdn, 'uom', r.message.stock_uom);
                    }
                }
            });
        }
    },

    qty: function(frm, cdt, cdn) {
        frm.events.calculate_totals(frm, cdt, cdn);
    },

    rate: function(frm, cdt, cdn) {
        frm.events.calculate_totals(frm, cdt, cdn);
    }
});
```

**Grid Operations**:
```javascript
// Pattern from: erpnext/stock/doctype/stock_entry/stock_entry.js
frappe.ui.form.on('Stock Entry', {
    items_add: function(frm, cdt, cdn) {
        let row = locals[cdt][cdn];
        row.s_warehouse = frm.doc.from_warehouse;
        row.t_warehouse = frm.doc.to_warehouse;
    },

    items_remove: function(frm) {
        frm.events.calculate_totals(frm);
    }
});
```

### 4. Custom Buttons and Actions

**Standard Button Patterns**:
```javascript
// Pattern from: erpnext/accounts/doctype/sales_invoice/sales_invoice.js
frappe.ui.form.on('Sales Invoice', {
    refresh: function(frm) {
        if (frm.doc.docstatus === 1 && frm.doc.outstanding_amount > 0) {
            frm.add_custom_button(__('Payment'), function() {
                frm.events.make_payment_entry(frm);
            }, __('Create'));
        }

        // Add custom button in toolbar
        if (frm.doc.docstatus === 0) {
            frm.add_custom_button(__('Get Items from Sales Order'), function() {
                erpnext.utils.map_current_doc({
                    method: 'erpnext.selling.doctype.sales_order.sales_order.make_sales_invoice',
                    source_doctype: 'Sales Order',
                    target: frm,
                    setters: {
                        customer: frm.doc.customer || undefined
                    },
                    get_query_filters: {
                        docstatus: 1,
                        status: ['not in', ['Closed', 'On Hold']]
                    }
                });
            });
        }
    },

    make_payment_entry: function(frm) {
        return frappe.call({
            method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry',
            args: {
                dt: frm.doc.doctype,
                dn: frm.doc.name
            },
            callback: function(r) {
                let doc = frappe.model.sync(r.message);
                frappe.set_route('Form', doc[0].doctype, doc[0].name);
            }
        });
    }
});
```

### 5. Data Fetching and API Calls

**Fetch from Database**:
```javascript
// Pattern from: erpnext/stock/doctype/item/item.js
frappe.ui.form.on('Item', {
    item_group: function(frm) {
        if (frm.doc.item_group) {
            frappe.db.get_value('Item Group', frm.doc.item_group, 'default_warehouse')
                .then(r => {
                    if (r.message && r.message.default_warehouse) {
                        frm.set_value('default_warehouse', r.message.default_warehouse);
                    }
                });
        }
    }
});
```

**Server Method Calls**:
```javascript
// Pattern from: erpnext/accounts/doctype/payment_entry/payment_entry.js
frappe.ui.form.on('Payment Entry', {
    party: function(frm) {
        if (frm.doc.party_type && frm.doc.party) {
            frappe.call({
                method: 'erpnext.accounts.party.get_party_details',
                args: {
                    party: frm.doc.party,
                    party_type: frm.doc.party_type,
                    company: frm.doc.company
                },
                callback: function(r) {
                    if (r.message) {
                        frm.set_value('party_name', r.message.party_name);
                        frm.set_value('party_account', r.message.party_account);
                    }
                }
            });
        }
    }
});
```

### 6. Form Validations

**Before Save Validation**:
```javascript
// Pattern from: erpnext/accounts/doctype/sales_invoice/sales_invoice.js
frappe.ui.form.on('Sales Invoice', {
    validate: function(frm) {
        // Validate posting date
        if (frm.doc.posting_date > frappe.datetime.get_today()) {
            frappe.throw(__('Posting Date cannot be future date'));
        }

        // Validate items
        if (!frm.doc.items || frm.doc.items.length === 0) {
            frappe.throw(__('Please add at least one item'));
        }

        // Validate total
        if (frm.doc.grand_total <= 0) {
            frappe.throw(__('Grand Total must be greater than 0'));
        }
    }
});
```

**Before Submit Validation**:
```javascript
// Pattern from: erpnext/stock/doctype/stock_entry/stock_entry.js
frappe.ui.form.on('Stock Entry', {
    before_submit: function(frm) {
        let has_qty = false;
        frm.doc.items.forEach(function(item) {
            if (item.qty > 0) {
                has_qty = true;
            }
        });

        if (!has_qty) {
            frappe.throw(__('Please enter quantity for at least one item'));
        }
    }
});
```

### 7. Conditional Field Display

**Show/Hide Fields**:
```javascript
// Pattern from: erpnext/accounts/doctype/payment_entry/payment_entry.js
frappe.ui.form.on('Payment Entry', {
    payment_type: function(frm) {
        frm.events.toggle_fields(frm);
    },

    toggle_fields: function(frm) {
        let is_receive = (frm.doc.payment_type === 'Receive');
        let is_pay = (frm.doc.payment_type === 'Pay');

        frm.toggle_display('paid_from', is_pay);
        frm.toggle_display('paid_to', is_receive);
        frm.toggle_reqd('paid_from', is_pay);
        frm.toggle_reqd('paid_to', is_receive);
    }
});
```

**Field Property Changes**:
```javascript
// Pattern from: erpnext/selling/doctype/sales_order/sales_order.js
frappe.ui.form.on('Sales Order', {
    refresh: function(frm) {
        // Make field read-only based on condition
        frm.set_df_property('customer', 'read_only', frm.doc.docstatus === 1);

        // Change field label
        frm.set_df_property('delivery_date', 'label',
            frm.doc.order_type === 'Sales' ? __('Delivery Date') : __('Delivery By'));

        // Set field as mandatory
        frm.toggle_reqd('delivery_date', frm.doc.order_type === 'Sales');
    }
});
```

### 8. Calculations and Totals

**Calculate Child Table Totals**:
```javascript
// Pattern from: erpnext/accounts/doctype/sales_invoice/sales_invoice.js
frappe.ui.form.on('Sales Invoice', {
    calculate_totals: function(frm) {
        let total = 0;
        frm.doc.items.forEach(function(item) {
            item.amount = flt(item.qty) * flt(item.rate);
            total += item.amount;
        });
        frm.set_value('total', total);

        // Calculate tax and grand total
        let tax_amount = flt(total * frm.doc.tax_rate / 100);
        frm.set_value('total_taxes_and_charges', tax_amount);
        frm.set_value('grand_total', total + tax_amount);
    }
});

frappe.ui.form.on('Sales Invoice Item', {
    qty: function(frm, cdt, cdn) {
        let item = locals[cdt][cdn];
        frappe.model.set_value(cdt, cdn, 'amount',
            flt(item.qty) * flt(item.rate));
        frm.events.calculate_totals(frm);
    },

    rate: function(frm, cdt, cdn) {
        let item = locals[cdt][cdn];
        frappe.model.set_value(cdt, cdn, 'amount',
            flt(item.qty) * flt(item.rate));
        frm.events.calculate_totals(frm);
    }
});
```

### 9. Link Field Filters (set_query)

**Filter Link Field Options**:
```javascript
// Pattern from: erpnext/stock/doctype/stock_entry/stock_entry.js
frappe.ui.form.on('Stock Entry', {
    setup: function(frm) {
        // Filter items based on item group
        frm.set_query('item_code', 'items', function(doc, cdt, cdn) {
            return {
                filters: {
                    'item_group': ['in', ['Raw Material', 'Sub Assemblies']],
                    'is_stock_item': 1
                }
            };
        });

        // Dynamic filters based on doc values
        frm.set_query('warehouse', function() {
            return {
                filters: {
                    'company': frm.doc.company,
                    'is_group': 0
                }
            };
        });
    }
});
```

**Complex Query Filters**:
```javascript
// Pattern from: erpnext/accounts/doctype/payment_entry/payment_entry.js
frappe.ui.form.on('Payment Entry', {
    setup: function(frm) {
        frm.set_query('party', function() {
            let party_type = frm.doc.party_type;
            if (party_type === 'Customer') {
                return {query: 'erpnext.controllers.queries.customer_query'};
            } else if (party_type === 'Supplier') {
                return {query: 'erpnext.controllers.queries.supplier_query'};
            }
        });

        frm.set_query('reference_doctype', 'references', function() {
            let doctypes = [];
            if (frm.doc.party_type === 'Customer') {
                doctypes = ['Sales Invoice', 'Sales Order'];
            } else if (frm.doc.party_type === 'Supplier') {
                doctypes = ['Purchase Invoice', 'Purchase Order'];
            }
            return {
                filters: {
                    'name': ['in', doctypes]
                }
            };
        });
    }
});
```

### 10. Dialogs and Prompts

**Create Custom Dialog**:
```javascript
// Pattern from: erpnext/stock/doctype/stock_entry/stock_entry.js
frappe.ui.form.on('Stock Entry', {
    get_items: function(frm) {
        let dialog = new frappe.ui.Dialog({
            title: __('Get Items'),
            fields: [
                {
                    fieldtype: 'Link',
                    label: __('Warehouse'),
                    fieldname: 'warehouse',
                    options: 'Warehouse',
                    reqd: 1,
                    get_query: function() {
                        return {
                            filters: {
                                'company': frm.doc.company
                            }
                        };
                    }
                },
                {
                    fieldtype: 'Link',
                    label: __('Item Group'),
                    fieldname: 'item_group',
                    options: 'Item Group'
                }
            ],
            primary_action_label: __('Get Items'),
            primary_action: function(values) {
                frappe.call({
                    method: 'get_items',
                    doc: frm.doc,
                    args: values,
                    callback: function(r) {
                        dialog.hide();
                        frm.refresh_field('items');
                    }
                });
            }
        });
        dialog.show();
    }
});
```

## References

### Frappe Core Client Script Examples (Primary Reference)

**Learn from Frappe Framework:**
- Frappe Form Scripts: https://github.com/frappe/frappe/tree/develop/frappe/desk/doctype
  - `form/form.js` - Core form functionality
  - `todo/todo.js` - Simple form script example
- Frappe UI Components: https://github.com/frappe/frappe/tree/develop/frappe/public/js/frappe/ui

**ERPNext Client Script Examples:**
- Sales Invoice: https://github.com/frappe/erpnext/blob/develop/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
- Purchase Order: https://github.com/frappe/erpnext/blob/develop/erpnext/buying/doctype/purchase_order/purchase_order.js
- Stock Entry: https://github.com/frappe/erpnext/blob/develop/erpnext/stock/doctype/stock_entry/stock_entry.js
- Payment Entry: https://github.com/frappe/erpnext/blob/develop/erpnext/accounts/doctype/payment_entry/payment_entry.js
- Item: https://github.com/frappe/erpnext/blob/develop/erpnext/stock/doctype/item/item.js

### Official Documentation (Secondary Reference)

- Form Scripts: https://frappeframework.com/docs/user/en/desk/scripting/form-scripts
- Client API: https://frappeframework.com/docs/user/en/api/form
- frappe.ui.form.on: https://frappeframework.com/docs/user/en/api/form#frappeuiformon

## Best Practices

1. **Event Handler Organization**: Group related handlers together
2. **Reusable Functions**: Extract common logic into reusable methods
3. **Null Checks**: Always validate data before using it
4. **Async Operations**: Use callbacks for database and API calls
5. **User Feedback**: Use `frappe.show_alert()` for success/error messages
6. **Performance**: Debounce expensive operations in change handlers
7. **Translation**: Use `__()` for translatable strings
8. **Error Handling**: Wrap risky operations in try-catch
9. **Child Tables**: Use `locals[cdt][cdn]` to access child table rows
10. **Field Updates**: Use `frappe.model.set_value()` for child table fields

## File Output Format

Generated client scripts should be saved at:
```
apps/<app_name>/<module>/doctype/<doctype_name>/<doctype_name>.js
```

Always include:
- Clear comments explaining functionality
- Proper indentation (4 spaces or tab as per project)
- Event handler grouping
- Error handling where appropriate
- Translation wrappers for user-facing strings

## Common Patterns Summary

- **refresh**: Add buttons, set field properties
- **setup**: One-time setup, set query filters
- **onload**: Initialize values for new docs
- **validate**: Pre-save validations
- **before_submit**: Pre-submission checks
- **field_name**: Handle field changes
- **child_table_field**: Handle child table field changes
- **items_add/remove**: Handle row additions/removals

## Decision Tree & Reference

Source skill: **`frappe-syntax-clientscripts`** (Frappe Claude Skill Package workspace). Summarizes event timing, API cheatsheet, handler choice, and hard rules—supplements the ERPNext-pattern examples above, does not replace them.

### Quick Reference (syntax cheatsheet)

| Action | Code |
|--------|------|
| Set value | `frm.set_value('field', value)` or `frm.set_value({ ... })` |
| Get value | `frm.doc.fieldname` |
| Hide field | `frm.toggle_display('field', false)` |
| Mandatory | `frm.toggle_reqd('field', true)` |
| Read-only | `frm.toggle_enable('field', false)` |
| DF property | `frm.set_df_property('field', 'options', [...])` |
| Filter Link | `frm.set_query('field', () => ({ filters: {} }))` |
| Server | `frappe.call({ method, args })` |
| Doc method | `frm.call('method_name', { args })` |
| Block save | `frappe.throw(__('...'))` in `validate` |
| Child row | `frm.add_child('table', { ... }); frm.refresh_field('table')` |
| Alert | `frappe.show_alert({ message: __('...'), indicator: 'green' })` |

### Event execution order (condensed)

- **Load**: `setup` → `onload` → `refresh` → `onload_post_render`
- **Save**: `validate` → `before_save` → *[server]* → `after_save` → `refresh`
- **Submit**: `validate` → `before_submit` → *[server]* → `on_submit` → `refresh`
- **Cancel**: `before_cancel` → *[server]* → `after_cancel` → `refresh`
- **Reload / frm.refresh**: `before_load` → `onload` → `refresh` → `onload_post_render`

### Form event reference table

| Event | When | Params | Typical use |
|-------|------|--------|--------------|
| `setup` | Once per form instance | `(frm)` | `set_query`, one-time config |
| `onload` | Loaded, before render | `(frm)` | Defaults, preprocessing |
| `refresh` | After load/reload/save | `(frm)` | Buttons, visibility |
| `onload_post_render` | DOM ready | `(frm)` | DOM-dependent work |
| `validate` | Before save/submit | `(frm)` | Block with `frappe.throw()` |
| `before_save` | After validate | `(frm)` | Last-minute values |
| `after_save` | After save | `(frm)` | Follow-up UI/notifications |
| `before_submit` | Before submit | `(frm)` | Pre-submit checks |
| `on_submit` | After submit | `(frm)` | Post-submit actions |
| `before_cancel` / `after_cancel` | Around cancel | `(frm)` | Cancel hooks |
| `before_workflow_action` / `after_workflow_action` | Workflow | `(frm)` | Workflow hooks |
| `timeline_refresh` | Timeline rendered | `(frm)` | Timeline customization |

**Field change handlers:** use exact `fieldname` as event key—they run on UI change, `frm.set_value()`, and child `frappe.model.set_value()`; they do **not** run after raw `frm.doc.field = …` assignments.

**Child DocType handlers:** register on the **child** DocType (`frappe.ui.form.on('Sales Order Item', { … })`). Grid lifecycle: `{table}_add`, `{table}_remove`, `{table}_move`, `{table}_before_remove` (v14+).

### setup vs refresh

| Aspect | `setup` | `refresh` |
|--------|---------|-----------|
| Frequency | Once | Every load/reload/save |
| Use for | `set_query`, formatters | Buttons, dynamic UI |

### Event decision tree (syntax)

```
What do you need?
├── One-time setup (queries, formatters)?
│   └── ALWAYS use setup — once per form instance
├── Show/hide, buttons, refresh UI each load?
│   └── ALWAYS use refresh
├── Validate before save?
│   └── ALWAYS use validate — frappe.throw() to block
├── Change values immediately before save?
│   └── Use before_save
├── After persisted save?
│   └── Use after_save
├── React to field change?
│   └── Use fieldname handler
├── Workflow transition?
│   └── before_workflow_action / after_workflow_action
└── After full DOM render?
    └── onload_post_render — NEVER manipulate fields via bare jQuery
```

### ALWAYS / NEVER (syntax-critical)

1. ALWAYS call `frm.refresh_field('table')` after child table mutations (`add_child`, `clear_table`, bulk row edits) so the grid stays in sync.
2. NEVER assign `frm.doc.field = value` on the parent form—use `frm.set_value()` so triggers, dirty state, and UI update run.
3. ALWAYS wrap user-facing strings in `__()`.
4. ALWAYS define `set_query` in `setup`, not `refresh` (callbacks still read fresh `frm.doc`).
5. NEVER use `async: false` on `frappe.call` (locks the UI).
6. ALWAYS guard custom action buttons (`frm.is_new()`, `docstatus`, business fields).
7. NEVER drive field visibility or enablement by raw jQuery—use `frm.toggle_display`, `frm.toggle_enable`, `frm.set_df_property`.
8. NEVER keep form state in global variables—stash on `frm` (e.g. `frm._cache_key`).
9. ALWAYS check `r.message` before using `frappe.call` results unless you rely on upstream guarantees.
10. In `validate`, ALWAYS block saves with `frappe.throw()`; with async/server checks use `async validate` + `await`—NEVER rely on callbacks that finish after validate returns.

**Async validation:** NEVER fire an un-awaited `frappe.call` inside `validate`—saving continues before your callback resolves; use `async/await` instead.

Discuss this resource

Platform guidance

Imported from the listed AgentSkill.sh category. Complete supplied package files and notices are retained. Required external tools and credentials are not installed. No bundled code was executed during import. Individual agent task behavior has not been tested.