$npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-impl-serverscriptsUse when implementing server-side features via Setup > Server Script: document validation, auto-fill, API endpoints, scheduled tasks, permission queries. Covers sandbox-safe coding, script type selection, testing, migration to controllers. Keywords: how to implement server script
| 1 | # Server Scripts — Implementation Workflows |
| 2 | |
| 3 | Step-by-step workflows for building server-side features without a custom app. For exact syntax, see `frappe-syntax-serverscripts`. |
| 4 | |
| 5 | **Version**: v14/v15/v16 | **v15+ Note**: Server Scripts disabled by default — enable with `bench set-config server_script_enabled true` |
| 6 | |
| 7 | ## CRITICAL: Sandbox Limitations |
| 8 | |
| 9 | ``` |
| 10 | ALL IMPORTS BLOCKED — RestrictedPython sandbox |
| 11 | import json → ImportError: __import__ not found |
| 12 | from frappe.utils → ImportError |
| 13 | import requests → ImportError |
| 14 | |
| 15 | SOLUTION: Use pre-loaded namespace: |
| 16 | frappe.utils.nowdate() frappe.utils.flt() |
| 17 | frappe.parse_json(data) json.loads() (json IS available) |
| 18 | frappe.as_json(obj) json.dumps() |
| 19 | frappe.make_get_request(url) (replaces requests.get) |
| 20 | ``` |
| 21 | |
| 22 | **Rule**: If you need `import` statements beyond `json`, ALWAYS use a Controller instead. |
| 23 | |
| 24 | ## Workflow 1: Create a Server Script |
| 25 | |
| 26 | 1. Enable server scripts: `bench set-config server_script_enabled true` |
| 27 | 2. Navigate to **Setup > Server Script** (or awesomebar: "New Server Script") |
| 28 | 3. Select **Script Type** (see decision tree below) |
| 29 | 4. Configure type-specific settings (DocType, event, API method, cron) |
| 30 | 5. Write script in the editor |
| 31 | 6. Save — script is active immediately |
| 32 | 7. Test by triggering the configured event |
| 33 | 8. Use "Compare Versions" button to diff changes |
| 34 | |
| 35 | ## Workflow 2: Choose the Script Type |
| 36 | |
| 37 | ``` |
| 38 | WHAT DO YOU NEED? |
| 39 | │ |
| 40 | ├── React to document save/submit/cancel? |
| 41 | │ └── Document Event |
| 42 | │ └── Select DocType + Event (Before Save, After Save, etc.) |
| 43 | │ |
| 44 | ├── Create a REST API endpoint? |
| 45 | │ └── API |
| 46 | │ └── Set method name + guest access setting |
| 47 | │ └── Endpoint: /api/method/{method_name} |
| 48 | │ |
| 49 | ├── Run task on schedule (daily/hourly/cron)? |
| 50 | │ └── Scheduler Event |
| 51 | │ └── Set cron pattern or frequency |
| 52 | │ |
| 53 | └── Filter list views per user/role? |
| 54 | └── Permission Query |
| 55 | └── Select DocType — set `conditions` variable |
| 56 | ``` |
| 57 | |
| 58 | > See [references/decision-tree.md](references/decision-tree.md) for complete decision tree. |
| 59 | |
| 60 | ## Workflow 3: Document Event: Validation |
| 61 | |
| 62 | **Goal**: Validate Sales Order before save. |
| 63 | |
| 64 | **Step 1**: Choose event — "Before Save" maps to `validate` hook. |
| 65 | |
| 66 | **Step 2**: Write sandbox-safe script: |
| 67 | |
| 68 | ```python |
| 69 | # Type: Document Event | Event: Before Save | DocType: Sales Order |
| 70 | |
| 71 | errors = [] |
| 72 | |
| 73 | if not doc.customer: |
| 74 | errors.append("Customer is required") |
| 75 | |
| 76 | if doc.delivery_date and doc.delivery_date < frappe.utils.today(): |
| 77 | errors.append("Delivery date cannot be in the past") |
| 78 | |
| 79 | for item in doc.items: |
| 80 | if item.qty <= 0: |
| 81 | errors.append(f"Row {item.idx}: Quantity must be positive") |
| 82 | |
| 83 | if errors: |
| 84 | frappe.throw("<br>".join(errors), title="Validation Error") |
| 85 | ``` |
| 86 | |
| 87 | **Rules**: |
| 88 | - ALWAYS collect errors and throw once (better UX than multiple throws) |
| 89 | - NEVER call `doc.save()` in Before Save — framework handles it |
| 90 | - ALWAYS use `frappe.throw()` — `msgprint` does NOT stop save |
| 91 | |
| 92 | ## Workflow 4: Document Event: Auto-Calculate |
| 93 | |
| 94 | **Goal**: Auto-calculate totals and set derived fields. |
| 95 | |
| 96 | ```python |
| 97 | # Type: Document Event | Event: Before Save | DocType: Purchase Order |
| 98 | |
| 99 | doc.total_qty = sum(item.qty or 0 for item in doc.items) |
| 100 | doc.total_amount = sum((item.qty or 0) * (item.rate or 0) for item in doc.items) |
| 101 | |
| 102 | if doc.total_amount > 50000: |
| 103 | doc.requires_approval = 1 |
| 104 | doc.approval_status = "Pending" |
| 105 | |
| 106 | if doc.supplier and not doc.supplier_name: |
| 107 | doc.supplier_name = frappe.db.get_value("Supplier", doc.supplier, "supplier_name") |
| 108 | ``` |
| 109 | |
| 110 | **Rule**: ALWAYS modify `doc` fields directly in Before Save — they are automatically persisted. |
| 111 | |
| 112 | ## Workflow 5: Document Event: Create Related Document |
| 113 | |
| 114 | **Goal**: Create a ToDo when a new Lead is inserted. |
| 115 | |
| 116 | ```python |
| 117 | # Type: Document Event | Event: After Insert | DocType: Lead |
| 118 | |
| 119 | frappe.get_doc({ |
| 120 | "doctype": "ToDo", |
| 121 | "allocated_to": doc.lead_owner or doc.owner, |
| 122 | "reference_type": "Lead", |
| 123 | "reference_name": doc.name, |
| 124 | "description": f"Follow up with new lead: {doc.lead_name}", |
| 125 | "date": frappe.utils.add_days(frappe.utils.today(), 1), |
| 126 | "priority": "High" if doc.status == "Hot" else "Medium" |
| 127 | }).insert(ignore_permissions=True) |
| 128 | ``` |
| 129 | |
| 130 | **Rules**: |
| 131 | - ALWAYS use After Insert or After Save for creating related docs |
| 132 | - NEVER create documents in Before Save — `doc.name` may not exist yet |
| 133 | - ALWAYS use |