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 |
ImportError: import not allowed |
Any import statement in sandbox |
Use frappe.utils, frappe.parse_json(), etc. |
NameError: name 'dict' is not defined |
Some Python builtins blocked by RestrictedPython |
Use frappe._dict() or literal {} |
SyntaxError: try/except not allowed |
RestrictedPython blocks exception handling [v14-v15] |
Use conditional checks (if/else) instead |
SyntaxError: raise not allowed |
RestrictedPython blocks raise |
Use frappe.throw() |
Script not executing |
Wrong Script Type or Event selected |
Verify type matches: Document Event, API, or Scheduler |
doc is not defined |
Using doc in API or Scheduler script (no document context) |
doc is only available in Document Event scripts |
PermissionError in Scheduler |
Scheduler runs as Administrator but script accesses restricted resource |
Use ignore_permissions=True where appropriate |
Changes not saved in Scheduler |
Missing frappe.db.commit() |
ALWAYS call frappe.db.commit() in Scheduler scripts |
API returns empty response |
Forgot to set frappe.response["message"] |
ALWAYS set frappe.response["message"] = result |
Timeout / killed |
Infinite loop or processing too many records |
ALWAYS add limit to queries, ALWAYS use batch processing |
ValidationError: qty is required |
doc.save() called in Before Save (recursion) |
NEVER call doc.save() in Before Save; just set values |
SQL injection via string format |
User input in SQL without escaping |
ALWAYS use frappe.db.escape() 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 |
frappe.get_doc(), frappe.new_doc(), frappe.get_last_doc(), frappe.get_cached_doc(), frappe.get_mapped_doc(), frappe.rename_doc(), frappe.delete_doc() |
| Database |
frappe.db.get_list(), frappe.db.get_all(), frappe.db.get_value(), frappe.db.get_single_value(), frappe.db.set_value(), frappe.db.exists(), frappe.db.sql(), frappe.db.commit(), frappe.db.rollback(), frappe.db.escape() |
| Query Builder |
frappe.qb (full query builder) |
| HTTP |
frappe.make_get_request(), frappe.make_post_request(), frappe.make_put_request() |
| Utility |
frappe.utils.* (all utility functions), frappe.parse_json(), frappe.as_json() |
| User/Session |
frappe.session.user, frappe.get_roles(), frappe.has_permission() |
| Messages |
frappe.throw(), frappe.msgprint(), frappe.log_error(), frappe.sendmail() |
| Module |
json (the ONLY importable module) |
Script Type Selection Errors
ALWAYS verify you selected the correct Script Type:
| Script Type |
Trigger |
Has doc? |
Has frappe.form_dict? |
Auto-commit? |
| Document Event |
DocType lifecycle (Before Save, After Save, etc.) |
YES |
NO |
YES |
| API |
HTTP request to /api/method/{method_name} |
NO |
YES |
YES |
| Scheduler Event |
Cron schedule |
NO |
NO |
NO — MUST call frappe.db.commit() |
| Permission Query |
Every list query on the DocType |
NO |
NO (has user) |
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 |
frappe.ValidationError |
417 |
Input validation failure |
frappe.PermissionError |
403 |
Access denied |
frappe.DoesNotExistError |
404 |
Record not found |
frappe.AuthenticationError |
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
frappe.utils.* instead of Python imports — Only json module is importable
- Use
frappe.throw() instead of raise — raise is blocked by sandbox
- Use conditional checks instead of
try/except — Exception handling is blocked [v14-v15]
- Call
frappe.db.commit() in Scheduler scripts — Changes are NOT auto-committed
- Add
limit to ALL queries in Scheduler scripts — Prevent memory exhaustion
- Set
frappe.response["message"] in API scripts — Otherwise response is empty
- Use
frappe.db.escape() for user input in SQL — Prevent SQL injection
- Log errors in Scheduler scripts with
frappe.log_error() — No user to see errors
- Verify Script Type matches your intent — Document Event vs API vs Scheduler
NEVER
- NEVER use
import statements (except json) — Blocked by RestrictedPython
- NEVER use
try/except or raise — Blocked by sandbox [v14-v15]
- NEVER call
doc.save() in Before Save — Causes infinite recursion
- NEVER use string formatting for SQL with user input — SQL injection risk
- NEVER process unlimited records in Scheduler — Always use
limit
- NEVER assume
doc exists in API/Scheduler scripts — Only available in Document Events
- NEVER forget
frappe.db.commit() in Scheduler — All changes will be lost
Reference Files
| File |
Contents |
references/examples.md |
Real error scenarios with diagnosis |
references/anti-patterns.md |
Common sandbox mistakes with fixes |
references/patterns.md |
Defensive error handling patterns by script type |
1---2name: frappe-errors-serverscripts3description: >4 Use when debugging or preventing errors in Frappe Server Scripts.5 Prevents ImportError (the #1 error), NameError for restricted builtins,6 sandbox violations, doc_events not firing, wrong script type selection,7 SQL injection, permission denied in scheduled scripts, infinite loops,8 and API scripts not returning JSON. Covers error message mapping table.9 Keywords: server script error, ImportError, NameError, sandbox,, ImportError in server script, script not running, sandbox error, restricted function.10 restricted, frappe.throw, doc_events, scheduler, API script, SQL injection.11license: MIT12compatibility: "Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16."13metadata:14 author: OpenAEC-Foundation15 version: "2.0"16---1718# Server Script Errors — Diagnosis and Resolution1920Cross-refs: `frappe-syntax-serverscripts` (syntax), `frappe-impl-serverscripts` (workflows), `frappe-errors-clientscripts` (client-side).2122---2324## CRITICAL: Server Scripts Disabled by Default [v15+]2526Starting from Frappe v15, Server Scripts are **disabled by default**. You MUST enable them:2728```python29# In site_config.json30{ "server_script_enabled": 1 }31```3233On Frappe Cloud: Server Scripts are ONLY available on **private benches**, NOT on shared benches.3435---3637## Error Diagnosis Flowchart3839```40ERROR IN SERVER SCRIPT41│42├─► ImportError / NameError43│ ├─► "import json" → BLOCKED. Use frappe.parse_json()44│ ├─► "import datetime" → BLOCKED. Use frappe.utils45│ ├─► "import os/sys/subprocess" → BLOCKED. Security restriction46│ └─► "NameError: name 'dict' is not defined" → Some builtins restricted47│48├─► SyntaxError: not allowed49│ ├─► "try/except" → BLOCKED by RestrictedPython [v14-v15]50│ ├─► "raise ValueError" → BLOCKED. Use frappe.throw()51│ └─► "exec/eval" → BLOCKED. Security restriction52│53├─► Script runs but nothing happens54│ ├─► Wrong Script Type selected → Check Document Event vs API vs Scheduler55│ ├─► Wrong DocType selected → Verify exact DocType name56│ ├─► Wrong Event selected → Before Save ≠ After Save57│ └─► Script disabled → Check "Enabled" checkbox58│59├─► 403 Permission Denied60│ ├─► Scheduler script → Runs as Administrator, check role permissions61│ ├─► API script → Check Allow Guest setting62│ └─► doc_event → User lacks DocType permission63│64├─► Data not saved in Scheduler65│ └─► Missing frappe.db.commit() → REQUIRED in scheduler scripts66│67└─► API script returns empty/wrong response68 └─► Not setting frappe.response["message"] → ALWAYS set response69```7071---7273## Error Message → Cause → Fix Table7475| Error Message | Cause | Fix |76|---------------|-------|-----|77| `ImportError: import not allowed` | Any `import` statement in sandbox | Use `frappe.utils`, `frappe.parse_json()`, etc. |78| `NameError: name 'dict' is not defined` | Some Python builtins blocked by RestrictedPython | Use `frappe._dict()` or literal `{}` |79| `SyntaxError: try/except not allowed` | RestrictedPython blocks exception handling [v14-v15] | Use conditional checks (`if/else`) instead |80| `SyntaxError: raise not allowed` | RestrictedPython blocks `raise` | Use `frappe.throw()` |81| `Script not executing` | Wrong Script Type or Event selected | Verify type matches: Document Event, API, or Scheduler |82| `doc is not defined` | Using `doc` in API or Scheduler script (no document context) | `doc` is only available in Document Event scripts |83| `PermissionError` in Scheduler | Scheduler runs as Administrator but script accesses restricted resource | Use `ignore_permissions=True` where appropriate |84| `Changes not saved` in Scheduler | Missing `frappe.db.commit()` | ALWAYS call `frappe.db.commit()` in Scheduler scripts |85| `API returns empty response` | Forgot to set `frappe.response["message"]` | ALWAYS set `frappe.response["message"] = result` |86| `Timeout / killed` | Infinite loop or processing too many records | ALWAYS add `limit` to queries, ALWAYS use batch processing |87| `ValidationError: qty is required` | `doc.save()` called in Before Save (recursion) | NEVER call `doc.save()` in Before Save; just set values |88| `SQL injection via string format` | User input in SQL without escaping | ALWAYS use `frappe.db.escape()` or parameterized queries |8990---9192## The #1 Error: ImportError9394**Every beginner hits this.** The Server Script sandbox blocks ALL imports except `json`.9596```python97# ❌ BLOCKED — These ALL fail with ImportError98import json # Use frappe.parse_json() / frappe.as_json()99from datetime import datetime # Use frappe.utils.now(), frappe.utils.today()100import re # Not available in sandbox101import os # Security: blocked102import requests # Use frappe.make_get_request(), frappe.make_post_request()103104# ✅ CORRECT — Sandbox equivalents105data = frappe.parse_json(doc.json_field) # Instead of json.loads()106today = frappe.utils.today() # Instead of datetime.date.today()107now = frappe.utils.now() # Instead of datetime.now()108diff = frappe.utils.date_diff(date1, date2) # Instead of timedelta109resp = frappe.make_get_request("https://api.com") # Instead of requests.get()110resp = frappe.make_post_request("https://api.com", data=payload)111```112113### Available Sandbox API (Complete Reference)114115| Category | Available Methods |116|----------|-------------------|117| **Document** | `frappe.get_doc()`, `frappe.new_doc()`, `frappe.get_last_doc()`, `frappe.get_cached_doc()`, `frappe.get_mapped_doc()`, `frappe.rename_doc()`, `frappe.delete_doc()` |118| **Database** | `frappe.db.get_list()`, `frappe.db.get_all()`, `frappe.db.get_value()`, `frappe.db.get_single_value()`, `frappe.db.set_value()`, `frappe.db.exists()`, `frappe.db.sql()`, `frappe.db.commit()`, `frappe.db.rollback()`, `frappe.db.escape()` |119| **Query Builder** | `frappe.qb` (full query builder) |120| **HTTP** | `frappe.make_get_request()`, `frappe.make_post_request()`, `frappe.make_put_request()` |121| **Utility** | `frappe.utils.*` (all utility functions), `frappe.parse_json()`, `frappe.as_json()` |122| **User/Session** | `frappe.session.user`, `frappe.get_roles()`, `frappe.has_permission()` |123| **Messages** | `frappe.throw()`, `frappe.msgprint()`, `frappe.log_error()`, `frappe.sendmail()` |124| **Module** | `json` (the ONLY importable module) |125126---127128## Script Type Selection Errors129130ALWAYS verify you selected the correct Script Type:131132| Script Type | Trigger | Has `doc`? | Has `frappe.form_dict`? | Auto-commit? |133|-------------|---------|:----------:|:-----------------------:|:------------:|134| Document Event | DocType lifecycle (Before Save, After Save, etc.) | YES | NO | YES |135| API | HTTP request to `/api/method/{method_name}` | NO | YES | YES |136| Scheduler Event | Cron schedule | NO | NO | NO — MUST call `frappe.db.commit()` |137| Permission Query | Every list query on the DocType | NO | NO (has `user`) | N/A |138139### Common Mistake: Wrong Event140141```python142# ❌ WRONG — "After Save" cannot prevent save143# Script Type: Document Event, Event: After Save144if not doc.customer:145 frappe.throw("Customer is required") # Document already saved!146147# ✅ CORRECT — Use "Before Save" or "Before Validate"148# Script Type: Document Event, Event: Before Save149if not doc.customer:150 frappe.throw("Customer is required") # Prevents save151```152153---154155## Sandbox Workarounds156157### try/except Is Blocked: Use Conditional Checks158159```python160# ❌ BLOCKED in sandbox161try:162 customer = frappe.get_doc("Customer", doc.customer)163except Exception:164 frappe.throw("Customer not found")165166# ✅ CORRECT — Check first, then access167if not frappe.db.exists("Customer", doc.customer):168 frappe.throw(f"Customer '{doc.customer}' not found")169customer = frappe.get_doc("Customer", doc.customer)170```171172### raise Is Blocked: Use frappe.throw()173174```python175# ❌ BLOCKED176if amount < 0:177 raise ValueError("Amount cannot be negative")178179# ✅ CORRECT180if amount < 0:181 frappe.throw("Amount cannot be negative")182```183184### frappe.throw() Exception Types for API Scripts185186| Exception | HTTP Code | Use When |187|-----------|:---------:|----------|188| `frappe.ValidationError` | 417 | Input validation failure |189| `frappe.PermissionError` | 403 | Access denied |190| `frappe.DoesNotExistError` | 404 | Record not found |191| `frappe.AuthenticationError` | 401 | Not logged in |192| (default, no exc) | 417 | General validation error |193194```python195# API Script — Correct exception types196if not customer:197 frappe.throw("Customer param required", exc=frappe.ValidationError) # 417198if not frappe.db.exists("Customer", customer):199 frappe.throw("Customer not found", exc=frappe.DoesNotExistError) # 404200if not frappe.has_permission("Customer", "read", customer):201 frappe.throw("Access denied", exc=frappe.PermissionError) # 403202```203204---205206## Scheduler Script: Critical Mistakes207208```python209# ❌ WRONG — No limit, no commit, no error logging210invoices = frappe.get_all("Sales Invoice", filters={"status": "Unpaid"})211for inv in invoices:212 frappe.db.set_value("Sales Invoice", inv.name, "reminder_sent", 1)213214# ✅ CORRECT — Limit, batch commit, error logging215BATCH_SIZE = 50216invoices = frappe.get_all(217 "Sales Invoice",218 filters={"status": "Unpaid", "docstatus": 1},219 fields=["name", "customer"],220 limit=500 # ALWAYS limit221)222223errors = []224for i in range(0, len(invoices), BATCH_SIZE):225 batch = invoices[i:i + BATCH_SIZE]226 for inv in batch:227 if not frappe.db.exists("Customer", inv.customer):228 errors.append(f"{inv.name}: Customer not found")229 continue230 frappe.db.set_value("Sales Invoice", inv.name, "reminder_sent", 1)231 frappe.db.commit() # REQUIRED232233if errors:234 frappe.log_error("\n".join(errors), "Reminder Errors")235frappe.db.commit()236```237238---239240## SQL Injection Prevention241242```python243# ❌ VULNERABLE — String interpolation with user input244territory = frappe.form_dict.get("territory")245conditions = f"`tabCustomer`.territory = '{territory}'" # SQL INJECTION!246247# ✅ SAFE — Use frappe.db.escape()248territory = frappe.form_dict.get("territory")249conditions = f"`tabCustomer`.territory = {frappe.db.escape(territory)}"250251# ✅ SAFEST — Use parameterized query or Query Builder252results = frappe.db.get_all("Customer", filters={"territory": territory})253```254255---256257## ALWAYS / NEVER Rules258259### ALWAYS2602611. **Use `frappe.utils.*` instead of Python imports** — Only `json` module is importable2622. **Use `frappe.throw()` instead of `raise`** — `raise` is blocked by sandbox2633. **Use conditional checks instead of `try/except`** — Exception handling is blocked [v14-v15]2644. **Call `frappe.db.commit()` in Scheduler scripts** — Changes are NOT auto-committed2655. **Add `limit` to ALL queries in Scheduler scripts** — Prevent memory exhaustion2666. **Set `frappe.response["message"]` in API scripts** — Otherwise response is empty2677. **Use `frappe.db.escape()` for user input in SQL** — Prevent SQL injection2688. **Log errors in Scheduler scripts** with `frappe.log_error()` — No user to see errors2699. **Verify Script Type matches your intent** — Document Event vs API vs Scheduler270271### NEVER2722731. **NEVER use `import` statements** (except `json`) — Blocked by RestrictedPython2742. **NEVER use `try/except` or `raise`** — Blocked by sandbox [v14-v15]2753. **NEVER call `doc.save()` in Before Save** — Causes infinite recursion2764. **NEVER use string formatting for SQL with user input** — SQL injection risk2775. **NEVER process unlimited records in Scheduler** — Always use `limit`2786. **NEVER assume `doc` exists in API/Scheduler scripts** — Only available in Document Events2797. **NEVER forget `frappe.db.commit()` in Scheduler** — All changes will be lost280281---282283## Reference Files284285| File | Contents |286|------|----------|287| `references/examples.md` | Real error scenarios with diagnosis |288| `references/anti-patterns.md` | Common sandbox mistakes with fixes |289| `references/patterns.md` | Defensive error handling patterns by script type |