frappe-impl-serverscripts — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited frappe-impl-serverscripts (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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
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.
bench set-config server_script_enabled trueWHAT 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` variableSee references/decision-tree.md for complete decision tree.
Goal: Validate Sales Order before save.
Step 1: Choose event — "Before Save" maps to validate hook.
Step 2: Write sandbox-safe script:
# 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:
doc.save() in Before Save — framework handles itfrappe.throw() — msgprint does NOT stop saveGoal: Auto-calculate totals and set derived fields.
# 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.
Goal: Create a ToDo when a new Lead is inserted.
# 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:
doc.name may not exist yetignore_permissions=True for system-generated documentsGoal: Create authenticated REST API returning customer data.
# 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:
min(frappe.utils.cint(limit), 100)Goal: Daily reminder for overdue invoices.
# 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": inv.name,
"status": "Open"
}):
frappe.get_doc({
"doctype": "ToDo",
"allocated_to": inv.owner,
"reference_type": "Sales Invoice",
"reference_name": inv.name,
"description": f"Invoice {inv.name} is {days} days overdue"
}).insert(ignore_permissions=True)
frappe.db.commit() # REQUIRED in scheduler scriptsRules:
frappe.db.commit() at end of scheduler scriptslimit to queries — prevent memory exhaustiontry/except + frappe.log_error() in loopsGoal: Users see only their territory's customers.
# Type: Permission Query | DocType: Customer
user_territory = frappe.db.get_value("User", user, "territory")
user_roles = frappe.get_roles(user)
if "System Manager" in user_roles:
conditions = "" # Full access
elif user_territory:
conditions = f"`tabCustomer`.territory = {frappe.db.escape(user_territory)}"
else:
conditions = f"`tabCustomer`.owner = {frappe.db.escape(user)}"Rules:
conditions = "")frappe.db.escape() for user input in SQLconditions variable — it is the outputfrappe.db.get_list, NOT frappe.db.get_all| UI Name | Internal Hook | Best For |
|---|---|---|
| Before Validate | before_validate | Pre-validation defaults |
| Before Save | `validate` | Validation + calculations (MOST COMMON) |
| After Save | on_update | Notifications, audit logs |
| After Insert | after_insert | Create related docs (new only) |
| Before Submit | before_submit | Submit-time validation |
| After Submit | on_submit | Post-submit automation |
| Before Cancel | before_cancel | Cancel prevention |
| After Cancel | on_cancel | Cleanup after cancel |
| Before Delete | on_trash | Delete prevention |
| Need | Use (NOT import) |
|---|---|
| Parse JSON | frappe.parse_json() or json.loads() |
| Serialize JSON | frappe.as_json() or json.dumps() |
| Today's date | frappe.utils.today() |
| Now (datetime) | frappe.utils.now() |
| Add days | frappe.utils.add_days(date, n) |
| Date diff | frappe.utils.date_diff(d1, d2) |
| Float conversion | frappe.utils.flt(val) |
| Int conversion | frappe.utils.cint(val) |
| HTTP GET | frappe.make_get_request(url) |
| HTTP POST | frappe.make_post_request(url, data) |
| Render template | frappe.render_template(tmpl, ctx) |
| Log error | frappe.log_error(msg, title) |
| Send email | frappe.sendmail(recipients, subject, message) |
ALWAYS migrate to a Document Controller when:
import statements (beyond json)frappe.enqueue() for background jobsMigration path: See frappe-impl-controllers for controller implementation.
frappe-syntax-serverscripts — Exact sandbox API referencefrappe-errors-serverscripts — Error handling and anti-patternsfrappe-core-database — frappe.db.* operationsfrappe-core-permissions — Permission system detailsfrappe-impl-controllers — When to migrate from Server ScriptSee references/decision-tree.md for complete decision trees. See references/workflows.md for extended patterns. See references/examples.md for 10+ complete examples.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.