Skip to content

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:

my_app/doctypes/employee/
├── employee.json
└── employee.rhai

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_insertvalidatebefore_save → DB INSERT → after_insertafter_save

Update: before_savevalidate → DB UPDATE → on_updateafter_save

Submit: before_submitvalidate → 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

let result = loom_call("my_app.get_balance", #{ employee: "EMP-001" });

Whitelisted API Methods

Place scripts in api/:

my_app/api/get_leave_balance.rhai
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:

POST /api/method/my_app.get_leave_balance
Content-Type: application/json

{"employee": "EMP-001"}

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:

my_app/doctypes/todo/
├── todo.json
└── todo.client.js

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.

loom.add_button(label, callback, options)
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