Server Scripts
A Server Script is Python that runs on the server. Unlike a client script, it cannot be bypassed: it runs whether the document was saved from Desk, from a floor app, from an import or from the API.
This is where real rules live.
Turning them on
Server scripts are disabled by default, because they run code on your server. Enabling them is a site-level setting an administrator applies:
- On Frappe Cloud: open your site's dashboard → Site Config → add
server_script_enabledset totrue. - Self-hosted: add
"server_script_enabled": 1to the site'ssite_config.jsonand restart.
Until that is done, the Server Script list will tell you they are disabled.
Anyone who can create a Server Script can run code on your server as part of every save. Restrict the Script Manager role tightly — in practice, to the same small group who hold System Manager.
Creating one
- Awesome Bar (Cmd/Ctrl + K) →
server script→ + Add Server Script. - Give it a Name that says what it does —
PO: require hold reason. - Choose the Script Type (see below).
- Fill in what that type needs, write the script, tick Enabled, save.
The four script types
| Type | Runs when | Use for |
|---|---|---|
| DocType Event | A document is saved, submitted, cancelled… | Validation, defaults, automation |
| API | Someone calls your endpoint | Custom integrations |
| Scheduler Event | On a schedule | Nightly jobs, reminders |
| Permission Query | A list is loaded | Restricting what a role can see |
DocType Event scripts
The most common. Pick a Reference Document Type and a DocType Event:
| Event | When | Typical use |
|---|---|---|
| Before Validate | Before validation | Normalise input |
| Before Save / Validate | Before writing | Reject bad data, compute fields |
| After Insert | Just after the first save | Act on creation |
| On Update | After a save | React to a change |
| Before Submit / On Submit | Around submitting | Final checks, create related documents |
| On Cancel | On cancelling | Reverse what submit did |
| On Trash | Before deletion | Block deletion, clean up |
Inside the script, doc is the document being saved.
Enforce a rule
if doc.status == "On Hold" and not doc.custom_hold_reason:
frappe.throw("Enter a hold reason before putting this order on hold.")
frappe.throw() aborts the save and shows the message to whoever triggered it — a user in Desk, or an API client as an error response.
Set this on Production Order, event Validate, and the rule now holds everywhere. The matching client script is still worth having, because it tells the user at the right moment rather than after they press save.
Compute a field
total = 0
for row in doc.items:
total += (row.qty or 0)
doc.custom_total_pieces = total
On Before Save, assigning to doc.<field> is enough — the value is written as part of the save in progress. (Make the field Read Only so nobody types over it.)
Create a related document
if doc.result == "Fail":
task = frappe.new_doc("ToDo")
task.description = f"Failed fabric test on {doc.tech_pack}"
task.reference_type = "Fabric Test Certificate"
task.reference_name = doc.name
task.priority = "High"
task.insert(ignore_permissions=True)
On After Insert of your Fabric Test Certificate, a failed test raises a task automatically.
ignore_permissions=True lets the script create the ToDo even if the user couldn't create one by hand. Use it deliberately — it is a bypass.
Guard against loops
A script on On Update that saves the same document will trigger itself. Either write with frappe.db.set_value(), which does not re-run document events:
frappe.db.set_value("Production Order", doc.name, "custom_flag", 1, update_modified=False)
…or compute in Before Save and let the save in progress persist it.
The Python you can use
Server scripts run in a restricted sandbox. You cannot import anything; you get a curated set of helpers instead.
Available:
frappe.db.get_value(doctype, name_or_filters, fieldname)
frappe.db.get_list(doctype, filters={...}, fields=[...], limit=20)
frappe.db.exists(doctype, name)
frappe.db.count(doctype, filters={...})
frappe.db.set_value(doctype, name, fieldname, value)
frappe.get_doc(doctype, name) # a full document, with its child tables
frappe.new_doc(doctype)
frappe.delete_doc(doctype, name)
frappe.throw(msg) # abort with an error
frappe.msgprint(msg) # show a message, carry on
frappe.log_error(message, title) # write to the Error Log
frappe.session.user # who triggered this
frappe.utils.today(), nowdate(), add_days(), flt(), cint(), getdate()
frappe.sendmail(recipients=[], subject="", message="")
Plus normal Python: if, for, len(), sum(), f-strings, lists and dicts.
Not available: import, file access, network calls, eval, exec. If you need those, you need a custom app, not a server script.
Never call frappe.db.commit(). Frappe commits when the request finishes. Committing mid-way can leave half a transaction written if something later fails.
API scripts
Script Type API creates your own HTTP endpoint. Set API Method to a name — factory_summary — and write:
open_orders = frappe.db.count("Production Order", {"status": ["in", ["Planned", "Released", "In Progress"]]})
running_batches = frappe.db.count("Production Batch", {"status": "In Progress"})
frappe.response["message"] = {
"open_orders": open_orders,
"running_batches": running_batches,
}
Whatever you put in frappe.response["message"] is the JSON returned.
Call it at:
GET https://your-site.com/api/method/factory_summary
Tick Allow Guest only if the data is genuinely public. Otherwise callers authenticate normally — see API & integrations.
This is the quickest way to give an external system exactly the shape it wants, instead of making it assemble five REST calls.
Scheduler Event scripts
Script Type Scheduler Event, then a frequency: Hourly, Daily, Weekly, Monthly, or Cron for a specific time.
from_date = frappe.utils.add_days(frappe.utils.today(), 3)
due_soon = frappe.db.get_list(
"Production Order",
filters={"status": ["in", ["Released", "In Progress"]], "end_date": ["<=", from_date]},
fields=["name", "end_date"],
)
if due_soon:
lines = "".join(f"<li>{d.name} — due {d.end_date}</li>" for d in due_soon)
frappe.sendmail(
recipients=["planning@yourfactory.com"],
subject=f"{len(due_soon)} orders due within 3 days",
message=f"<ul>{lines}</ul>",
)
Three things to get right with scheduled scripts:
- They must be safe to run twice. A retry, or a second server, will run them again.
- Nobody sees the errors. Check the Error Log and Scheduled Job Log if a job seems not to fire.
- The scheduler must be enabled on the site. On Frappe Cloud it is; self-hosted, confirm it.
For most alerting, a Notification is easier and needs no code. Reach for a scheduled script when the logic is beyond what a Notification's condition can express.
Permission Query scripts
Restrict which records a role sees in list views and reports. Set Reference Document Type, and return a SQL condition:
if "Line Supervisor" in frappe.get_roles(frappe.session.user):
unit = frappe.db.get_value("Employee", {"user_id": frappe.session.user}, "custom_production_unit")
if unit:
conditions = f"`tabProduction Batch`.production_unit = {frappe.db.escape(unit)}"
Assign the condition to conditions. It is appended to every query for that DocType.
Always pass values through frappe.db.escape(). Building the string from raw user input is a SQL injection.
Testing safely
- Write it disabled. Create the script with Enabled off, then turn it on when you are ready.
- Start with
frappe.msgprint, notfrappe.throw. See what it would have blocked before it blocks anything. - Try the failure case. A validation nobody has tested with bad data is a validation you don't know works.
- Watch the Error Log. Awesome Bar →
error log. Anything your script raises that isn't athrowlands there with a traceback.
To disable a misbehaving script fast, untick Enabled. It takes effect on the next request — no restart.
When you have outgrown this
Move to a custom app when you hit any of:
- you need libraries the sandbox forbids,
- the logic wants tests,
- several sites need the same behaviour,
- you want it in git and code-reviewed.
The logic transfers almost unchanged into an app's hooks.py doc_events — the sandbox is a subset of what an app can do, so nothing is wasted.
What to do next
Continue to Print documents, or to Configuring notifications if the reason you came here was alerting.