Getting Started with Ragic Workflow
Ragic provides a server-side JavaScript workflow engine based on Nashorn, supporting ECMAScript 5.1. This engine enables complex business logic automation across forms.
Nashorn supports ECMAScript 5.1, so avoid using ECMAScript 6 syntax. Additionally, data handled and passed by Java to JavaScript (such as arrays) may be subject to certain limitations, preventing the direct use of JavaScript methods like join, map, etc. However, if you define a JavaScript array directly within the workflow and store data in it, these JavaScript methods can be used normally. Likewise, browser-specific APIs such as setTimeout, setInterval, alert, document, etc., cannot be used here.
var query = db.getAPIQuery(pathToForm);
var entry = query.getAPIResult();
var values = entry.getFieldValues(fieldId); // Returns an array
var str = values.join(','); // Not usable, will cause an error
var ary = [];
for (var i = 0; i < values.length; i++) {
ary.push(values[i]);
}
var str = ary.join(','); // Usable
What does JavaScript Workflow Do?
Ragic's form design interface can handle most of your data management work, such as creating, editing, and querying records without much problem. On the other hand, manual data maintenance can be a bit time-consuming and routine after a while. This is the time when Ragic users will start thinking of ways to automate these processes. Inside Ragic, there is a powerful scripting engine where you can write JavaScript that runs on the server-side, to retrieve data you have on your form, make modifications, or even create many records with one click. Typical usage includes updating inventory, creating a new record based on another (creating a sales order from a quote, creating a contact from a sales lead), or doing record validation based on database data.
There are 5 main ways to run your JavaScript workflow:
And there is a Global Workflow where you can place common JavaScript function definitions shared by more than one of your workflow scripts.
Quick Start
Your First Workflow
- Open your Ragic sheet
- Click the sheet tab menu (down arrow next to sheet name)
- Select Javascript Workflow
- Choose the workflow type (e.g., Post-Workflow)
- Write your JavaScript code:
// Post-workflow: Log a message when a record is saved
var entry = param.getUpdatedEntry();
var recordId = entry.getRootNodeId();
log.info("Record saved: " + recordId);
Accessing Form Data
// Get a query object for the current form
var query = db.getAPIQuery("/your-path/1");
// Retrieve a specific record
var entry = query.getAPIEntry(12345);
// Read a field value
var customerName = entry.getFieldValue(1000001);
Modifying Records
// Get and modify a record
var entry = query.getAPIEntry(12345);
entry.setFieldValue(1000002, "Updated Value");
entry.save();
What's Next
- Core Concepts - Understand the data model
- Workflow Types - Choose the right workflow
- API Reference - Full API documentation
Core Concepts
Glossary of Terms
In the workflow, several specific terms frequently appear. These terms are defined as follows. Suppose a data entry in our form has the URL https://www.ragic.com/testAP/testForm/1/3:
| Term | Definition |
|---|---|
| APName | testAP, the name of the user's database |
| Path | /testForm, the tab where this form is located; the leading slash is required |
| SheetIndex | 1, the index of this form within the tab |
| pathToForm | /testForm/1, which is the combination of Path and SheetIndex, commonly used as a parameter |
| rootNodeId, recordId | 3, the ID of this data entry (also referred to as a record); it can also be obtained through getNewNodeId(keyFieldId) or getOldNodeId(keyFieldId) |
| Key field | The primary key of this form; keyFieldId is the ID of the primary key field, which can be found in the database field definition document, or obtained using entry.getKeyFieldId() |
| Field | A field, with fieldId being the field's ID and fieldName being the field's name. The fieldId can be found in the database field definition document, or by selecting the field in design mode and going to "Field Settings" => "Basic"; the seven-digit number under the field name is the fieldId |
| Subtable | A subtable, which can be imagined as a form beneath another form; it also has its own keyFieldId, fieldId, and rootNodeId |
| subtableId | The ID of the subtable, which can be found in the database field definition document. It can be imagined as the subtable's keyFieldId |
| subtableRowIndex | The index of the subtable data, usually specified using a loop |
| subtableFieldId | The ID of the subtable's field, which can be found in the database field definition document. It can be imagined as the subtable's fieldId |
| subtableRootNodeId | The ID of the subtable record, which can be obtained using getSubtableRootNodeId(subtableId, subtableRowIndex). It can be imagined as the subtable's rootNodeId |
Display Message
You can display a message in a pop-up like this. Please note that Action Buttons do not support setStatus('CONFIRM').
response.setStatus('WARN');
response.setMessage(message);
Additionally, since the workflow does not support JavaScript commands like console or alert, if you want to debug, you can use log.setToConsole(true) and log.println(message) to display messages.
var message = "hello ragic";
log.setToConsole(true); // Display the console block
log.println(message); // Print "hello ragic"
Data Model
Records and Entries
A record (also called an entry) is the fundamental unit of data in Ragic. Each record is identified by a unique root node ID (also called record ID).
// Get a record by its ID
var entry = query.getAPIEntry(12345);
// Get the record's ID
var recordId = entry.getRootNodeId(); // Returns 12345
Fields and fieldId
Each field in a form has a unique field ID (also called fieldId). Field IDs are numbers like 1000001.
// Read a field value by ID
var value = entry.getFieldValue(1000001);
// Read by field name (alternative)
var value = entry.getFieldValueByName("Customer Name");
Subtables
Subtables are groups of repeating rows within a record. Each subtable has a subtable root field ID, and each row has its own root node ID.
// Count rows in a subtable
var rowCount = entry.getSubtableSize(1000100);
// Read from a specific row
var qty = entry.getSubtableFieldValue(1000100, 0, "1000101");
// Add a new row (use negative IDs)
entry.setSubtableFieldValue(1000101, -100, "New Value");
Workflow Context
Available Objects
Different workflow types have access to different global objects:
| Object | Action Button | Pre-Workflow | Post-Workflow | Daily | Approval |
|---|---|---|---|---|---|
db |
✓ | ✓ | ✓ | ✓ | ✓ |
response |
✓ | ✓ | ✓ | ✓ | ✓ |
user |
✓ | ✓ | ✓ | ✓* | ✓ |
param |
- | ✓ | ✓ | - | - |
approval |
✓ | - | ✓ | - | - |
approvalParam |
- | - | - | - | ✓ |
mailer |
✓ | ✓ | ✓ | ✓ | ✓ |
util |
✓ | ✓ | ✓ | ✓ | ✓ |
account |
✓ | ✓ | ✓ | ✓ | ✓ |
log |
✓ | ✓ | ✓ | ✓ | ✓ |
* In Daily workflows, user represents the system user dailyJob@ragic.com with admin privileges, not a real user.
Magic Variables
Only one magic variable is available:
__actionButtonExecuteNodeId- Record ID (Action Button workflow only)
For other contexts, use the provided objects:
- Record ID:
param.getRootNodeId()orentry.getRootNodeId() - User email:
user.getEmail()(in Daily workflow, returnsdailyJob@ragic.com) - Account name:
account.getApname()
Resource Limits
Workflows have built-in limits to prevent runaway processes:
| Resource | Limit |
|---|---|
| Database queries | 1,000,000 calls |
| Records created | 45,000 records |
| HTTP requests | 500 calls |
| File downloads | 100 files |
| Email sending | Varies by plan * |
| Query results | 5,000 per query |
* Email sending quota is determined by your subscription plan. Free plans allow 100 emails per day, while paid plans start at 1,000 emails per user per day. Please refer to your plan details for exact quota.
Workflow Types
1. Action Button Workflow
User-triggered scripts executed when clicking configured buttons in the Actions panel.
Setup: To write an action button script, right-click on a sheet and choose Javascript Workflow:

And choose installed sheet scope from the top dropdown:

You can then go to the form page design, add an Action Button of the type JS Workflow, and refer to the JavaScript function that you have written.

Note that you can pass the record id of the current record by using {id} in the argument for the function call like:
setStatus({id});
Please note that Action Buttons do not support setStatus('CONFIRM').
Use Cases:
- Generate reports on demand
- Send manual notifications
- Bulk operations on selected records
Available Objects: db, response, user, approval, mailer, util, account, log
// Action Button: Export record to external system
function setStatus(id) {
var recordId = id;
var query = db.getAPIQuery("/orders/1");
var entry = query.getAPIEntry(recordId);
var orderData = {
orderId: entry.getFieldValue(1000001),
customer: entry.getFieldValue(1000002),
total: entry.getFieldValue(1000003),
};
var result = util.postURL(
"https://api.external-system.com/orders",
JSON.stringify(orderData)
);
response.setStatus("SUCCESS");
response.setMessage("Order exported successfully");
}
2. Post-Workflow
Executes immediately after a record is saved. Useful for automated changes that formulas cannot handle.
Setup: To add a post-workflow, right-click on a sheet and choose Javascript Workflow:

And choose Post-workflow from the top dropdown. A typical post-workflow would look something like this:
var recordId = param.getNewNodeId(keyFieldId);
var query = db.getAPIQuery(pathToForm);
var record = query.getAPIEntry(recordId);
// do what you would like to do with the record retrieved
Please note that post-workflow will not be executed if you modify entries on the listing page.
Use Cases:
- Update related records
- Inventory adjustments
- Send notifications after save
- Create audit logs
Available Objects: db, response, user, param, approval, mailer, util, account, log
// Post-workflow: Update inventory after order
var entry = param.getUpdatedEntry();
var productId = entry.getFieldValue(1000010);
var quantity = parseInt(entry.getFieldValue(1000011));
// Get inventory record
var invQuery = db.getAPIQuery("/inventory/1");
invQuery.addFilter(1000001, "=", productId);
var invEntry = invQuery.getAPIResult();
if (invEntry != null) {
var currentStock = parseInt(invEntry.getFieldValue(1000002));
invEntry.setFieldValue(1000002, (currentStock - quantity).toString());
invEntry.save(); // Default behavior does not trigger workflow
}
3. Pre-Workflow
Runs before a record is saved, enabling validation. Can block saves using status codes. Generally, most validation can be done with front-end regular expression checks or the unique checkbox for free text fields. But for more complex backend checks, pre-workflow is needed.
Setup: To add a pre-workflow, right-click on a sheet and choose Javascript Workflow:

And choose Pre-workflow from the top dropdown. Here is a simple example: Suppose we have a list,

and we want to ensure the price we want to save is not negative.
/**
* Key Field: 1000004
*
* Field Name Field Id
* ----------- --------
* ID : 1000001
* Price : 1000002
* Name : 1000003
* Available? : 1000005
*/
function showMsg(str) {
response.setStatus("INVALID");
response.setMessage(str);
}
function ifThePriceIsNegative() {
var newPrice = param.getNewValue(1000002);
if (parseInt(newPrice) < 0) {
return true;
}
}
if (ifThePriceIsNegative()) {
showMsg("Price is negative !!");
}
And now we try to save a negative price, we would get:

Blocking a save: To prevent the record from being saved, call response.setStatus("INVALID") (or "ERROR"). When the status is set to either of these values, the save is blocked and the message from response.setMessage() is displayed to the user. If the pre-workflow finishes without setting one of these statuses, the save proceeds normally.
It is noteworthy that we couldn't use entry.getFieldValue while writing the pre-workflow in Ragic (since Ragic hasn't saved it). Try to use the param to get the old value and the new value instead.
Use Cases:
- Field validation
- Enforce business rules
- Auto-fill fields before save
- Prevent duplicate entries
Available Objects: db, response, user, param, mailer, util, account, log
Important: Cannot use entry.getFieldValue() - must use param.getNewValue()
// Pre-workflow: Validate order total
var total = parseFloat(param.getNewValue(1000003));
var creditLimit = parseFloat(param.getNewValue(1000004));
if (total > creditLimit) {
response.setStatus("INVALID");
response.setMessage("Order total exceeds credit limit");
}
4. Daily Workflow
Automatic daily execution. Default time is 19:00 in the account's configured timezone, configurable via Company Settings.
Setup: To add a daily workflow, right-click on a tab and choose Global Javascript Workflow:

And choose Daily Workflow from the top dropdown.

By default, the daily workflow runs at 19:00 in the account's configured timezone. You can change the Default Job Schedule Execution Time and Company Local Time Zone in the Company Settings. To set a different execution time specifically for Daily Workflows, adjust the settings directly in Job Schedules.
Click History to view the workflow modification history.

Use Cases:
- Generate daily reports
- Send reminder emails
- Data cleanup and archiving
- Recurring calculations
Available Objects: db, response, user, mailer, util, account, log
Note: The user object represents the system user dailyJob@ragic.com with admin privileges, not a real user.
// Daily workflow: Send overdue reminders
var query = db.getAPIQuery("/invoices/1");
var today = new Date();
var todayStr =
today.getFullYear() + "/" + (today.getMonth() + 1) + "/" + today.getDate();
query.addFilter(1000005, "<", todayStr); // Due date
query.addFilter(1000006, "=", "Unpaid"); // Status
var results = query.getAPIResultsFull();
while (results.hasNext()) {
var entry = results.next();
var customerEmail = entry.getFieldValue(1000002);
var invoiceNum = entry.getFieldValue(1000001);
mailer.compose(
customerEmail,
null,
"billing@company.com",
"Billing Department",
"Invoice " + invoiceNum + " is overdue",
"Please settle your outstanding invoice."
);
mailer.send();
}
5. Approval Workflow
Triggered when approvals are created, finished (fully approved), rejected, or canceled.
Setup: To add an approval workflow, right-click on a sheet and choose Javascript Workflow:

And choose Approval Workflow from the top dropdown.
Available Objects: db, response, user, approvalParam, mailer, util, account, log
Approval Actions:
CREATE- Approval workflow startedFINISH- All steps completed (fully approved)REJECT- Approval rejected at any stepCANCEL- Approval canceled
// Approval workflow: Notify on completion
var action = approvalParam.getApprovalAction();
var recordId = approvalParam.getEntryRootNodeId();
if (action === "FINISH") {
var query = db.getAPIQuery("/purchase-orders/1");
var entry = query.getAPIEntry(recordId);
var requester = entry.getFieldValue(1000001);
mailer.sendAppNotification(
requester,
"Your purchase order has been approved",
"/purchase-orders/1",
recordId
);
}
Handling All Approval Actions
approvalParam is a predefined variable in the approval workflow scope. You can get the record ID and approval action, then customize your requirements:
var action = approvalParam.getApprovalAction();
var recordId = approvalParam.getEntryRootNodeId();
var query = db.getAPIQuery(pathToForm);
var entry = query.getAPIEntry(recordId);
if (action === "CANCEL") {
entry.setFieldValue(STATUS_FIELD, "approval is canceled!");
} else if (action === "FINISH") {
entry.setFieldValue(STATUS_FIELD, "approval is completed!");
} else if (action === "CREATE") {
entry.setFieldValue(STATUS_FIELD, "approval is created!");
} else if (action === "REJECT") {
entry.setFieldValue(STATUS_FIELD, "approval is rejected!");
}
// Note: Only CREATE, FINISH, REJECT, and CANCEL are valid actions
entry.save();
6. Global Workflow
Reusable function repository shared across all workflows within the same account. The global workflow is evaluated when the script engine initializes, and functions and variables defined in it are available to any type of workflow listed above. It's a great place to put scripts that you might need to duplicate across multiple sheets otherwise.
Setup: To add a global workflow, right-click on a tab and choose Global Javascript Workflow:

Click History to view the workflow modification history.

How it works: The global workflow script is evaluated first when the script engine initializes, before any sheet scope, pre-workflow, or post-workflow code runs. Functions and variables defined in the global workflow are available to all subsequent workflow scripts in the same execution because they share the same JavaScript engine instance.
Evaluation order:
- Global workflow — evaluated once per engine initialization
- Sheet scope (per-sheet shared code) — evaluated before the specific workflow
- Pre-workflow / Post-workflow / Action Button / Approval Workflow / Daily Workflow — the actual workflow script
// Global workflow: Define shared functions
function formatCurrency(amount) {
return "$" + parseFloat(amount).toFixed(2);
}
function sendNotification(email, subject, message) {
mailer.compose(email, null, "system@company.com", "System", subject, message);
mailer.send();
}
// These functions are now available in all pre/post/action button workflows:
// var formatted = formatCurrency(total);
// sendNotification(email, "Order Confirmed", message);
Note: Global workflow functions share scope with all workflows. Avoid defining variables with common names (like
resultordata) at the top level, as they may conflict with variables in individual workflows.
API Reference
db Object
The main entry point for querying and manipulating records.
db.getAPIQuery(pathToForm)
Creates a query object for the specified form.
| Parameter | Type | Description |
|---|---|---|
| pathToForm | string | Form path (e.g., "/sales/1") |
Returns: Query object
var query = db.getAPIQuery("/sales/1");
db.getAPIQuery()
Creates a query object for the current form (the form where the workflow is attached).
Returns: Query object
var query = db.getAPIQuery();
db.getAPIQuery(path, sheetIndex)
Creates a query object by specifying path and sheet index separately. Useful for multi-level paths.
| Parameter | Type | Description |
|---|---|---|
| path | string | The tab path (e.g., "/sales/orders") |
| sheetIndex | number | Sheet index within the tab |
Returns: Query object
var query = db.getAPIQuery("/sales/orders", 1);
db.getApname()
Returns the current account (database) name.
Returns: string
db.getPath()
Returns the current form path.
Returns: string
db.getSheetIndex()
Returns the current sheet index.
Returns: number
db.deleteOldRecords(pathToForm, daysOld)
Deletes records older than the specified number of days (max 500 records per call). Compares against the record's creation date. For example, if today is 2025/07/10 and daysOld is 1, it will delete records created before 2025/07/09 00:00:00.
| Parameter | Type | Description |
|---|---|---|
| pathToForm | string | Form path |
| daysOld | number | Delete records older than this many days |
db.deleteOldRecords("/logs/1", 90); // Delete records older than 90 days
db.deleteOldRecords(pathToForm, daysOld, hasTime)
Same as above, but with time-level precision when hasTime is true. If hasTime is true, the comparison is based on the current time of execution. For example, if the execution time is 2025/07/10 21:00:00 and daysOld is 1, it will delete records created before 2025/07/09 21:00:00.
| Parameter | Type | Description |
|---|---|---|
| pathToForm | string | Form path |
| daysOld | number | Days threshold |
| hasTime | boolean | Include time in comparison |
db.recalculateAll(pathToForm)
Recalculates all formula fields for every record in the specified form.
| Parameter | Type | Description |
|---|---|---|
| pathToForm | string | Form path |
db.recalculateAll("/sales/orders/1");
db.recalculateAll(pathToForm, fieldId...)
Recalculates specific formula fields for every record.
| Parameter | Type | Description |
|---|---|---|
| pathToForm | string | Form path |
| fieldId... | number | One or more field IDs to recalculate |
db.recalculateAll("/sales/orders/1", 1000005, 1000006);
db.loadLinkAndLoadAll(pathToForm)
Synchronizes all Link and Load fields for every record in the specified form.
| Parameter | Type | Description |
|---|---|---|
| pathToForm | string | Form path |
db.loadLinkAndLoadAll("/sales/orders/1");
db.setLogRecalcCostTime(enable)
When set to true, displays the execution time in the "Sheet Workflow Formula Recalculation Execution Time Log", which records the time taken to recalculate a form using the db.recalculateAll() formula.
| Parameter | Type | Description |
|---|---|---|
| enable | boolean | true to enable logging |
query Object
query.getAPIResult()
Retrieves the first matching record.
Returns: Entry object or null
query.addFilter(1000001, "=", "Active");
var entry = query.getAPIResult();
query.getAPIEntry(recordId)
Retrieves a single record by ID.
| Parameter | Type | Description |
|---|---|---|
| recordId | number | The root node ID |
Returns: Entry object or null
var entry = query.getAPIEntry(12345);
query.getAPIResultsFull()
Retrieves multiple records matching filters. Returns an iterator.
Returns: Iterator with hasNext() and next() methods
var results = query.getAPIResultsFull();
while (results.hasNext()) {
var entry = results.next();
// Process entry
}
query.getAPIResultList()
Retrieves all records matching filters as an array. Unlike getAPIResultsFull() which returns an iterator, this method loads all matching records into memory at once and returns them as an array.
Note: Prefer
getAPIResultsFull()for most use cases — it processes records one at a time using an iterator, which is more memory-efficient. UsegetAPIResultList()only when you need random access to results (e.g., accessing by index) or need to iterate over the same results multiple times.
Returns: Entry[]
var entries = query.getAPIResultList();
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
// Process entry
}
query.addFilter(fieldId, operator, value)
Adds a filter condition.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
| operator | string | "=" or "eq", ">" or "gt", ">=" or "gte", "<" or "lt", "<=" or "lte", "like", "regex" |
| value | string | Value to compare |
query.addFilter(1000001, "=", "Active");
query.addFilter(1000002, ">=", "2024/01/01");
query.setFullTextSearch(searchText)
Performs full-text search across indexed fields.
| Parameter | Type | Description |
|---|---|---|
| searchText | string | Search term |
query.setFullTextSearch("important keyword");
query.insertAPIEntry()
Creates a new record.
Returns: New Entry object
var entry = query.insertAPIEntry();
entry.setFieldValue(1000001, "New Record");
entry.save();
query.deleteEntry(nodeId)
Permanently deletes a record.
| Parameter | Type | Description |
|---|---|---|
| nodeId | number | The root node ID of the record to delete |
query.deleteEntry(12345);
query.deleteEntryToRecycleBin(nodeId)
Moves a record to the recycle bin (soft delete).
| Parameter | Type | Description |
|---|---|---|
| nodeId | number | The root node ID of the record to move |
query.deleteEntryToRecycleBin(12345);
query.setUpdateMode()
Optimizes the query for update scenarios. This method:
- Bypasses field masking — returns real values instead of masked ones (e.g., full ID numbers instead of
A1234*****) - Skips full-text index string generation for better performance
Note:
setUpdateMode()is recommended when reading records that you plan to modify, especially if the form contains masked fields. However,setFieldValue()andsave()work regardless of whether this method is called.
query.setUpdateMode();
var entry = query.getAPIEntry(12345);
entry.setFieldValue(1000001, "Updated");
entry.save();
query.setOrder(fieldId, orderDir)
Sets sort order for results.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID to sort by |
| orderDir | number | 1=ascending, 2=descending, 3=secondary ascending, 4=secondary descending |
query.setOrder(1000001, 1); // Sort ascending by field 1000001
query.setLimitSize(size)
Sets maximum records to return per query. By default, queries return 1000 records. We do not recommend returning too many records per query because it may consume too much memory and have performance implications. Consider using setLimitFrom() for paging instead.
| Parameter | Type | Description |
|---|---|---|
| size | number | Maximum number of records to return |
query.setLimitSize(100);
query.setLimitFrom(offset)
Sets pagination offset. This method is for paging through all the records on a query. Setting a limitFrom will tell the query to start returning records from an offset. Since queries return 1000 records by default, you should check for the next page if the number of records returned equals the limit size.
| Parameter | Type | Description |
|---|---|---|
| offset | number | Number of records to skip |
query.setLimitFrom(100); // Skip first 100
query.setIfIgnoreFixedFilter(ignore)
When set to true, ignores fixed filters configured on the sheet.
| Parameter | Type | Description |
|---|---|---|
| ignore | boolean | true to bypass fixed filters |
Warning: Fixed filters enforce row-level access control. Bypassing them may expose records the current user should not see. Use only when the workflow needs to access all records regardless of user-facing filters, and ensure results are not returned directly to users.
query.setIgnoreAllMasks(ignore)
Bypasses field masking for all fields.
| Parameter | Type | Description |
|---|---|---|
| ignore | boolean | true to bypass all field masks |
query.addIgnoreMaskDomain(fieldId)
Bypasses field masking for a specific field.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID to unmask |
query.addIgnoreMaskDomain(1000005);
query.getKeyFieldId()
Returns the key field ID of the form.
Returns: number
query.getFieldIdByName(fieldName)
Looks up a field ID by its name. If multiple fields share the same name, returns the first standalone field ID.
| Parameter | Type | Description |
|---|---|---|
| fieldName | string | Field name |
Returns: number
var fieldId = query.getFieldIdByName("Customer Name");
query.setGetUserNameAsSelectUserValue(enable)
Controls whether Select User fields return the user's display name instead of email. By default (true), the query returns the user's display name for Select User fields. Set to false to retrieve the user's email instead.
| Parameter | Type | Description |
|---|---|---|
| enable | boolean | true to return display names (default), false to return emails |
query.setFieldValueModeAsNodeIdOnly(mode)
Sets the field value retrieval mode. When enabled (true), field values retrieved via the API will return the nodeId of the linked record instead of the displayed value. When disabled (false), the API will return the actual displayed value of the field (such as text, numbers, or dates).
| Parameter | Type | Description |
|---|---|---|
| mode | boolean | true to return nodeId only, false to return display values |
query.setFieldValueModeAsNodeIdAndValue(mode)
Configures how field values are retrieved when loading default values. When enabled (true), field values retrieved through the API will include both the related record's nodeId and the display value. During default value loading, the system can then determine whether a field should load its default value based on the presence of a nodeId, rather than relying on whether the field's display value is empty. When disabled (false), only the actual field value (such as text, numbers, or dates) is returned.
| Parameter | Type | Description |
|---|---|---|
| mode | boolean | true to include both nodeId and display value |
If you find that field default values do not behave the same as on the form page when calling loadAllDefaultValues or loadDefaultValue, you can use this method to change how field values are evaluated.
query.isFieldValueModeIsNodeIdOnly()
Checks whether the current field value retrieval mode is set to nodeId only.
Returns: boolean
query.setIfIncludeInfo(include)
When true, includes full record metadata (create date, create user, approval status, etc.) in query results.
| Parameter | Type | Description |
|---|---|---|
| include | boolean | true to include record metadata |
query.setIfIncludeInfo(true);
var entry = query.getAPIEntry(recordId);
var createDate = entry.getFieldValue('_create_date');
entry Object
entry.getRootNodeId()
Gets the record's root node ID.
Returns: number (-1 for unsaved new entries)
entry.getFieldValue(fieldId)
Gets a field value. Does not include subtable fields; use getSubtableFieldValue() for subtable data.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
Returns: string or null
var name = entry.getFieldValue(1000001);
entry.getFieldValueByName(fieldName)
Gets a field value by field name. Does not include subtable fields. If multiple fields share the same name, returns the value of the first field.
| Parameter | Type | Description |
|---|---|---|
| fieldName | string | Field name |
Returns: string or null
var name = entry.getFieldValueByName("Customer Name");
entry.getFieldValues(fieldId)
Gets values from a multiple-select field as an array. Does not include subtable fields.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
Returns: string[]
var tags = entry.getFieldValues(1000003);
entry.setFieldValue(fieldId, value)
Sets a field value. For subtable fields, use setSubtableFieldValue() instead.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
| value | string | Value to set |
entry.setFieldValue(1000001, "New Value");
entry.setFieldValue(fieldId, value, appendValue)
Sets a field value. When appendValue is true, appends to multi-select fields instead of replacing.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
| value | string | Value to set |
| appendValue | boolean | true to append, false to replace |
entry.setFieldValue(1000003, "NewTag", true); // Append to multi-select
entry.setFieldValueByName(fieldName, value)
Sets a field value by field name.
| Parameter | Type | Description |
|---|---|---|
| fieldName | string | Field name |
| value | string | Value to set |
entry.setFieldValueByName("Status", "Completed");
entry.removeFieldValue(fieldId, value)
Removes a specific value from a multiple-selection field.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
| value | string | The value to remove |
entry.getFieldValueFormatted(fieldId)
Gets the formatted display value of a field (e.g., with number formatting applied).
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
Returns: string or null
entry.getFieldIdByName(fieldName)
Looks up a field ID by its name. If multiple fields share the same name, returns the first field ID.
| Parameter | Type | Description |
|---|---|---|
| fieldName | string | Field name |
Returns: number
entry.getOriginalCreateUser()
Gets the email address of the user who originally created this record.
Note: This method requires
query.setUpdateMode()orquery.setIfIncludeInfo(true)to be called before retrieving the entry. Otherwise, the create user information is not loaded and this method returns an empty string.
Returns: string
entry.extractTextFromFileUploadField(fieldId)
Extracts text content from a file upload field (e.g., PDF, DOCX).
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | File upload field ID |
Returns: string
entry.setSendNotification(send)
Controls whether saving this entry triggers email notifications.
| Parameter | Type | Description |
|---|---|---|
| send | boolean | true to send notifications |
entry.setFieldFile(fieldId, fileName, fileContent)
Creates a file with the given fileName and fileContent, and saves it to the specified file upload field so that the file is available for download on the user interface. This method works for both file upload and graphic fields. For subtable fields, use setSubtableFieldFile() instead.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | File upload field ID |
| fileName | string | File name |
| fileContent | string | File content |
entry.setFieldFile(fieldId, fileName, fileContent, appendValue)
Creates or appends a file upload.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | File upload field ID |
| fileName | string | File name |
| fileContent | string | File content |
| appendValue | boolean | true to append file |
entry.save()
Saves all changes.
Controlling Workflow Execution
Default behavior: Entries do not trigger workflows when saved (default value is false).
Triggering workflows: To trigger workflows on other forms, use setIfExecuteWorkflow(true):
// Trigger post-workflow when saving
entry.setIfExecuteWorkflow(true);
entry.save();
Important restriction: In post-workflows, attempting to set true for the same form that triggered the workflow will throw an exception to prevent infinite loops.
// Within a post-workflow
var entry = param.getUpdatedEntry();
entry.setIfExecuteWorkflow(true); // ❌ Throws: Workflow execution is denied
entry.save();
entry.getKeyFieldId()
Returns the key field ID of the form.
Returns: number
entry.recalculateAllFormulas()
Recalculates all formulas in the entry before saving.
entry.recalculateFormula(fieldId)
Recalculates a specific formula field.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID to recalculate |
Note: If two or more fields share the same fieldId, please use
recalculateFormula(fieldId, cellName)instead.
entry.recalculateFormula(fieldId, cellName)
Recalculates a formula by cell location (such as A1, C2, H21, etc). This method is used on sheets with two or more fields that share the same fieldId.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
| cellName | string | Cell location (e.g., "A1", "C2", "H21") |
entry.lock()
Locks the entry to prevent other users from editing.
entry.unlock()
Unlocks the entry.
entry.isLocked()
Checks if the entry is locked.
Returns: boolean
entry.setCreateHistory(enable)
Controls whether saving creates a history record.
| Parameter | Type | Description |
|---|---|---|
| enable | boolean | true to create history records |
Warning: Setting this to
falsedisables the audit trail for the save operation. Use only when bulk-updating records where history would create excessive noise. Never disable history to hide changes from users.
entry.isCreateHistory()
Checks whether history creation is enabled.
Returns: boolean
entry.setIgnoreEmptyCheck(ignore)
Skips required field validation when saving.
| Parameter | Type | Description |
|---|---|---|
| ignore | boolean | true to skip required field validation |
Warning: This bypasses required field checks, which may result in incomplete records. Use only when programmatically creating or updating records where certain required fields are intentionally left empty.
entry.setRecalParentFormula(recalculate)
If this form was created from another form's subtable, or is referenced by another form (i.e., it has a parent form), this method controls whether to recalculate the parent form's formulas after save.
| Parameter | Type | Description |
|---|---|---|
| recalculate | boolean | true to recalculate parent form formulas after save |
entry.setIfDoLnls(enable)
When true, syncs Link and Load on the source sheet after save.
| Parameter | Type | Description |
|---|---|---|
| enable | boolean | true to sync Link and Load after save |
You should always call this method on the source sheet to correctly trigger sync on other sheets, and make sure that "Keep loaded value sync with source" is enabled in the corresponding Link and Load configuration (see documentation).
Important: This method is only supported for action buttons. If you need to sync Link and Load fields in a post-workflow, you can directly retrieve the entry that needs to be updated and use
loadAllLinkAndLoad().
entry.loadAllLinkAndLoad()
Loads all Link and Load field values.
entry.loadLinkAndLoad(linkFieldId)
Loads a specific Link and Load field.
| Parameter | Type | Description |
|---|---|---|
| linkFieldId | number | Link field ID |
entry.loadLinkAndLoadField(loadFieldId)
Loads a single loaded field value.
| Parameter | Type | Description |
|---|---|---|
| loadFieldId | number | Load field ID |
entry.loadAllDefaultValues(user)
Loads all default values for the given user.
| Parameter | Type | Description |
|---|---|---|
| user | user | User object |
entry.loadDefaultValue(fieldId, user)
Loads the default value for a specific field.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
| user | user | User object |
entry.getSubtableSize(subtableId)
Get the number of rows in a subtable.
| Parameter | Type | Description |
|---|---|---|
| subtableId | number | Subtable ID |
Returns: number
entry.getSubtableFieldValue(subtableId, rowIndex, fieldId)
Read a value from a specific row and field in a subtable.
| Parameter | Type | Description |
|---|---|---|
| subtableId | number | Subtable ID |
| rowIndex | number | Row index (0-based) |
| fieldId | string | Subtable field ID (as string) |
Returns: string or null
var value = entry.getSubtableFieldValue(1000100, 0, "1000101");
entry.getSubtableFieldValues(subtableId, rowIndex, fieldId)
Get all values from a multi-select field in a subtable.
| Parameter | Type | Description |
|---|---|---|
| subtableId | number | Subtable ID |
| rowIndex | number | Row index (0-based) |
| fieldId | string | Subtable field ID (as string) |
Returns: string[]
entry.getSubtableRootNodeId(subtableId, rowIndex)
Get the node ID of a subtable row.
| Parameter | Type | Description |
|---|---|---|
| subtableId | number | Subtable ID |
| rowIndex | number | Row index (0-based) |
Returns: number
entry.setSubtableFieldValue(fieldId, rowNodeId, value)
Set a value for a specific field and row in a subtable.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Subtable field ID |
| rowNodeId | number | Row node ID |
| value | string | Value to set |
entry.setSubtableFieldValue(fieldId, rowNodeId, value, appendValue)
Set or append a value for a subtable field. When appendValue is true, appends to a multi-select field instead of replacing.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Subtable field ID |
| rowNodeId | number | Row node ID |
| value | string | Value to set |
| appendValue | boolean | true to append, false to replace |
entry.setSubtableFieldFile(fieldId, rowNodeId, fileName, fileContent)
Upload a file to a subtable file upload field.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Subtable field ID |
| rowNodeId | number | Row node ID |
| fileName | string | File name |
| fileContent | string | File content |
entry.setSubtableFieldFile(fieldId, rowNodeId, fileName, fileContent, appendValue)
Upload or append a file to a subtable file upload field.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Subtable field ID |
| rowNodeId | number | Row node ID |
| fileName | string | File name |
| fileContent | string | File content |
| appendValue | boolean | true to append, false to replace |
entry.deleteSubtableRow(subtableId, rowNodeId)
Delete a subtable row by node ID.
| Parameter | Type | Description |
|---|---|---|
| subtableId | number | Subtable ID |
| rowNodeId | number | Row node ID |
entry.deleteSubtableRowByRowNumber(subtableId, rowIndex)
Delete a subtable row by index.
| Parameter | Type | Description |
|---|---|---|
| subtableId | number | Subtable ID |
| rowIndex | number | Row index (0-based) |
entry.deleteSubtableRowAll(subtableId)
Delete all rows in a subtable.
| Parameter | Type | Description |
|---|---|---|
| subtableId | number | Subtable ID |
response Object
response.setStatus(status)
Sets workflow result status.
| Parameter | Type | Description |
|---|---|---|
| status | string | Status code (see values below) |
| Status | Description |
|---|---|
| SUCCESS | Completed successfully |
| DEBUG | Debug mode |
| WARN | Warning (allows continuation) |
| CONFIRM | Requires confirmation |
| INVALID | Validation failed (blocks save) |
| ERROR | Error (blocks save) |
response.getStatus()
Gets the current status.
Returns: string
response.setMessage(message)
Sets the message to display. Can be called multiple times to stack messages.
| Parameter | Type | Description |
|---|---|---|
| message | string | Message text to display |
Note: Messages are accumulated in order during execution and displayed all at once after the workflow finishes. They are not shown in real time like
console.login a browser — the user sees all messages together when the workflow completes.
response.setStatus("WARN");
response.setMessage("Some fields are incomplete.");
response.setMessage("Please review before submitting.");
// Both messages appear together after the workflow finishes
response.numOfMessages()
Returns the number of stacked messages.
Returns: number
response.setOpenURL(url)
Redirects to a URL after the workflow completes.
| Parameter | Type | Description |
|---|---|---|
| url | string | URL to open |
response.setOpenURL("https://www.example.com/result");
response.addOpenURL(url)
Adds an additional URL to open. Can be called multiple times to open multiple URLs.
| Parameter | Type | Description |
|---|---|---|
| url | string | URL to open |
response.setOpenURLInNewTab(openInNewTab)
Controls whether the URL opens in a new tab (default: true).
| Parameter | Type | Description |
|---|---|---|
| openInNewTab | boolean | true to open in new tab (default: true) |
response.getRootNodeId()
Gets the entry's root node ID after the operation.
Returns: number
user Object
user.getEmail()
Gets user's email address.
user.getUserName()
Gets user's display name.
user.getGroups()
Gets list of user's groups.
Returns: List of strings (use .size() for count, .get(i) or [i] for indexing)
user.isInGroup(groupName)
Checks group membership.
| Parameter | Type | Description |
|---|---|---|
| groupName | string | Group name to check |
if (user.isInGroup("Managers")) {
// Allow action
}
user.isValid()
Checks if the user's account is not expired.
Returns: boolean
user.isIdentifiable()
Checks if the user is not a guest or anonymous.
Returns: boolean
param Object (Pre/Post-Workflow)
param.getNewValue(fieldId)
Gets submitted field value.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
param.getOldValue(fieldId)
Gets previous field value.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
param.getNewValues(fieldId)
Gets submitted values for multi-select fields.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
Returns: List of strings (use .size() for count, [i] for indexing). Can return null.
param.getOldValues(fieldId)
Gets previous values for multi-select fields.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
Returns: List of strings (use .size() for count, [i] for indexing). Can return null.
param.getNewNodeId(fieldId)
Gets the node ID of the new value.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
Returns: number
param.getOldNodeId(fieldId)
Gets the node ID of the old value.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
Returns: number
param.getNewNodeIds(fieldId)
Gets node IDs of new values (for multi-select).
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
Returns: List of numbers (use .size() for count, [i] for indexing). Can return null.
param.getOldNodeIds(fieldId)
Gets node IDs of old values (for multi-select).
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
Returns: List of numbers (use .size() for count, [i] for indexing). Can return null.
param.getOperationType(fieldId)
Gets the operation type for a field.
| Parameter | Type | Description |
|---|---|---|
| fieldId | number | Field ID |
Returns: "I" (insert), "M" (modify), or "L" (link — multi-selection change on list page, pre-workflow only)
param.isCreateNew()
Checks if creating new record.
param.getRootNodeId()
Gets the root node ID of the record (-1 for new records).
Returns: number
param.getUpdatedEntry()
Gets the saved entry (Post-workflow only).
param.getEntry()
Alias for getUpdatedEntry() (Post-workflow only).
param.getSubtableEntry(subtableId)
Gets subtable row parameters.
| Parameter | Type | Description |
|---|---|---|
| subtableId | number | Subtable ID |
var rows = param.getSubtableEntry(1000100);
for (var i = 0; i < rows.size(); i++) {
var qty = rows[i].getNewValue(1000101);
}
mailer Object
mailer.compose(to, cc, from, fromPersonal, subject, content)
Composes an email. Both the to and cc parameters accept multiple email addresses separated by commas.
| Parameter | Type | Description |
|---|---|---|
| to | string | Recipient email address(es), comma-separated |
| cc | string | CC recipients, comma-separated |
| from | string | Sender email address |
| fromPersonal | string | Sender display name |
| subject | string | Email subject line |
| content | string | Email body (HTML supported) |
mailer.compose(
"recipient@example.com",
null,
"sender@example.com",
"Sender Name",
"Subject",
"<p>HTML content</p>"
);
mailer.attach(url)
Attaches a file (must be https URL). You can attach Ragic records by using the record's URL in PDF, Excel, or Mail Merge format. See Email Attachment Formats in Common Patterns for URL format details.
| Parameter | Type | Description |
|---|---|---|
| url | string | URL of the file to attach |
mailer.setBcc(bcc)
Sets BCC recipients (comma-separated).
| Parameter | Type | Description |
|---|---|---|
| bcc | string | BCC email addresses, comma-separated |
mailer.send()
Sends the composed email. This method calls sendAsync() internally — both behave identically (asynchronous).
Note: We do enforce some limitations on how many emails you can send. If you have questions about email sending quota, send us an email at support@ragic.com.
mailer.sendAsync()
Sends the composed email asynchronously. Functionally identical to send().
mailer.setSendRaw(sendRaw)
When true, sends without Ragic HTML template wrapping. Set this to true when your content is already in HTML.
| Parameter | Type | Description |
|---|---|---|
| sendRaw | boolean | true to send without Ragic template |
mailer.sendMail(to, subject, content)
One-call shorthand to send an email.
| Parameter | Type | Description |
|---|---|---|
| to | string | Recipient email address |
| subject | string | Email subject line |
| content | string | Email body (HTML supported) |
mailer.sendMail("recipient@example.com", "Subject", "<p>Content</p>");
mailer.sendMail(to, subject, content, attachmentURL)
One-call send with file attachment.
| Parameter | Type | Description |
|---|---|---|
| to | string | Recipient email address |
| subject | string | Email subject line |
| content | string | Email body (HTML supported) |
| attachmentURL | string | URL of file to attach |
mailer.sendAppNotification(email, message)
Sends a mobile app notification to the user if they have the Ragic iOS or Android app installed.
| Parameter | Type | Description |
|---|---|---|
| string | Recipient's email address | |
| message | string | Notification message |
mailer.sendAppNotification(email, message, pathToForm, nodeId)
Sends a mobile app notification to the user if they have the Ragic iOS or Android app installed. When the user taps the notification, they will be redirected to the specified record. pathToForm should be in the format "/forms/1" (tab folder and sheet index, not including the account name).
| Parameter | Type | Description |
|---|---|---|
| string | Recipient's email address | |
| message | string | Notification message |
| pathToForm | string | Form path (e.g., "/forms/1") |
| nodeId | number | Record ID to link to |
util Object
util.getURL(url)
Send an HTTP GET request. Returns the response body as a string.
| Parameter | Type | Description |
|---|---|---|
| url | string | Request URL |
Returns: string — the response body
var result = util.getURL("https://api.example.com/data");
log.info("Response: " + result);
// Parse JSON response
var data = JSON.parse(result);
log.info("Name: " + data.name);
util.postURL(url, body)
Send an HTTP POST request. Returns the response body as a string.
| Parameter | Type | Description |
|---|---|---|
| url | string | Request URL |
| body | string | Request body |
Returns: string — the response body
util.setHeader("Content-Type", "application/json");
var payload = JSON.stringify({ name: "Order #123", status: "confirmed" });
var result = util.postURL("https://api.example.com/orders", payload);
log.info("Response: " + result);
util.putURL(url, body)
Send an HTTP PUT request.
| Parameter | Type | Description |
|---|---|---|
| url | string | Request URL |
| body | string | Request body |
Returns: string
util.deleteURL(url)
Send an HTTP DELETE request.
| Parameter | Type | Description |
|---|---|---|
| url | string | Request URL |
Returns: string
util.setHeader(name, value)
Set a custom header for subsequent HTTP requests.
| Parameter | Type | Description |
|---|---|---|
| name | string | Header name |
| value | string | Header value |
util.setHeader("Authorization", "Bearer token");
util.setHeader("Content-Type", "application/json");
var result = util.postURL(url, body);
util.removeHeader(name)
Remove a previously set custom header.
| Parameter | Type | Description |
|---|---|---|
| name | string | Header to remove |
util.downloadFile(fileUrl)
Downloads the file returned by fileUrl to the upload folder in the database.
| Parameter | Type | Description |
|---|---|---|
| fileUrl | string | URL of the file to download |
Returns: string — The filename of the downloaded file, stored in the account's upload directory. Can be passed directly to entry.setFieldValue() to attach to a file upload field.
util.downloadFile(fileUrl, postBody)
Downloads a file via POST request to the upload folder in the database.
| Parameter | Type | Description |
|---|---|---|
| fileUrl | string | URL of the file to download |
| postBody | string | POST request body |
Returns: string — The filename of the downloaded file, stored in the account's upload directory. Can be passed directly to entry.setFieldValue() to attach to a file upload field.
util.postFile(sourceFileUrl, destinationUrl)
Downloads a file from the source URL to the account's upload directory, then uploads it to the destination URL.
| Parameter | Type | Description |
|---|---|---|
| sourceFileUrl | string | Source file URL |
| destinationUrl | string | Destination URL to upload to |
Returns: string — The filename of the downloaded file in the account's upload directory
util.getDayOfMonth()
Get the day of the month (1–31).
Returns: number
util.getDayOfWeek()
Get the day of the week (1=Sunday, 2=Monday, ..., 7=Saturday).
Returns: number
util.getMonthOfYear()
Get the month of the year (0–11, 0=January).
Returns: number
util.getYear()
Get the current year.
Returns: number
util.ignoreSSL()
Disable SSL certificate verification for subsequent HTTP requests.
util.resetSSL()
Re-enable SSL certificate verification.
util.logWorkflowError(text)
Records a log message in the workflow log, which can be found on the database maintenance page.
| Parameter | Type | Description |
|---|---|---|
| text | string | Log message text |
util.logWorkflowError("Order processing failed for record " + recordId);
approval Object
approval.create(signersJson)
Post-workflow and Action Button only. Creates approval workflow. signersJson is a JSON string of an array containing objects with the following format:
| Field | Description |
|---|---|
| stepIndex | Step of the approval, starting from 0 |
| approver | Approver's email address |
| stepName | Approver's name or title |
Returns: boolean — true if approval was created successfully
var signers = [
{ stepIndex: 0, approver: "manager@example.com", stepName: "Manager" }
];
approval.create(JSON.stringify(signers));
approval.cancel()
Post-workflow and Action Button only. Cancels current approval.
Returns: boolean
Special Approver Formats
You can use these special formats for deciding approvers dynamically according to your company tree:
$DS- The approver will be set to the user's direct supervisor$SS- The approver will be set to the user's supervisor's supervisor$DSL- The approver will be set as the supervisor of the previous approver
signers.push({
'stepIndex': '1',
'approver': '$DSL',
'stepName': ''
});
You don't have to provide a non-empty stepName while using these special formats. Note: If the user in a special format doesn't follow the rule you set in the design mode, this approver will not be created.
Creating and Cancelling Approval
You can start or cancel a record's approval by adding the post-workflow script to your sheet. First, set the approval step in design mode:

And now set the approval detail in the post workflow:
function autoStartApprover() {
var signers = [];
signers.push({
'stepIndex': '0',
'approver': 'kingjo@ragic.com',
'stepName': 'START'
});
signers.push({
'stepIndex': '1',
'approver': 'HR0000@hr.hr',
'stepName': 'HR'
});
approval.create(JSON.stringify(signers));
}
autoStartApprover();
Result:

Your approval would not be created if there was something wrong in the parameter format or if the approvers you provided didn't follow the rule you set in the design mode. For example, if the second step in design mode only has "HR00@gmail.com" as a candidate, providing a different email like "kingjo@ragic.com" for that step would cause the approval creation to fail. We also provide 3 special formats for deciding your approvers dynamically according to your company tree. See Approval Flow Configuration for further information.
Record Info Fields
You can include full record info by issuing the following command on the query object:
query.setIfIncludeInfo(true);
Then you can get the approval information and other metadata for a record:
entry.getFieldValue('_approve_status'); // Getting status of current approval
entry.getFieldValue('_approve_next'); // Getting the next person who should sign this record
entry.getFieldValue('_create_date'); // Getting the create date of the record
entry.getFieldValue('_create_user'); // Getting the create user email of the record
The approval status values are: F for approved, REJ for rejected, and P for processing.
approvalParam Object (Approval Workflow)
approvalParam.getEntryRootNodeId()
Gets the record ID being approved.
Returns: number
approvalParam.getApprovalAction()
Gets the approval action that triggered the workflow.
Returns: "CREATE", "FINISH", "REJECT", or "CANCEL"
account Object
account.getApname()
Gets the current account (database) name.
Returns: string
account.getUserName(email)
Gets a user's display name by email.
| Parameter | Type | Description |
|---|---|---|
| string | User's email address |
Returns: string (returns the email itself if not found)
account.getUserEmail(userName)
Gets a user's email by display name.
| Parameter | Type | Description |
|---|---|---|
| userName | string | User's display name |
Returns: string or null
account.getSheetPathByName(sheetName)
Looks up a sheet's full path by its name.
| Parameter | Type | Description |
|---|---|---|
| sheetName | string | Sheet name |
Returns: string or null
account.ifFree()
Checks if the account is on a free plan.
Returns: boolean
account.getTimeZoneOffset()
Gets the account's timezone offset in milliseconds.
Returns: number
account.getTimeZoneOffsetInHours()
Gets the account's timezone offset in hours.
Returns: number
account.reset()
Purges all account-related cache and reloads the page when the script finishes executing.
Warning: This invalidates all account-wide caches. Frequent calls can cause significant performance degradation for all users of the account. Use sparingly and only when cache invalidation is truly needed (e.g., after modifying form design programmatically).
log Object
The log object records messages during workflow execution. You can view these logs in the Workflow Execution Log, accessible from the top-left hamburger menu under Database Maintenance.

log.debug(message)
Log a debug-level message.
| Parameter | Type | Description |
|---|---|---|
| message | string | Log message |
log.info(message)
Log an info-level message.
| Parameter | Type | Description |
|---|---|---|
| message | string | Log message |
log.warn(message)
Log a warning-level message.
| Parameter | Type | Description |
|---|---|---|
| message | string | Log message |
log.error(message)
Log an error-level message.
| Parameter | Type | Description |
|---|---|---|
| message | string | Log message |
log.print(message)
Write a message to the console output.
| Parameter | Type | Description |
|---|---|---|
| message | string | Output message |
log.println(message)
Write a message to the console output with a line break.
| Parameter | Type | Description |
|---|---|---|
| message | string | Output message |
log.setToConsole(enable)
When set to true, shows a console block in the UI for viewing log output in real time.
| Parameter | Type | Description |
|---|---|---|
| enable | boolean | true to show console block in UI |
log.setToConsole(true);
log.info("This message will appear in the console block");
Workflow Cheat Sheet
Data Operations
Get Records
// Single record
var query = db.getAPIQuery("/path/1");
var entry = query.getAPIEntry(recordId);
// First matching record
query.addFilter(1000001, "=", "value");
var entry = query.getAPIResult();
// Multiple records (iterator)
var results = query.getAPIResultsFull();
while (results.hasNext()) {
var entry = results.next();
}
// As array (being phased out — prefer getAPIResultsFull)
var entries = query.getAPIResultList(); // entries[0], entries[1], entries.length
// Current form (no arguments)
var query = db.getAPIQuery();
// Multi-level path (path + sheet index)
var query = db.getAPIQuery("/sales/orders", 1);
Update Records
var entry = query.getAPIEntry(recordId);
entry.setFieldValue(1000001, "new value");
entry.save(); // Does not trigger workflow by default
// To trigger workflow for this entry:
// entry.setIfExecuteWorkflow(true);
// entry.save();
Create Records
var query = db.getAPIQuery("/path/1");
var entry = query.insertAPIEntry();
entry.setFieldValue(1000001, "value");
entry.save();
var newId = entry.getRootNodeId();
Delete Records
query.deleteEntry(nodeId); // Permanent
query.deleteEntryToRecycleBin(nodeId); // Soft delete
db.deleteOldRecords("/logs/1", 90); // Bulk delete by age
Filter & Sort
| Operator | Description | Example |
|---|---|---|
| = or eq | Exact match | addFilter(1000001, "=", "Active") |
| like | Contains | addFilter(1000001, "like", "urgent") |
| > or gt | Greater than | addFilter(1000002, ">", "100") |
| >= or gte | Greater or equal | addFilter(1000002, ">=", "2024/01/01") |
| < or lt | Less than | addFilter(1000002, "<", "2024/12/31") |
| <= or lte | Less or equal | addFilter(1000003, "<=", "1000") |
| regex | Regular expression | addFilter(1000001, "regex", "^A.*") |
// Sort: 1=asc, 2=desc, 3=secondary asc, 4=secondary desc
query.setOrder(1000001, 1);
// Full-text search
query.setFullTextSearch("keyword");
// Pagination
query.setLimitSize(100);
query.setLimitFrom(200);
// Lookup field ID by name
var fieldId = query.getFieldIdByName("Customer Name");
Fields & Subtables
Field Access by Name
var value = entry.getFieldValueByName("Customer Name");
entry.setFieldValueByName("Status", "Completed");
Multi-Select Fields
// Read all values
var tags = entry.getFieldValues(1000003);
// Append to multi-select (3rd arg = true)
entry.setFieldValue(1000003, "NewTag", true);
// Read in param
var newValues = param.getNewValues(1000003);
var oldValues = param.getOldValues(1000003);
File Upload Fields
entry.setFieldFile(1000005, "report.pdf", fileContent);
entry.setFieldFile(1000005, "extra.pdf", fileContent, true); // Append
Subtables
// Read
var count = entry.getSubtableSize(1000100);
var value = entry.getSubtableFieldValue(1000100, rowIndex, "1000101");
// Write existing row
var rowId = entry.getSubtableRootNodeId(1000100, rowIndex);
entry.setSubtableFieldValue(1000101, rowId, "new value");
// Append to multi-select in subtable
entry.setSubtableFieldValue(1000101, rowId, "value", true);
// File upload in subtable
entry.setSubtableFieldFile(1000101, rowId, "file.txt", content);
// Add new row (negative ID)
entry.setSubtableFieldValue(1000101, -100, "value1");
entry.setSubtableFieldValue(1000102, -100, "value2");
// Delete
entry.deleteSubtableRow(1000100, rowId); // By node ID
entry.deleteSubtableRowByRowNumber(1000100, 0); // By index
entry.deleteSubtableRowAll(1000100); // All rows
Record Locking
entry.lock();
entry.unlock();
var locked = entry.isLocked();
Link and Load
entry.loadAllLinkAndLoad(); // All fields
entry.loadLinkAndLoad(1000010); // Specific link field
entry.loadLinkAndLoadField(1000011); // Specific loaded field
db.loadLinkAndLoadAll("/path/1"); // All records in form
Recalculate Formulas
entry.recalculateAllFormulas(); // All formulas
entry.recalculateFormula(1000005); // Specific field
db.recalculateAll("/path/1"); // All records
db.recalculateAll("/path/1", 1000005, 1000006); // Specific fields
Workflow Context
Pre-workflow (param)
var newValue = param.getNewValue(1000001);
var oldValue = param.getOldValue(1000001);
var isNew = param.isCreateNew();
var recordId = param.getRootNodeId();
var opType = param.getOperationType(1000001); // "I" or "M"
// Node IDs
var newNodeId = param.getNewNodeId(1000001);
var oldNodeId = param.getOldNodeId(1000001);
// Subtable changes
var rows = param.getSubtableEntry(1000100);
Post-workflow
var entry = param.getUpdatedEntry(); // or param.getEntry()
var recordId = entry.getRootNodeId();
Response Status
response.setStatus("SUCCESS"); // Green checkmark
response.setStatus("WARN"); // Yellow warning
response.setStatus("INVALID"); // Block save
response.setStatus("ERROR"); // Block save
response.setMessage("Your message here");
// Redirect after completion
response.setOpenURL("https://example.com/result");
response.setOpenURLInNewTab(false); // Same tab
Communication
mailer.compose(to, cc, from, fromName, subject, htmlContent);
mailer.setBcc("bcc@example.com");
mailer.attach(url);
mailer.send();
mailer.sendAsync(); // Non-blocking send
// Simple one-line send
mailer.sendMail(to, subject, content);
mailer.sendMail(to, subject, content, attachmentURL);
// App notification
mailer.sendAppNotification(email, message);
mailer.sendAppNotification(email, message, "/path/1", nodeId);
HTTP Requests
var result = util.getURL(url);
var result = util.postURL(url, body);
var result = util.putURL(url, body);
var result = util.deleteURL(url);
// With custom headers
util.setHeader("Authorization", "Bearer token");
util.setHeader("Content-Type", "application/json");
var result = util.postURL(url, body);
util.removeHeader("Authorization");
// File download
var ragicUrl = util.downloadFile(externalUrl);
var ragicUrl = util.downloadFile(externalUrl, postBody);
System & Utilities
Approval
var signers = [
{stepIndex: 0, approver: "email@example.com", stepName: "Step Name"}
];
approval.create(JSON.stringify(signers));
approval.cancel();
// In approval workflow
var action = approvalParam.getApprovalAction(); // CREATE, FINISH, REJECT, CANCEL
var recordId = approvalParam.getEntryRootNodeId();
Account Info
var apname = account.getApname();
var displayName = account.getUserName("user@example.com");
var email = account.getUserEmail("Display Name");
var sheetPath = account.getSheetPathByName("Sheet Name");
var isFree = account.ifFree();
User Info
var email = user.getEmail();
var name = user.getUserName();
var groups = user.getGroups();
if (user.isInGroup("Managers")) { }
if (user.isValid()) { } // Not expired
if (user.isIdentifiable()) { } // Not guest/anonymous
Logging
log.debug("Debug info");
log.info("Information");
log.warn("Warning");
log.error("Error");
log.setToConsole(true); // Show console block in UI
Date Format
Always use: yyyy/MM/dd or yyyy/MM/dd HH:mm:ss
entry.setFieldValue(1000001, "2024/03/15");
entry.setFieldValue(1000001, "2024/03/15 14:30:00");
Magic Variables
| Variable | Description | Workflow Type |
|---|---|---|
__actionButtonExecuteNodeId |
Record ID | Action Button only |
Note: For other contexts, use the available objects:
- Record ID:
param.getRootNodeId()(pre/post) orentry.getRootNodeId() - User email:
user.getEmail()(in Daily workflow, returnsdailyJob@ragic.com) - Account name:
db.getApname()oraccount.getApname()
Common Patterns
Retrieving a Record
There are two different ways to get the current record that the user is viewing (for action buttons), the values being submitted (for pre-workflow), or the record that was just saved (for post-workflow).
Action Button
The form page action button works by configuring an action button to call a function you defined in the installed sheet scope. When you define the function, you can pass a parameter {id} which is the ID of the record where you click the action button. You can see the example at Action Button Workflow.
Pre-workflow
You can get the record ID by:
var recordId = param.getNewNodeId(keyFieldId);
param is a predefined variable, you can use it in pre-workflow and post-workflow. After you get the record ID, this is how you get the record (entry):
var query = db.getAPIQuery(pathToForm);
var entry = query.getAPIEntry(recordId);
Post-workflow
For post-workflow, you can use either the above method just like with pre-workflow. But also there is a convenient method to retrieve the record that was just saved:
var entry = param.getUpdatedEntry();
Handling Masked Fields
If there are masked fields on the form page, you will get masked values by default. You can get unmasked values by:
var query = db.getAPIQuery(pathToForm);
query.setUpdateMode();
query.setIgnoreAllMasks(true); // Unmask all masked fields
var entry = query.getAPIEntry(recordId);
You can use query.addIgnoreMaskDomain(fieldId) instead of query.setIgnoreAllMasks(true) to unmask only specific fields.
Querying for a Set of Records
If you want to get more than one record by filtering, you can add filters to query by addFilter(fieldId, operator, value), and call getAPIResultsFull() to get the list of records. Here's the list of operands:
| Operand Name | Operand Value |
|---|---|
| Equals | = |
| Regular Expression | regex |
| Greater or equals | >= |
| Less or equals | <= |
| Greater | > |
| Less | < |
| Contains | like |
Single Condition
Use addFilter once to filter records that meet the condition:
var colA = "1000002"; // Field A
var query = db.getAPIQuery("/workflow-demo/1");
query.addFilter(colA, '=', 'Green');
var results = query.getAPIResultsFull();
var entry = results.next();
while (entry) {
// do something
entry = results.next();
}
Compound Condition (AND)
Use addFilter multiple times on different fields to filter records that meet all conditions:
var colA = "1000002"; // Field A
var colB = "1000004"; // Field B
var query = db.getAPIQuery("/workflow-demo/1");
query.addFilter(colA, '=', 'Green');
query.addFilter(colB, '=', 'Yes');
var results = query.getAPIResultsFull(); // All records where Field A is Green AND Field B is Yes
var entry = results.next();
while (entry) {
// do something
entry = results.next();
}
Compound Condition (OR)
Use addFilter multiple times on the same field to filter records that meet any of the conditions:
var colA = "1000002"; // Field A
var query = db.getAPIQuery("/workflow-demo/1");
query.addFilter(colA, '=', 'Green');
query.addFilter(colA, '=', 'Red');
var results = query.getAPIResultsFull(); // All records where Field A is Green OR Field A is Red
var entry = results.next();
while (entry) {
// do something
entry = results.next();
}
Range Query
var colA = "1000006"; // Field A
var query = db.getAPIQuery("/workflow-demo/1");
query.addFilter(colA, '>', '20');
query.addFilter(colA, '<', '40');
var results = query.getAPIResultsFull(); // All records where Field A is greater than 20 and less than 40
var entry = results.next();
while (entry) {
// do something
entry = results.next();
}
Please note that when you filter by date or date time, they will need to be in the following format: yyyy/MM/dd or yyyy/MM/dd HH:mm:ss.
You can also use a full-text search as a query filter by calling setFullTextSearch(queryTerm) instead of addFilter().
Updating Records
Let's start with a simple example that retrieves the current record, updates its value with a button, and saves it back to the database. Here is what the demo form looks like:

We would like to design some buttons that will change the value of our status field with the click of a button executing simple server-side JavaScript workflow. Here's the code behind the button:
function setStatus(recordId, status) {
var STATUS_FIELD = 1000012; // field id of the status field
var query = db.getAPIQuery("/workflow-demo/2");
var entry = query.getAPIEntry(recordId);
if (status) {
entry.setFieldValue(STATUS_FIELD, status);
} else {
// Toggle
var newStatus = entry.getFieldValue(STATUS_FIELD) == 'On' ? 'Off' : 'On';
entry.setFieldValue(STATUS_FIELD, newStatus);
}
entry.save();
}
The variable db is predefined. Generally, we call getAPIQuery(pathToForm) to retrieve the query object for a sheet. Then retrieve a record with getAPIEntry(recordId), and call setFieldValue(fieldId, value) to set a value or getFieldValue(fieldId) to retrieve a value.
Date format requirements: When adding date values, they must be in one of these formats:
yyyy/MM/ddyyyy/MM/dd HH:mm:ssHH:mm:ss
Multiple Selection Fields
If you're retrieving a value from a multiple selection field, use getFieldValues(fieldId) to retrieve all values as an array. You can call setFieldValue(fieldId, value, true) with an extra true argument to append an option to the current list instead of overwriting.
To copy a multiple selection field value to another multiple selection field:
var multipleSelectionFieldValue = entry.getFieldValues(1013251);
var targetMultipleSelectionField = "";
for (var i = 0; i < multipleSelectionFieldValue.length; i++) {
if (i == 0) {
targetMultipleSelectionField = multipleSelectionFieldValue[i];
} else {
targetMultipleSelectionField = targetMultipleSelectionField + "|" + multipleSelectionFieldValue[i];
}
}
entry.setFieldValue(1013252, targetMultipleSelectionField, false);
You need to format options with a vertical bar character (|) like when importing data from Excel or CSV files.
File Upload Fields
If you're setting values to a file upload field, you can use setFieldFile(fieldId, fileName, fileContent) to create a file and save it to the specified field.
Creating Records
Creating records is very similar to updating records. Instead of calling query.getAPIEntry(), call query.insertAPIEntry() instead. It will return a record where you can use setFieldValue to add field values. The record will be created after calling entry.save():
function createRecord() {
var query = db.getAPIQuery("/workflow-demo/2");
var entry = query.insertAPIEntry(); // Create a new record object
entry.setFieldValue(1000011, "NEW-001");
entry.setFieldValue(1000012, "Active");
entry.save();
}
Subtables Tutorial
If you have subtables in a sheet, you can also use the API to retrieve data or make edits. Here is a form with a subtable:

This workflow walks through each row in the subtable, finds the total amount for each year (based on the date field), and identifies which year has the highest total. This is a post-workflow, so the read-only fields will be filled after the record is saved:
var KEY_FIELD = 1000006;
var AMOUNT_SUBTABLE_ID = 1000007;
var DATE_FIELD = 1000003;
var AMOUNT_FIELD = 1000004;
var MAX_YEAR_FIELD = 1000008;
var MAX_TOTAL_FIELD = 1000009;
var THIS_YEAR_TOTAL_FIELD = 1000010;
var query = db.getAPIQuery("/workflow-demo/1");
var entry = query.getAPIEntry(param.getNewNodeId(KEY_FIELD));
var subtableSize = entry.getSubtableSize(AMOUNT_SUBTABLE_ID);
var yearTotal = {};
for (var i = 0; i < subtableSize; i++) {
var year = parseInt(entry.getSubtableFieldValue(AMOUNT_SUBTABLE_ID, i, DATE_FIELD).substr(0, 4));
var amount = parseInt(entry.getSubtableFieldValue(AMOUNT_SUBTABLE_ID, i, AMOUNT_FIELD));
if (year in yearTotal) {
yearTotal[year] += amount;
} else {
yearTotal[year] = amount;
}
}
var maxYear;
for (var year in yearTotal) {
if (!maxYear || yearTotal[maxYear] < yearTotal[year]) {
maxYear = year;
}
}
entry.setFieldValue(MAX_YEAR_FIELD, maxYear);
entry.setFieldValue(MAX_TOTAL_FIELD, yearTotal[maxYear]);
entry.setFieldValue(THIS_YEAR_TOTAL_FIELD, yearTotal[new Date().getFullYear()]);
entry.save();
The basic idea is to use getSubtableSize(subtableId) to get the number of rows, and getSubtableFieldValue(subtableId, subtableRowIndex, subtableFieldId) to retrieve values. You can also use setSubtableFieldValue(subtableFieldId, subtableRootNodeId, value) to set values. The subtableRootNodeId specifies which row you're referring to. Use getSubtableRootNodeId(subtableId, subtableRowIndex) to find it. To add a new row, use a negative subtableRootNodeId like -100. All values set to the same negative ID will be applied to the same new row.
Subtable Access in Pre-workflow and Post-workflow
To get subtable values in pre-workflow and post-workflow, use param.getSubtableEntry():
var list = param.getSubtableEntry(AMOUNT_SUBTABLE_ID);
var arr = list.toArray();
response.setStatus('WARN');
for (var i = 0; i < arr.length; i++) {
response.setMessage('Date: ' + arr[i].getNewValue(DATE_FIELD) + ', Amount: ' + arr[i].getNewValue(AMOUNT_FIELD) + '\r\n');
}
Access All Records
Use getAPIResultsFull() to traverse records in a query. It retrieves data one by one using an iterator, keeping memory usage low and suitable for any dataset size.
var query = db.getAPIQuery("/workflow-demo/1");
var results = query.getAPIResultsFull();
while (results.hasNext()) {
var entry = results.next();
// do something
}
Checking User Access Right
You can do conditional processing based on which groups the user is in. A user can be in multiple user groups, so you can use user.isInGroup(groupName) to check. The user object and isInGroup call can be used in all workflow types except Daily Workflow, which is triggered by the system.
if (!user.isInGroup('SYSAdmin')) {
response.setStatus('INVALID');
response.setMessage('You do not have the access right to do this.');
}
Sending Email Notifications
You can send email notifications based on conditions or customize notification content. Here is the script to send an email:
var name = entry.getFieldValue(1001426);
var email = entry.getFieldValue(1001428);
var title = entry.getFieldValue(1001386);
mailer.compose(
email, // to
null, // cc
'support@example.com', // reply to
'Acme, Inc.', // displayed from
title,
'Hi ' + name + ', we have received your sales order \n' +
'and will be processing your order very soon.\n' +
'You can see your order details at https://www.ragic.com/example/1 \n' +
'Thanks. \n\n\n' +
'Best Regards, Sophia, Sales Manager Acme, Inc.'
);
// mailer.attach(myURL); // You can use .attach to attach content from a URL
mailer.send();
Note that if you would like to send an email to multiple recipients, just separate each email address with commas.
We do enforce some limitations on how many emails you can send. If you have questions on email sending quota, send us an email at support@ragic.com.
Sending Mobile App Notifications
Other than email notifications, you can also send a mobile app notification. It will be sent to the user if the iOS app or Android app has been installed on their mobile device:
mailer.sendAppNotification("mike@example.com", "test mobile notification");
If you would like the user to be redirected to the specified record when clicking on this notification, provide a path to the form and the record ID:
mailer.sendAppNotification("mike@example.com", "test mobile notification", "/forms/1", 12);
Send HTTP Request
You can send an HTTP GET/POST/DELETE/PUT request to a URL and get the returned result:
util.getURL(urlstring)
util.postURL(urlstring, postBody)
util.deleteURL(urlstring)
util.putURL(urlstring, putBody)
The variable util is predefined. If you need to ignore SSL certificate validation for your current HTTP request, add the following code before sending the request:
util.ignoreSSL();
You can also call util.setHeader(name, value) to set HTTP headers and util.removeHeader(name) to remove headers.
Download and Upload Files
You can download a file from a URL and get the filename:
var fileName = util.downloadFile(fileUrl);
// fileUrl: The source URL of the file.
// Return value: The filename of the downloaded file.
After downloading the file to the database, you can use setFieldValue to fill in the filename in the file field, allowing you to preview the file in the form or download it to your local device:
var fileFieldId = 1000087;
var fileName = util.downloadFile(fileUrl);
entry.setFieldValue(1000087, fileName);
entry.save();
You can upload a file and get the filename:
util.postFile(sourceFileUrl, destinationUrl);
// sourceFileUrl: The source URL of the file.
// destinationUrl: The destination URL to which the file will be uploaded.
// Return value: The filename of the uploaded file.
You can refer to this guide for methods on retrieving complete file and image links.
Inventory Management
Deduct Stock on Order
// Post-workflow on Orders form
var entry = param.getUpdatedEntry();
var subtableId = "1000100"; // Order items subtable
var itemCount = entry.getSubtableSize(subtableId);
for (var i = 0; i < itemCount; i++) {
var productId = entry.getSubtableFieldValue(subtableId, i, "1000101");
var qty = parseInt(entry.getSubtableFieldValue(subtableId, i, "1000102"));
// Create a new query per iteration — addFilter accumulates and results are cached
var invQuery = db.getAPIQuery("/inventory/1");
invQuery.setUpdateMode();
invQuery.addFilter(1000001, "=", productId);
var invEntry = invQuery.getAPIResult();
if (invEntry != null) {
var stock = parseInt(invEntry.getFieldValue(1000002));
invEntry.setFieldValue(1000002, (stock - qty).toString());
invEntry.save();
}
}
Cascading Updates
Update All Related Records
// When customer info changes, update all their orders
var entry = param.getUpdatedEntry();
var customerId = entry.getRootNodeId();
var newAddress = entry.getFieldValue(1000003);
var orderQuery = db.getAPIQuery("/orders/1");
orderQuery.setUpdateMode();
orderQuery.addFilter(1000010, "=", customerId.toString());
var orders = orderQuery.getAPIResultsFull();
while (orders.hasNext()) {
var order = orders.next();
order.setFieldValue(1000011, newAddress);
order.save();
}
Validation Patterns
Prevent Duplicates
// Pre-workflow: Check for duplicate email
var email = param.getNewValue(1000001);
var currentId = param.getRootNodeId();
var query = db.getAPIQuery("/contacts/1");
query.addFilter(1000001, "=", email);
var results = query.getAPIResultsFull();
while (results.hasNext()) {
var entry = results.next();
if (entry.getRootNodeId() !== currentId) {
response.setStatus("INVALID");
response.setMessage("Email already exists: " + email);
break;
}
}
Required Fields Based on Status
// Pre-workflow: Require approval date when approved
var status = param.getNewValue(1000001);
var approvalDate = param.getNewValue(1000002);
if (status === "Approved" && !approvalDate) {
response.setStatus("INVALID");
response.setMessage("Approval date is required when status is Approved");
}
Auto-Generation
Generate Sequential Numbers
// Post-workflow: Auto-generate order number on new records
var entry = param.getUpdatedEntry();
if (param.isCreateNew()) {
var query = db.getAPIQuery("/orders/1");
query.setOrder(1000001, 2); // Sort by order number descending (2 = desc, 1 = asc)
query.setLimitSize(1);
var lastEntry = query.getAPIResult();
var nextNumber = 1;
if (lastEntry != null) {
var lastNum = lastEntry.getFieldValue(1000001);
nextNumber = parseInt(lastNum.replace("ORD-", "")) + 1;
}
entry.setFieldValue(1000001, "ORD-" + nextNumber);
entry.save();
}
Notifications
Conditional Email Notifications
// Post-workflow: Notify manager for high-value orders
var entry = param.getUpdatedEntry();
var total = parseFloat(entry.getFieldValue(1000003));
var customerName = entry.getFieldValue(1000001);
if (total > 10000) {
mailer.compose(
"manager@company.com",
null,
"orders@company.com",
"Order System",
"High-value order from " + customerName,
"<h2>New High-Value Order</h2>" +
"<p>Customer: " + customerName + "</p>" +
"<p>Total: $" + total.toFixed(2) + "</p>" +
"<p><a href='https://www.ragic.com/" + account.getApname() + "/orders/1/" +
entry.getRootNodeId() + "'>View Order</a></p>"
);
mailer.send();
}
External API Integration
POST to Webhook
// Post-workflow: Send data to external system
var entry = param.getUpdatedEntry();
var payload = {
event: "order_created",
orderId: entry.getRootNodeId(),
customer: entry.getFieldValue(1000001),
total: entry.getFieldValue(1000003),
timestamp: new Date().toISOString()
};
// Set headers before making the request
util.setHeader("Content-Type", "application/json");
util.setHeader("Authorization", "Bearer your-api-key");
try {
var result = util.postURL(
"https://webhook.site/your-endpoint",
JSON.stringify(payload)
);
log.info("Webhook response: " + result);
} catch (e) {
log.error("Webhook failed: " + e);
}
Approval Workflows
Auto-Start Approval Based on Amount
// Post-workflow: Start approval for purchases over $1000
var entry = param.getUpdatedEntry();
var amount = parseFloat(entry.getFieldValue(1000003));
var status = entry.getFieldValue(1000005);
if (amount > 1000 && status === "Pending") {
var signers = [];
if (amount <= 5000) {
signers.push({
stepIndex: 0,
approver: "manager@company.com",
stepName: "Manager Approval"
});
} else {
signers.push({
stepIndex: 0,
approver: "manager@company.com",
stepName: "Manager Approval"
});
signers.push({
stepIndex: 1,
approver: "director@company.com",
stepName: "Director Approval"
});
}
approval.create(JSON.stringify(signers));
}
Date Handling
Calculate Due Date
// Post-workflow: Set due date to 30 days from order date
var entry = param.getUpdatedEntry();
var orderDate = entry.getFieldValue(1000002);
if (orderDate) {
var parts = orderDate.split("/");
var date = new Date(parts[0], parts[1] - 1, parts[2]);
date.setDate(date.getDate() + 30);
var dueDate = date.getFullYear() + "/" +
(date.getMonth() + 1) + "/" +
date.getDate();
entry.setFieldValue(1000003, dueDate);
entry.save();
}
Subtable Processing
Calculate Subtable Totals
// Post-workflow: Sum subtable values
var entry = param.getUpdatedEntry();
var subtableId = "1000100";
var rowCount = entry.getSubtableSize(subtableId);
var total = 0;
for (var i = 0; i < rowCount; i++) {
var qty = parseFloat(entry.getSubtableFieldValue(subtableId, i, "1000101")) || 0;
var price = parseFloat(entry.getSubtableFieldValue(subtableId, i, "1000102")) || 0;
total += qty * price;
}
entry.setFieldValue(1000010, total.toString());
entry.save();
Adding Subtable Rows to New Records
When creating a record with insertAPIEntry(), you can add subtable rows using negative IDs before calling save(). Use the same negative ID for all fields in the same row. Different negative IDs create different rows.
var query = db.getAPIQuery("/orders/1");
var entry = query.insertAPIEntry();
entry.setFieldValue(1000001, "ORD-001");
// Add first subtable row (use -100 for all fields in this row)
entry.setSubtableFieldValue(1000101, -100, "Product A");
entry.setSubtableFieldValue(1000102, -100, "5");
entry.setSubtableFieldValue(1000103, -100, "100");
// Add second subtable row (use -101, a different negative ID)
entry.setSubtableFieldValue(1000101, -101, "Product B");
entry.setSubtableFieldValue(1000102, -101, "3");
entry.setSubtableFieldValue(1000103, -101, "250");
entry.save();
Note: Negative IDs are temporary identifiers used only during insertion. The system maps them to actual node IDs when the record is saved. The specific negative values don't matter as long as fields belonging to the same row share the same negative ID.
Copying Records (entryCopier)
Copying records is one of the most common workflow programs. Ragic provides a simple entryCopier function to simplify this type of operation. Let's say we would like to see a record on this sheet:

With the click of a button, generate a record on this sheet:

Here is the code for this action button:
function copyEntry(nodeId) {
db.entryCopier(JSON.stringify({
THIS_PATH: "/workflow-demo/3",
THIS_NODEID: nodeId,
NEW_PATH: "/workflow-demo/4",
COPY: {
1000030: 1000014, // A
1000031: 1000015, // C
1000032: 1000018, // S1
1000033: 1000020 // S3
}
}), response);
}
The entryCopier takes a JSON string as its parameter. Just put down the source sheet, target sheet, the record that we're copying, and most importantly, which field should be mapped to which field (target field ID: source field ID). When the mapping is complete, you can create action buttons to copy records from one sheet to another very easily.
Copy with Additional Modifications
Here we want to copy a record from "CopyFrom" to "CopyTo" and set the status to "New", plus record the created date:

function copyEntry(nodeId) {
db.entryCopier(JSON.stringify({
THIS_PATH: "/entrycopier/1",
THIS_NODEID: nodeId,
NEW_PATH: "/entrycopier/2",
COPY: {
1000004: 1000001, // To-ID : From-ID
1000005: 1000002 // To-Name : From-Name
}
}), response);
// Get the rootNodeId of the copied entry
var newEntryRootNodeId = response.getRootNodeId();
var toApiQuery = db.getAPIQuery('/entrycopier/2');
var newEntry = toApiQuery.getAPIEntry(newEntryRootNodeId);
newEntry.setFieldValue(1000007, "New");
var current = new Date();
newEntry.setFieldValue(1000008, current.getFullYear() + '/' + (current.getMonth() + 1) + '/' + current.getDate());
newEntry.save();
}
Set up the Action Button and save it:

Then click the Action Button to display the result:

Paging Through Records
This section demonstrates how to use setLimitSize and setLimitFrom to manually page through records. In this example, we have 20 records total and retrieve 6 at a time, displaying a message for each batch.
/**
* Field Name Field ID
* ----------- --------
* ID : 1000009
* Amount : 1000010
*/
function limitFromExample() {
var apiQuery = db.getAPIQuery('/entrycopier/3');
// Remember the offset
var fromNumber = 0;
apiQuery.setLimitFrom(fromNumber);
// Fetch 6 at a time
apiQuery.setLimitSize(6);
var resultList = apiQuery.getAPIResultList();
while (resultList.length > 0) {
var msg = '';
for (var i = 0; i < resultList.length; i++) {
var entry = resultList[i];
msg += entry.getFieldValue(1000009);
if (i != resultList.length - 1) msg += ',';
}
response.setMessage(msg);
// Update the offset
fromNumber += resultList.length;
// Create a new query with updated offset
apiQuery = db.getAPIQuery('/entrycopier/3');
apiQuery.setLimitSize(6);
apiQuery.setLimitFrom(fromNumber);
resultList = apiQuery.getAPIResultList();
}
response.setStatus('WARN');
}
Entries:

Result:

Deleting Records
You can use query.deleteEntry(nodeId) to delete records. If you need to delete multiple records, use getAPIResultList() to get all matching records as an array, then delete each one.
Warning: Do not use
getAPIResultsFull()for deletion loops.getAPIResultsFull()fetches records lazily via an iterator — deleting records while iterating will cause unpredictable results. Always usegetAPIResultList(), which loads all records into memory first, so deletions won't interfere with the iteration.
var query = db.getAPIQuery(pathToForm);
query.addFilter(1000001, '=', 'To Be Deleted');
var entries = query.getAPIResultList();
for (var i = 0; i < entries.length; i++) {
query.deleteEntry(entries[i].getRootNodeId());
}
Email Attachment Formats
When attaching Ragic records as email attachments, you can use special URL formats. For example, if the record URL is https://www.ragic.com/wfdemo/workflow-demo/2/9 (always ignore the URL after the hash), you can use the following:
PDF version:
https://www.ragic.com/wfdemo/workflow-demo/2/9.pdf
Excel version:
https://www.ragic.com/wfdemo/workflow-demo/2/9.xlsx
Mail Merge version (cid is the Mail Merge template ID, obtainable from the URL when downloading the Mail Merge document):
https://www.ragic.com/wfdemo/workflow-demo/2/9.custom?rn=9&cid=1
Calling the Workflow of Other Forms
When using the workflow to modify data in other forms, the workflow for that form is not triggered by default. If you need to trigger the form's post-workflow or pre-workflow, use the following code:
var query = db.getAPIQuery(pathToForm);
var entry = query.getAPIEntry(recordId);
// do something...
entry.setIfExecuteWorkflow(true);
entry.save();
ES5 Compatibility Guide
Current Engine: Nashorn (ECMAScript 5.1)
Ragic workflows run on the Nashorn JavaScript engine, which only supports ECMAScript 5.1. Avoid using ES6+ features.
Note: We are currently preparing to upgrade to GraalVM, but the upgrade is not yet complete. Once completed, modern JavaScript features (such as let, const, arrow functions, etc.) will be available. Until then, please continue using ES5 syntax.
Feature Compatibility
Variables
// DO - ES5
var name = "value";
var CONSTANT = 100;
// DON'T - ES6
let name = "value"; // SyntaxError
const CONSTANT = 100; // SyntaxError
Functions
// DO - ES5
function processRecord(entry) {
return entry.getFieldValue(1000001);
}
var handler = function(data) {
return data.value;
};
// DON'T - ES6
const processRecord = (entry) => entry.getFieldValue(1000001); // SyntaxError
String Operations
// DO - ES5
var message = "Hello " + name + "!";
var multiline = "Line 1\n" +
"Line 2\n" +
"Line 3";
// DON'T - ES6
var message = `Hello ${name}!`; // SyntaxError
var multiline = `Line 1
Line 2
Line 3`; // SyntaxError
Object Properties
// DO - ES5
var obj = {
name: name,
value: value,
process: function(data) { return data; }
};
// DON'T - ES6
var obj = {
name, // Shorthand - SyntaxError
value,
process(data) { return data; } // Method shorthand - SyntaxError
};
Destructuring
// DO - ES5
var data = getResponse();
var name = data.name;
var value = data.value;
var arr = [1, 2, 3];
var first = arr[0];
var second = arr[1];
// DON'T - ES6
var { name, value } = getResponse(); // SyntaxError
var [first, second] = [1, 2, 3]; // SyntaxError
Array Methods
// DO - ES5 Array methods (available)
var filtered = [];
for (var i = 0; i < items.length; i++) {
if (items[i].active) {
filtered.push(items[i]);
}
}
// Or use forEach (ES5)
items.forEach(function(item) {
if (item.active) filtered.push(item);
});
// DON'T - ES6 Arrow functions
var filtered = items.filter(item => item.active); // SyntaxError
var names = items.map(item => item.name); // SyntaxError
Default Parameters
// DO - ES5
function greet(name) {
name = name || "Guest";
return "Hello " + name;
}
// DON'T - ES6
function greet(name = "Guest") { // SyntaxError
return "Hello " + name;
}
Spread Operator
// DO - ES5
var combined = arr1.concat(arr2);
var copy = arr.slice();
// DON'T - ES6
var combined = [...arr1, ...arr2]; // SyntaxError
var copy = [...arr]; // SyntaxError
Classes
// DO - ES5 Constructor pattern
function Order(id, customer) {
this.id = id;
this.customer = customer;
}
Order.prototype.getTotal = function() {
return this.calculateTotal();
};
// DON'T - ES6 Classes
class Order { // SyntaxError
constructor(id, customer) {
this.id = id;
this.customer = customer;
}
}
Promises / Async-Await
// Promises and async/await are NOT available
// Use synchronous code only
// DO - Synchronous
var result = util.getURL("https://api.example.com/data");
var data = JSON.parse(result);
processData(data);
// DON'T - Async (not supported)
fetch(url).then(response => response.json()); // Not available
async function getData() { } // Not available
Available ES5 Features
These work correctly:
vardeclarationsfunctiondeclarationsfor,while,do-whileloopsfor-inloopsif,else,switchstatementstry,catch,finally- Array methods:
forEach,map,filter,reduce,some,every JSON.parse()andJSON.stringify()- Regular expressions
Object.keys(),Object.create()Array.isArray()
GraalVM Upgrade Plan
We are preparing to upgrade from Nashorn to the GraalVM JavaScript engine, but the upgrade work is not yet complete. Until the upgrade is finished, please continue writing workflows using ES5 syntax.
Once the upgrade is complete, you'll be able to use the following modern JavaScript features:
letandconst- Arrow functions
- Template literals
- Destructuring
- Spread operator
- Classes
- And more ES6+ features
Code written in ES5 will continue to work after the migration.
Security Model
Overview
Ragic workflows run in a sandboxed environment with restricted access to Java classes and system resources. This ensures that workflows cannot compromise server security or access unauthorized data.
Blocked Java Classes
The following Java class prefixes are blocked and cannot be accessed:
| Blocked Prefix | Reason |
|---|---|
java.io |
File system access |
java.nio |
NIO file/network access |
java.net |
Network socket access |
java.lang.Runtime |
Process execution |
java.lang.System |
System properties |
java.lang.Process |
Process control |
java.lang.reflect |
Reflection access |
javax.script |
Script engine access |
java.security |
Security classes |
java.util.concurrent |
Thread management |
com.sun |
Internal JDK classes |
jdk.nashorn |
Engine internals |
Attempting to use blocked classes will result in a security exception.
Safe Operations
HTTP Requests
Use the util object for all HTTP operations:
// Safe - uses util object
var response = util.getURL("https://api.example.com/data");
var result = util.postURL("https://api.example.com/create", payload);
// Blocked - direct Java network access
var URL = Java.type("java.net.URL"); // SecurityException
File Operations
Use the util object for file downloads/uploads:
// Safe - uses util object
var fileUrl = util.downloadFile("https://example.com/file.pdf");
// Blocked - direct file system access
var File = Java.type("java.io.File"); // SecurityException
Best Practices
1. Validate External Input
// Always validate URLs before making requests
var url = entry.getFieldValue(1000001);
if (url && url.indexOf("https://") === 0) {
var response = util.getURL(url);
}
2. Handle Errors Gracefully
try {
var result = util.postURL(webhookUrl, payload);
log.info("Webhook success");
} catch (e) {
log.error("Webhook failed: " + e);
// Don't expose error details to users
response.setMessage("External service temporarily unavailable");
}
3. Limit Data Exposure
// Don't include sensitive data in logs
log.info("Processing order: " + orderId); // Good
log.info("Customer SSN: " + ssn); // Bad - sensitive data
4. Use Appropriate Access Levels
Workflows run with the permissions of the triggering user. For system-level operations, consider:
- Using post-workflow with appropriate field access rights
- Implementing approval workflows for sensitive operations
- Restricting action buttons to specific user groups
Account Isolation
Workflows can only access data within the current account. There is no cross-account query API — each workflow runs strictly within the scope of the account where it is defined.
Audit Trail
Workflow executions are logged with:
- Execution timestamp
- Triggering user
- Workflow type
- Status (success/failure)
- Error messages (if any)
Access logs through your sheet's workflow settings.
Troubleshooting Guide
Common Errors
"entry.getFieldValue is not a function" in Pre-workflow
Problem: Using entry.getFieldValue() in pre-workflow
Solution: Use param.getNewValue() instead
// Wrong (Pre-workflow)
var value = entry.getFieldValue(1000001);
// Correct (Pre-workflow)
var value = param.getNewValue(1000001);
"Post-workflow attempting to trigger itself"
Problem: Attempting to set setIfExecuteWorkflow(true) for the same form in post-workflow
Error message: "Workflow execution is denied because the trigger originates from the same form."
Cause: The system has built-in protection against infinite loops. In post-workflows, attempting to set true for the same form that triggered the workflow will throw an exception.
Solution:
- By default,
entry.save()does not trigger workflows, no extra setting needed - To trigger workflows on other forms, use
setIfExecuteWorkflow(true) - Never set
truefor the same form in post-workflow
// In an order form's post-workflow
var order = param.getUpdatedEntry();
// Wrong: Attempting to trigger its own post-workflow
order.setIfExecuteWorkflow(true);
order.save(); // Will throw exception
// Correct: Default behavior does not trigger workflow
order.setFieldValue(1000001, "processed");
order.save(); // Does not trigger post-workflow
// Correct: Trigger workflow on other forms
var invQuery = db.getAPIQuery("/inventory/1");
var invEntry = invQuery.getAPIEntry(123);
invEntry.setIfExecuteWorkflow(true); // Can trigger inventory form's workflow
invEntry.save();
"Cannot call methods on null"
Problem: Query returns no results
Solution: Always check for null/empty results
var entry = query.getAPIEntry(recordId);
if (entry != null) {
var value = entry.getFieldValue(1000001);
}
var result = query.getAPIResult();
if (result != null) {
// use result directly
}
Date Parsing Issues
Problem: Dates not saving correctly
Solution: Use Ragic's date format yyyy/MM/dd
// Wrong
entry.setFieldValue(1000001, "03-15-2024");
entry.setFieldValue(1000001, "2024-03-15");
// Correct
entry.setFieldValue(1000001, "2024/03/15");
entry.setFieldValue(1000001, "2024/03/15 14:30:00");
Multiple Selection Not Working
Problem: Can't set multiple values
Solution: Use pipe-separated values or append mode
// Method 1: Pipe-separated string
entry.setFieldValue(1000001, "Option1|Option2|Option3");
// Method 2: Append one value at a time (3rd arg = true)
entry.setFieldValue(1000001, "Option1");
entry.setFieldValue(1000001, "Option2", true);
entry.setFieldValue(1000001, "Option3", true);
Query Not Finding Records
Problem: Filter returns empty results
Possible causes:
- Field ID is wrong
- Value format doesn't match
- Permissions issue
// Debug approach
log.info("Searching for field 1000001 = " + searchValue);
var results = query.getAPIResultsFull();
var count = 0;
while (results.hasNext()) {
results.next();
count++;
}
log.info("Found " + count + " records");
"ReferenceError: X is not defined"
Problem: Using ES6 features or undefined variables
Solution: Use ES5 syntax only
// WRONG (ES6) - will cause SyntaxError
let value = "test"; // WRONG - use var
const LIMIT = 100; // WRONG - use var
var items = results.map(r => r.getFieldValue(1000001)); // WRONG - no arrow functions
// Correct (ES5)
var value = "test";
var LIMIT = 100;
var items = [];
for (var i = 0; i < results.length; i++) {
items.push(results[i].getFieldValue(1000001));
}
Accidentally Overwriting Built-in Objects
Problem: Variable name shadows a workflow global object
Solution: Avoid naming local variables response, db, param, user, mailer, util, account, log, or approval. These are built-in workflow objects and overwriting them will cause unexpected failures.
// WRONG - overwrites the workflow response object
var response = JSON.parse(util.getURL(apiUrl));
response.setStatus("SUCCESS"); // TypeError: response.setStatus is not a function
// CORRECT - use a different variable name
var apiResponse = JSON.parse(util.getURL(apiUrl));
response.setStatus("SUCCESS"); // Works correctly
Performance Issues
Query Taking Too Long
Solutions:
- Add specific filters
- Limit result size
- Use getAPIResultsFull() for efficient iteration
// Add filters and use iterator
query.addFilter(1000005, "=", "Active");
var results = query.getAPIResultsFull();
while (results.hasNext()) {
var entry = results.next();
// process entry
}
Debugging Tips
Use Log Statements
log.info("Starting workflow");
log.debug("Record ID: " + recordId);
log.warn("Potential issue: " + warning);
log.error("Error occurred: " + error);
Check Workflow Execution Log
Access the workflow execution log through your sheet's workflow settings to view recent executions and any error messages.
Test in Development First
// Add safety check
if (user.getEmail() === "developer@company.com") {
// Test code here
log.info("Test mode - would update " + results.length + " records");
} else {
// Production code
}