Ragic JavaScript
Workflow

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:

  1. Action Button
  2. Post-Workflow
  3. Pre-Workflow
  4. Daily Workflow
  5. Approval 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

  1. Open your Ragic sheet
  2. Click the sheet tab menu (down arrow next to sheet name)
  3. Select Javascript Workflow
  4. Choose the workflow type (e.g., Post-Workflow)
  5. 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

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() or entry.getRootNodeId()
  • User email: user.getEmail() (in Daily workflow, returns dailyJob@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:

Right-click sheet to access Javascript Workflow

And choose installed sheet scope from the top dropdown:

Select installed sheet scope

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.

Action Button configuration

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:

Right-click sheet to access 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:

Right-click sheet to access Javascript Workflow

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

Pre-workflow demo 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:

Pre-workflow error result

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:

Right-click tab for Global Javascript Workflow

And choose Daily Workflow from the top dropdown.

Daily Workflow 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.

Daily workflow history button

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:

Right-click sheet to access 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 started
  • FINISH - All steps completed (fully approved)
  • REJECT - Approval rejected at any step
  • CANCEL - 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:

Right-click tab for Global Javascript Workflow

Click History to view the workflow modification history.

Global workflow history button

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:

  1. Global workflow — evaluated once per engine initialization
  2. Sheet scope (per-sheet shared code) — evaluated before the specific workflow
  3. 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 result or data) 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. Use getAPIResultList() 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() and save() 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() or query.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 false disables 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.log in 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
email 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
email 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:

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:

Approval 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
email 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.

Workflow Execution Log location

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();
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

Email

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) or entry.getRootNodeId()
  • User email: user.getEmail() (in Daily workflow, returns dailyJob@ragic.com)
  • Account name: db.getApname() or account.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:

Update records form

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/dd
  • yyyy/MM/dd HH:mm:ss
  • HH: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:

Subtable form

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

// 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:

Copy from sheet

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

Copy to 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:

Entry copier copy from Entry copier copy to

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:

Set action button

Then click the Action Button to display the result:

Copy 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:

Paging entries

Result:

Paging 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 use getAPIResultList(), 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:

  • var declarations
  • function declarations
  • for, while, do-while loops
  • for-in loops
  • if, else, switch statements
  • try, catch, finally
  • Array methods: forEach, map, filter, reduce, some, every
  • JSON.parse() and JSON.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:

  • let and const
  • 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 true for 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:

  1. Field ID is wrong
  2. Value format doesn't match
  3. 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:

  1. Add specific filters
  2. Limit result size
  3. 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
}