Frappe_Claude_Skill_Package frappe-errors-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/errors/frappe-errors-serverscripts" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-errors-serverscripts && rm -rf "$T"
manifest:
skills/source/errors/frappe-errors-serverscripts/SKILL.mdsource content
Server Script Errors — Diagnosis and Resolution
Cross-refs:
frappe-syntax-serverscripts (syntax), frappe-impl-serverscripts (workflows), frappe-errors-clientscripts (client-side).
CRITICAL: Server Scripts Disabled by Default [v15+]
Starting from Frappe v15, Server Scripts are disabled by default. You MUST enable them:
# In site_config.json { "server_script_enabled": 1 }
On Frappe Cloud: Server Scripts are ONLY available on private benches, NOT on shared benches.
Error Diagnosis Flowchart
ERROR IN SERVER SCRIPT │ ├─► ImportError / NameError │ ├─► "import json" → BLOCKED. Use frappe.parse_json() │ ├─► "import datetime" → BLOCKED. Use frappe.utils │ ├─► "import os/sys/subprocess" → BLOCKED. Security restriction │ └─► "NameError: name 'dict' is not defined" → Some builtins restricted │ ├─► SyntaxError: not allowed │ ├─► "try/except" → BLOCKED by RestrictedPython [v14-v15] │ ├─► "raise ValueError" → BLOCKED. Use frappe.throw() │ └─► "exec/eval" → BLOCKED. Security restriction │ ├─► Script runs but nothing happens │ ├─► Wrong Script Type selected → Check Document Event vs API vs Scheduler │ ├─► Wrong DocType selected → Verify exact DocType name │ ├─► Wrong Event selected → Before Save ≠ After Save │ └─► Script disabled → Check "Enabled" checkbox │ ├─► 403 Permission Denied │ ├─► Scheduler script → Runs as Administrator, check role permissions │ ├─► API script → Check Allow Guest setting │ └─► doc_event → User lacks DocType permission │ ├─► Data not saved in Scheduler │ └─► Missing frappe.db.commit() → REQUIRED in scheduler scripts │ └─► API script returns empty/wrong response └─► Not setting frappe.response["message"] → ALWAYS set response
Error Message → Cause → Fix Table
| Error Message | Cause | Fix |
|---|---|---|
| Any statement in sandbox | Use , , etc. |
| Some Python builtins blocked by RestrictedPython | Use or literal |
| RestrictedPython blocks exception handling [v14-v15] | Use conditional checks () instead |
| RestrictedPython blocks | Use |
| Wrong Script Type or Event selected | Verify type matches: Document Event, API, or Scheduler |
| Using in API or Scheduler script (no document context) | is only available in Document Event scripts |
in Scheduler | Scheduler runs as Administrator but script accesses restricted resource | Use where appropriate |
in Scheduler | Missing | ALWAYS call in Scheduler scripts |
| Forgot to set | ALWAYS set |
| Infinite loop or processing too many records | ALWAYS add to queries, ALWAYS use batch processing |
| called in Before Save (recursion) | NEVER call in Before Save; just set values |
| User input in SQL without escaping | ALWAYS use or parameterized queries |
The #1 Error: ImportError
Every beginner hits this. The Server Script sandbox blocks ALL imports except
json.
# ❌ BLOCKED — These ALL fail with ImportError import json # Use frappe.parse_json() / frappe.as_json() from datetime import datetime # Use frappe.utils.now(), frappe.utils.today() import re # Not available in sandbox import os # Security: blocked import requests # Use frappe.make_get_request(), frappe.make_post_request() # ✅ CORRECT — Sandbox equivalents data = frappe.parse_json(doc.json_field) # Instead of json.loads() today = frappe.utils.today() # Instead of datetime.date.today() now = frappe.utils.now() # Instead of datetime.now() diff = frappe.utils.date_diff(date1, date2) # Instead of timedelta resp = frappe.make_get_request("https://api.com") # Instead of requests.get() resp = frappe.make_post_request("https://api.com", data=payload)
Available Sandbox API (Complete Reference)
| Category | Available Methods |
|---|---|
| Document | , , , , , , |
| Database | , , , , , , , , , |
| Query Builder | (full query builder) |
| HTTP | , , |
| Utility | (all utility functions), , |
| User/Session | , , |
| Messages | , , , |
| Module | (the ONLY importable module) |
Script Type Selection Errors
ALWAYS verify you selected the correct Script Type:
| Script Type | Trigger | Has ? | Has ? | Auto-commit? |
|---|---|---|---|---|
| Document Event | DocType lifecycle (Before Save, After Save, etc.) | YES | NO | YES |
| API | HTTP request to | NO | YES | YES |
| Scheduler Event | Cron schedule | NO | NO | NO — MUST call |
| Permission Query | Every list query on the DocType | NO | NO (has ) | N/A |
Common Mistake: Wrong Event
# ❌ WRONG — "After Save" cannot prevent save # Script Type: Document Event, Event: After Save if not doc.customer: frappe.throw("Customer is required") # Document already saved! # ✅ CORRECT — Use "Before Save" or "Before Validate" # Script Type: Document Event, Event: Before Save if not doc.customer: frappe.throw("Customer is required") # Prevents save
Sandbox Workarounds
try/except Is Blocked: Use Conditional Checks
# ❌ BLOCKED in sandbox try: customer = frappe.get_doc("Customer", doc.customer) except Exception: frappe.throw("Customer not found") # ✅ CORRECT — Check first, then access if not frappe.db.exists("Customer", doc.customer): frappe.throw(f"Customer '{doc.customer}' not found") customer = frappe.get_doc("Customer", doc.customer)
raise Is Blocked: Use frappe.throw()
# ❌ BLOCKED if amount < 0: raise ValueError("Amount cannot be negative") # ✅ CORRECT if amount < 0: frappe.throw("Amount cannot be negative")
frappe.throw() Exception Types for API Scripts
| Exception | HTTP Code | Use When |
|---|---|---|
| 417 | Input validation failure |
| 403 | Access denied |
| 404 | Record not found |
| 401 | Not logged in |
| (default, no exc) | 417 | General validation error |
# API Script — Correct exception types if not customer: frappe.throw("Customer param required", exc=frappe.ValidationError) # 417 if not frappe.db.exists("Customer", customer): frappe.throw("Customer not found", exc=frappe.DoesNotExistError) # 404 if not frappe.has_permission("Customer", "read", customer): frappe.throw("Access denied", exc=frappe.PermissionError) # 403
Scheduler Script: Critical Mistakes
# ❌ WRONG — No limit, no commit, no error logging invoices = frappe.get_all("Sales Invoice", filters={"status": "Unpaid"}) for inv in invoices: frappe.db.set_value("Sales Invoice", inv.name, "reminder_sent", 1) # ✅ CORRECT — Limit, batch commit, error logging BATCH_SIZE = 50 invoices = frappe.get_all( "Sales Invoice", filters={"status": "Unpaid", "docstatus": 1}, fields=["name", "customer"], limit=500 # ALWAYS limit ) errors = [] for i in range(0, len(invoices), BATCH_SIZE): batch = invoices[i:i + BATCH_SIZE] for inv in batch: if not frappe.db.exists("Customer", inv.customer): errors.append(f"{inv.name}: Customer not found") continue frappe.db.set_value("Sales Invoice", inv.name, "reminder_sent", 1) frappe.db.commit() # REQUIRED if errors: frappe.log_error("\n".join(errors), "Reminder Errors") frappe.db.commit()
SQL Injection Prevention
# ❌ VULNERABLE — String interpolation with user input territory = frappe.form_dict.get("territory") conditions = f"`tabCustomer`.territory = '{territory}'" # SQL INJECTION! # ✅ SAFE — Use frappe.db.escape() territory = frappe.form_dict.get("territory") conditions = f"`tabCustomer`.territory = {frappe.db.escape(territory)}" # ✅ SAFEST — Use parameterized query or Query Builder results = frappe.db.get_all("Customer", filters={"territory": territory})
ALWAYS / NEVER Rules
ALWAYS
- Use
instead of Python imports — Onlyfrappe.utils.*
module is importablejson - Use
instead offrappe.throw()
—raise
is blocked by sandboxraise - Use conditional checks instead of
— Exception handling is blocked [v14-v15]try/except - Call
in Scheduler scripts — Changes are NOT auto-committedfrappe.db.commit() - Add
to ALL queries in Scheduler scripts — Prevent memory exhaustionlimit - Set
in API scripts — Otherwise response is emptyfrappe.response["message"] - Use
for user input in SQL — Prevent SQL injectionfrappe.db.escape() - Log errors in Scheduler scripts with
— No user to see errorsfrappe.log_error() - Verify Script Type matches your intent — Document Event vs API vs Scheduler
NEVER
- NEVER use
statements (exceptimport
) — Blocked by RestrictedPythonjson - NEVER use
ortry/except
— Blocked by sandbox [v14-v15]raise - NEVER call
in Before Save — Causes infinite recursiondoc.save() - NEVER use string formatting for SQL with user input — SQL injection risk
- NEVER process unlimited records in Scheduler — Always use
limit - NEVER assume
exists in API/Scheduler scripts — Only available in Document Eventsdoc - NEVER forget
in Scheduler — All changes will be lostfrappe.db.commit()
Reference Files
| File | Contents |
|---|---|
| Real error scenarios with diagnosis |
| Common sandbox mistakes with fixes |
| Defensive error handling patterns by script type |