Frappe_Claude_Skill_Package frappe-syntax-serverscripts
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-serverscripts" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-syntax-serverscripts && rm -rf "$T"
manifest:
skills/source/syntax/frappe-syntax-serverscripts/SKILL.mdsource content
Frappe Server Scripts — Complete Reference
Server Scripts are Python scripts managed via Setup > Server Script in the Frappe/ERPNext UI. They run inside a RestrictedPython sandbox.
CRITICAL: The Sandbox Rule
┌──────────────────────────────────────────────────────────────────┐ │ ALL import STATEMENTS ARE BLOCKED │ │ │ │ import json → ImportError: __import__ not found │ │ from datetime import * → ImportError: __import__ not found │ │ import frappe → ImportError (even frappe itself!) │ │ │ │ EVERYTHING you need is pre-loaded in the frappe namespace. │ │ NEVER write an import line. ALWAYS use frappe.utils.*, etc. │ └──────────────────────────────────────────────────────────────────┘
ALWAYS use the pre-loaded namespace instead of imports:
| Blocked import | Use instead |
|---|---|
| / |
| / |
| (already loaded) |
| / |
| Not available — restructure logic without regex |
/ | Not available — use a custom app instead |
Enabling Server Scripts
# v14: enabled by default # v15+: DISABLED by default — you MUST enable explicitly: bench set-config -g server_script_enabled 1 # Or set server_script_enabled: true in site_config.json
NEVER expect Server Scripts to work on Frappe Cloud shared benches — they require a private bench.
Script Types
| Type | Trigger | Key Variable |
|---|---|---|
| Document Event | Document lifecycle (save, submit, cancel) | |
| API | HTTP request to | |
| Scheduler Event | Cron schedule | (none) |
| Permission Query | Document list filtering | , |
Event Name Mapping (Document Events)
CRITICAL: The UI names differ from internal hook names:
| Server Script UI | Internal Hook | Fires When |
|---|---|---|
| Before Insert | | Before new doc saved to DB |
| After Insert | | After first DB insert |
| Before Validate | | Before framework validation |
| Before Save | | Before save (new + update) |
| After Save | | After successful save |
| Before Submit | | Before submit (docstatus 0→1) |
| After Submit | | After submit completes |
| Before Cancel | | Before cancel (docstatus 1→2) |
| After Cancel | | After cancel completes |
| Before Delete | | Before permanent delete |
| After Delete | | After permanent delete |
NEVER confuse "Before Save" with
before_save — the UI label "Before Save"
maps to the validate hook. The actual before_save hook runs AFTER validate.
Decision Tree: Server Script vs Document Controller
Need custom Python logic for a DocType? │ ├─► Can you install a custom Frappe app? │ ├─► YES: Use a Document Controller when you need: │ │ • import statements (any Python library) │ │ • File system access │ │ • Complex class inheritance │ │ • autoname / before_naming hooks │ │ • Unit-testable code │ │ │ └─► NO: Use a Server Script when: │ • You only have UI access (no bench CLI) │ • Logic is simple validation / field calculation │ • You need a quick API endpoint │ • You need dynamic permission filtering │ └─► Is logic > 50 lines or needs external libraries? ├─► YES → Document Controller in a custom app └─► NO → Server Script is fine
Quick Reference: Available in Sandbox
Pre-loaded Objects
doc # Current document (Document Event only) frappe # Core namespace — ALWAYS available frappe.db # Database operations frappe.utils # Date, number, string utilities frappe.session # Current session (user, csrf_token) frappe.form_dict # Request parameters (API scripts) frappe.response # Response object (API scripts) frappe.request # Werkzeug request object frappe.qb # Query Builder (v14+) json # Python json module (pre-loaded)
Core Methods
# Documents frappe.get_doc(doctype, name) # Fetch document frappe.new_doc(doctype) # Create new document frappe.get_cached_doc(doctype, name) # Cached fetch (read-only) frappe.get_last_doc(doctype) # Most recent document frappe.get_mapped_doc(...) # Map fields between DocTypes frappe.delete_doc(doctype, name) # Delete document frappe.rename_doc(doctype, old, new) # Rename document # Querying frappe.get_all(doctype, filters, fields, order_by, limit) # No permission check frappe.get_list(doctype, filters, fields, order_by, limit) # With permission check frappe.db.get_value(doctype, name, fieldname) frappe.db.get_single_value(doctype, fieldname) frappe.db.set_value(doctype, name, fieldname, value) frappe.db.exists(doctype, name_or_filters) frappe.db.count(doctype, filters) frappe.db.sql(query, values, as_dict) # ALWAYS parameterize! frappe.db.escape(value) # SQL escape frappe.db.commit() # ONLY in Scheduler scripts frappe.db.rollback() # ONLY in Scheduler scripts # Messaging frappe.throw(msg, exc, title) # Stop execution + show error frappe.msgprint(msg, title, indicator) # User notification frappe.log_error(message, title) # Error Log entry # HTTP (yes, these work in sandbox!) frappe.make_get_request(url, params, headers) frappe.make_post_request(url, data, headers) frappe.make_put_request(url, data, headers) # Email frappe.sendmail(recipients, sender, subject, message) # Utilities frappe.utils.today() # "2024-01-15" frappe.utils.now() # "2024-01-15 10:30:00" frappe.utils.now_datetime() # datetime object frappe.utils.add_days(date, n) # Date arithmetic frappe.utils.add_months(date, n) frappe.utils.date_diff(d1, d2) # Days between dates frappe.utils.flt(val) # Safe float (None → 0.0) frappe.utils.cint(val) # Safe int (None → 0) frappe.utils.cstr(val) # Safe string (None → "") frappe.parse_json(string) # JSON string → dict/list frappe.as_json(obj) # dict/list → JSON string frappe.render_template(template, ctx) # Jinja rendering frappe.get_url() # Site URL frappe.get_hooks(hook) # Read app hooks run_script(script_name, **kwargs) # Call another Server Script # Session / Permissions frappe.session.user # Current user email frappe.get_roles(user) # User's roles list frappe.has_permission(doctype, ptype, doc) frappe.get_fullname(user) # User's display name _("translatable string") # Translation function
Python Builtins Available
str, int, float, bool, list, dict, tuple, set # Types range, enumerate, zip, map, filter # Iteration sum, min, max, len, sorted, reversed # Aggregation isinstance, type, hasattr, getattr # Introspection all, any, abs, round, divmod # Math/logic print # → server log True, False, None # Constants
Python Builtins BLOCKED
open, file # No file I/O eval, exec, compile # No dynamic code execution __import__ # No imports (this is the root cause) globals, locals # No scope introspection
Syntax Per Script Type
Document Event
# Config: Reference DocType = Sales Invoice, Event = Before Save if doc.grand_total < 0: frappe.throw("Total MUST NOT be negative") doc.requires_approval = 1 if doc.grand_total > 10000 else 0
API
# Config: API Method = get_customer_orders, Allow Guest = No # Endpoint: /api/method/get_customer_orders customer = frappe.form_dict.get("customer") if not customer: frappe.throw("Parameter 'customer' is required") orders = frappe.get_all("Sales Order", filters={"customer": customer, "docstatus": 1}, fields=["name", "grand_total", "status"], order_by="creation desc", limit=20 ) frappe.response["message"] = {"orders": orders, "count": len(orders)}
Scheduler Event
# Config: Event Frequency = Cron, Cron Format = 0 9 * * * overdue = frappe.get_all("Sales Invoice", filters={"status": "Unpaid", "due_date": ["<", frappe.utils.today()], "docstatus": 1}, fields=["name", "customer", "grand_total"] ) for inv in overdue: frappe.log_error(f"Overdue: {inv.name} ({inv.customer})", "Invoice Reminder") frappe.db.commit() # ALWAYS commit in Scheduler scripts
Permission Query
# Config: Reference DocType = Sales Invoice # Variables available: user, conditions roles = frappe.get_roles(user) if "System Manager" in roles: conditions = "" elif "Sales User" in roles: conditions = f"`tabSales Invoice`.owner = {frappe.db.escape(user)}" else: conditions = "1=0"
Version Differences
| Feature | v14 | v15 | v16 |
|---|---|---|---|
| Server Scripts enabled | By default | Disabled by default | Disabled by default |
| Enable command | Not needed | | Same as v15 |
(Query Builder) | Available | Available | Available |
for libraries | v13+ | Available | Available |
| Available | Available | Available |
| Frappe Cloud shared bench | Supported | NOT supported | NOT supported |
Top 5 Rules
- NEVER write
— everything is in theimport
namespacefrappe - NEVER call
inside a Before Save script — causes infinite loopdoc.save() - NEVER call
in Document Event scripts — framework handles itfrappe.db.commit() - ALWAYS call
at the end of Scheduler scriptsfrappe.db.commit() - ALWAYS use parameterized queries:
with dict, NEVER f-strings in SQL%(var)s
References
- references/methods.md — Complete sandbox API reference
- references/events.md — Document lifecycle and execution order
- references/examples.md — Working examples per script type
- references/anti-patterns.md — Sandbox violations and common mistakes
- references/syntax.md — Quick syntax cheat sheet
- references/patterns.md — Common patterns (validation, auto-fill, API)
- references/hooks.md — Server Scripts vs hooks.py interaction
Cross-References
- frappe-syntax-api — Frappe REST API and whitelisted methods
- frappe-syntax-doctype — DocType field types and schema
- frappe-core-database — frappe.db deep dive
- frappe-core-permissions — Permission system architecture
- frappe-errors-common — Error handling patterns