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_enabled set to true.
  • Self-hosted: add "server_script_enabled": 1 to the site's site_config.json and restart.

Until that is done, the Server Script list will tell you they are disabled.

Caution

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

  1. Awesome Bar (Cmd/Ctrl + K) → server script+ Add Server Script.
  2. Give it a Name that says what it does — PO: require hold reason.
  3. Choose the Script Type (see below).
  4. 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.)

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.

Note

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

  1. Write it disabled. Create the script with Enabled off, then turn it on when you are ready.
  2. Start with frappe.msgprint, not frappe.throw. See what it would have blocked before it blocks anything.
  3. Try the failure case. A validation nobody has tested with bad data is a validation you don't know works.
  4. Watch the Error Log. Awesome Bar → error log. Anything your script raises that isn't a throw lands 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.

WhatsApp