Skill125 repo starsupdated 2mo ago
frappe-impl-serverscripts
>
Install in Claude Code
Copygit clone --depth 1 https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package /tmp/frappe-impl-serverscripts && cp -r /tmp/frappe-impl-serverscripts/skills/source/impl/frappe-impl-serverscripts ~/.claude/skills/frappe-impl-serverscriptsThen start a new Claude Code session; the skill loads automatically.
Definition
SKILL.md
# Server Scripts — Implementation Workflows
Step-by-step workflows for building server-side features without a custom app. For exact syntax, see `frappe-syntax-serverscripts`.
**Version**: v14/v15/v16 | **v15+ Note**: Server Scripts disabled by default — enable with `bench set-config server_script_enabled true`
## CRITICAL: Sandbox Limitations
```
ALL IMPORTS BLOCKED — RestrictedPython sandbox
import json → ImportError: __import__ not found
from frappe.utils → ImportError
import requests → ImportError
SOLUTION: Use pre-loaded namespace:
frappe.utils.nowdate() frappe.utils.flt()
frappe.parse_json(data) json.loads() (json IS available)
frappe.as_json(obj) json.dumps()
frappe.make_get_request(url) (replaces requests.get)
```
**Rule**: If you need `import` statements beyond `json`, ALWAYS use a Controller instead.
## Workflow 1: Create a Server Script
1. Enable server scripts: `bench set-config server_script_enabled true`
2. Navigate to **Setup > Server Script** (or awesomebar: "New Server Script")
3. Select **Script Type** (see decision tree below)
4. Configure type-specific settings (DocType, event, API method, cron)
5. Write script in the editor
6. Save — script is active immediately
7. Test by triggering the configured event
8. Use "Compare Versions" button to diff changes
## Workflow 2: Choose the Script Type
```
WHAT DO YOU NEED?
│
├── React to document save/submit/cancel?
│ └── Document Event
│ └── Select DocType + Event (Before Save, After Save, etc.)
│
├── Create a REST API endpoint?
│ └── API
│ └── Set method name + guest access setting
│ └── Endpoint: /api/method/{method_name}
│
├── Run task on schedule (daily/hourly/cron)?
│ └── Scheduler Event
│ └── Set cron pattern or frequency
│
└── Filter list views per user/role?
└── Permission Query
└── Select DocType — set `conditions` variable
```
> See [references/decision-tree.md](references/decision-tree.md) for complete decision tree.
## Workflow 3: Document Event: Validation
**Goal**: Validate Sales Order before save.
**Step 1**: Choose event — "Before Save" maps to `validate` hook.
**Step 2**: Write sandbox-safe script:
```python
# Type: Document Event | Event: Before Save | DocType: Sales Order
errors = []
if not doc.customer:
errors.append("Customer is required")
if doc.delivery_date and doc.delivery_date < frappe.utils.today():
errors.append("Delivery date cannot be in the past")
for item in doc.items:
if item.qty <= 0:
errors.append(f"Row {item.idx}: Quantity must be positive")
if errors:
frappe.throw("<br>".join(errors), title="Validation Error")
```
**Rules**:
- ALWAYS collect errors and throw once (better UX than multiple throws)
- NEVER call `doc.save()` in Before Save — framework handles it
- ALWAYS use `frappe.throw()` — `msgprint` does NOT stop save
## Workflow 4: Document Event: Auto-Calculate
**Goal**: Auto-calculate totals and set derived fields.
```python
# Type: Document Event | Event: Before Save | DocType: Purchase Order
doc.total_qty = sum(item.qty or 0 for item in doc.items)
doc.total_amount = sum((item.qty or 0) * (item.rate or 0) for item in doc.items)
if doc.total_amount > 50000:
doc.requires_approval = 1
doc.approval_status = "Pending"
if doc.supplier and not doc.supplier_name:
doc.supplier_name = frappe.db.get_value("Supplier", doc.supplier, "supplier_name")
```
**Rule**: ALWAYS modify `doc` fields directly in Before Save — they are automatically persisted.
## Workflow 5: Document Event: Create Related Document
**Goal**: Create a ToDo when a new Lead is inserted.
```python
# Type: Document Event | Event: After Insert | DocType: Lead
frappe.get_doc({
"doctype": "ToDo",
"allocated_to": doc.lead_owner or doc.owner,
"reference_type": "Lead",
"reference_name": doc.name,
"description": f"Follow up with new lead: {doc.lead_name}",
"date": frappe.utils.add_days(frappe.utils.today(), 1),
"priority": "High" if doc.status == "Hot" else "Medium"
}).insert(ignore_permissions=True)
```
**Rules**:
- ALWAYS use After Insert or After Save for creating related docs
- NEVER create documents in Before Save — `doc.name` may not exist yet
- ALWAYS use `ignore_permissions=True` for system-generated documents
## Workflow 6: API Endpoint
**Goal**: Create authenticated REST API returning customer data.
```python
# Type: API | Method: get_customer_dashboard | Allow Guest: No
# Endpoint: /api/method/get_customer_dashboard
customer = frappe.form_dict.get("customer")
if not customer:
frappe.throw("Parameter 'customer' is required")
# ALWAYS check permissions
if not frappe.has_permission("Customer", "read", customer):
frappe.throw("Access denied", frappe.PermissionError)
orders = frappe.db.count("Sales Order", {"customer": customer, "docstatus": 1})
revenue = frappe.db.get_value("Sales Invoice",
filters={"customer": customer, "docstatus": 1},
fieldname="sum(grand_total)") or 0
frappe.response["message"] = {
"customer": customer,
"total_orders": orders,
"total_revenue": revenue
}
```
**Rules**:
- ALWAYS validate input parameters
- ALWAYS check permissions (even with Allow Guest: No)
- ALWAYS cap query limits: `min(frappe.utils.cint(limit), 100)`
- NEVER expose full documents — return only needed fields
## Workflow 7: Scheduler Event
**Goal**: Daily reminder for overdue invoices.
```python
# Type: Scheduler Event | Cron: 0 9 * * * (daily at 9:00)
BATCH_SIZE = 50
today = frappe.utils.today()
overdue = frappe.get_all("Sales Invoice",
filters={
"status": "Unpaid",
"due_date": ["<", today],
"docstatus": 1
},
fields=["name", "customer", "owner", "due_date", "grand_total"],
limit=BATCH_SIZE
)
for inv in overdue:
days = frappe.utils.date_diff(today, inv.due_date)
if not frappe.db.exists("ToDo", {
"reference_type": "Sales Invoice",
"reference_name": in