Rhai Scripting¶
Rhai is the embedded scripting language for hooks, validations, and API methods. It's sandboxed, fast, and requires no external runtime.
DocType Scripts¶
Place a .rhai file next to the DocType JSON:
Hook Events¶
fn before_insert(doc) {
// Runs before a new document is inserted
doc
}
fn validate(doc) {
// Runs before save (insert or update)
if doc.email == "" {
throw("Email is required");
}
doc
}
fn before_save(doc) {
// Runs before DB write
doc.full_name = doc.first_name + " " + doc.last_name;
doc
}
fn on_update(doc) {
// Runs after update
}
fn on_submit(doc) {
// Runs after submit (docstatus 0 → 1)
}
fn before_cancel(doc) {
// Runs before cancel
}
fn on_trash(doc) {
// Runs before delete
}
Execution Order¶
Insert: before_insert → validate → before_save → DB INSERT → after_insert → after_save
Update: before_save → validate → DB UPDATE → on_update → after_save
Submit: before_submit → validate → DB UPDATE → on_submit
Cancel: before_cancel → DB UPDATE → on_cancel
Delete: on_trash → DB DELETE
Loom API¶
Available in all scripts:
Database¶
let doc = loom_db_get_doc("Employee", "EMP-001");
let name = loom_db_get_value("Employee", #{ department: "Engineering" }, "employee_name");
let employees = loom_db_get_all("Employee", #{
filters: #{ status: "Active" },
fields: ["employee_name", "department"],
order_by: "creation desc",
limit: 20,
});
loom_db_set_value("Employee", "EMP-001", "status", "Inactive");
loom_db_add_value("Leave Allocation", "LA-001", "used_leaves", 1.0);
let exists = loom_db_exists("Employee", #{ email: "john@example.com" });
let count = loom_db_count("Employee", #{ status: "Active" });
let new_doc = loom_db_insert(#{
doctype: "Todo",
title: "Follow up",
status: "Open",
});
loom_db_delete("Todo", "TODO-123");
// Read-only raw SQL
let results = loom_db_sql("SELECT id, title FROM \"tabTodo\" WHERE status = $1", ["Open"]);
Session¶
let user = loom_session_user(); // "john@example.com"
let roles = loom_session_roles(); // ["All", "Employee"]
Permissions¶
let can_read = loom_has_permission("Employee", "read"); // true/false
loom_check_permission("Employee", "write"); // throws if denied
Utilities¶
throw("Validation error message");
log("Debug message");
msgprint("Info message for the user");
let today_str = today(); // "2026-03-15"
let now_str = now(); // ISO 8601 datetime
let days = date_diff("2026-03-15", "2026-03-01"); // 14
let next_week = loom_add_days("2026-03-15", 7); // "2026-03-22"
let next_month = loom_add_months("2026-03-15", 1); // "2026-04-15"
Background Jobs¶
// Default queue
loom_enqueue("my_app.send_email", #{ to: "user@example.com" });
// Named queue with priority
loom_enqueue("my_app.heavy_report", #{ id: "RPT-001" }, #{
queue: "long",
priority: 5,
});
Call Other Methods¶
Whitelisted API Methods¶
Place scripts in api/:
fn main(params, loom) {
loom_check_permission("Leave Application", "read");
let balance = loom_db_get_value("Leave Allocation", #{
employee: params.employee,
}, "remaining_leaves");
#{ balance: balance }
}
Call via HTTP:
Client Scripts¶
Client scripts run in the browser and add UI behavior to DocType forms and list views. Place a .client.js file next to the DocType JSON:
loom.validate¶
Run validation before save. Return a string to block the save and show an error.
loom.validate = function(doc) {
if (!doc.title || doc.title.trim() === "") {
return "Title is required";
}
};
loom.add_button¶
Add custom action buttons to the form view, list view, or both.
| Parameter | Type | Description |
|---|---|---|
label |
string | Button text |
callback |
function | Called with doc (form) or selectedRows (list) |
options.variant |
string | "primary", "secondary", etc. |
options.view |
string | "form", "list", or "both" (default: "both") |
// Form only — receives the current document
loom.add_button("Approve", function(doc) {
doc.status = "Approved";
}, { variant: "primary", view: "form" });
// List only — receives selected rows
loom.add_button("Bulk Close", function(selectedRows) {
alert("Closing " + selectedRows.length + " items");
}, { view: "list" });
// Both views
loom.add_button("Print", function(docOrRows) {
// ...
}, { view: "both" });
loom.on_change¶
React to field value changes (form view):
loom.on_change = function(fieldname, value, doc) {
if (fieldname === "quantity" || fieldname === "rate") {
doc.amount = (doc.quantity || 0) * (doc.rate || 0);
}
};
How Client Scripts Are Loaded¶
Client scripts are stored in the __customization table and served as part of the DocType meta response (GET /api/doctype/{name}). The frontend evaluates them when the form or list view mounts.
Client scripts are synced from .client.js files to the database by loom migrate (and loom install-app, which runs migrate internally). In developer mode, saving a .client.js file also triggers an automatic reload into the database without restarting the server.
Sandbox Limits¶
Scripts run in a sandboxed environment:
| Limit | Value |
|---|---|
| Max expression depth | 64 |
| Max call stack depth | 32 |
| Max operations | 100,000 |
| Max string size | 1 MB |
| Max array size | 10,000 |
| Max map size | 10,000 |
| No filesystem access | — |
| No network access | — |