back to docs
[reference] actions DSL workflows triggers logic

Actions & Workflows

Server-side actions with the dForge DSL: parameters, conditions, execution modes, built-in functions, and triggers.

published · updated

What are Actions?

Actions are server-side operations that go beyond basic CRUD. They execute custom business logic defined in a simple scripting language (DSL). Examples:

  • Convert a lead into an account, contact, and opportunity
  • Generate an invoice from a quote
  • Transfer stock between warehouses
  • Send notification emails

DSL Structure

Each action is defined in a .dsl file under logic/actions/ in your module, with up to three blocks:

params:
    param_name: type required "Label" [constraints]

canExecute:
    [field] != 'SomeValue'

execute:
    // your logic here

The DSL is compiled to JavaScript and executed server-side in a sandboxed engine with a 5-second timeout. Each execution gets a fresh engine — there is no shared state between runs. The action is registered in ui/actions.json, which points to the script file and sets metadata like the icon, label, execution mode, and entity binding.

params (optional)

Declares parameters that the user fills in before the action runs:

params:
    dest_warehouse_id: text required "Destination Warehouse"
    quantity: number required "Quantity" min=1
    reference_no: text optional "Transfer Reference"

Parameter types: text, number, date, datetime, checkbox, lookup.

canExecute (optional)

A formula that determines whether the action is available. If it evaluates to false, the action button is disabled. Evaluated on the client for instant button state and re-checked on the server before execution.

If canExecute is omitted, the action is available to anyone who has the E (Execute) right on it.

canExecute:
    [stage] != 'Closed Won' AND [stage] != 'Closed Lost'

For multi-record actions, all selected records must satisfy canExecute — if any one fails, the button is disabled.

execute (required)

The main logic block. Has access to the current record’s fields, parameters, and built-in functions.

Field Access

Access the current record’s fields with bracket syntax:

var name = [account_name]
var total = [quantity] * [unit_price]

Navigate through references:

var industry = [account_id].[industry]

Access parameters:

var dest = params[dest_warehouse_id]

Built-in Functions

Data Functions

FunctionDescription
insert(entity, data)Insert a new record. Returns the full inserted row.
query(sql, args)Run a parameterized SELECT. Table references are auto-qualified with the current module schema. Returns an array of row objects.
getRecord(entity, pk)Fetch a single record by primary key. Returns a read-only accessor.
preloadRef(fkField)Load a foreign-key referenced record into memory (single/each mode only).
nextNumber(entity)Generate the next document number from the entity’s number sequence. Supports cross-module: nextNumber('fin.invoice').
callProc(name, args)Call a stored procedure with named arguments.

Notification Functions

FunctionDescription
notify(userId, message)Send an in-app notification to a specific user.
sendEmail(to, subject, body)Send an email (raw mode — direct subject and HTML body).
sendEmail(to, templateCd, data)Send an email rendered from a template in email_template.
info(message)Show a success/info message to the user.
warn(message)Show a warning message. Execution continues.
error(message)Show an error and abort the action.

Context & Utility

ExpressionDescription
userIdThe current user’s ID (long).
TODAY()Current date.
NOW()Current date and time (UTC).
addDays(date, n)Add days to a date value.
IF(cond, then, else)Conditional expression.

Example: Transfer Stock

params:
    dest_warehouse_id: text required "Destination Warehouse"
    quantity: number required "Quantity to Transfer" min=1

canExecute:
    [quantity] > 0

execute:
    if ([warehouse_id] == params[dest_warehouse_id]) {
        error('Source and destination cannot be the same')
    }

    var stock = query(
        "SELECT stock_id, quantity FROM stock WHERE warehouse_id = @wh AND product_id = @prod",
        { wh: [warehouse_id], prod: [product_id] }
    )

    if (stock[0]['quantity'] < params[quantity]) {
        error('Insufficient stock. Available: ' + stock[0]['quantity'])
    }

    insert('stock_movement', {
        movement_type: 'Transfer Out',
        warehouse_id: [warehouse_id],
        product_id: [product_id],
        quantity: params[quantity],
        movement_date: TODAY()
    })

    insert('stock_movement', {
        movement_type: 'Transfer In',
        warehouse_id: params[dest_warehouse_id],
        product_id: [product_id],
        quantity: params[quantity],
        movement_date: TODAY()
    })

    info('Transferred ' + params[quantity] + ' units successfully')

Execution Modes

Actions can run in different modes, configured via executionMode (and is_single) in ui/actions.json:

ModeDescription
singleAction operates on exactly one record. The button is enabled only when one record is selected.
eachAction runs once per selected record. Each execution sees one record.
batchAction receives all selected records at once and processes them as a group.

For multi-record actions, the isTransacted flag controls transaction scope:

  • isTransacted: true — all records are processed in one transaction. If any fail, all changes are rolled back.
  • isTransacted: false — each record runs in its own transaction. Failures are isolated; successful records still commit.

Background Actions

Long-running actions can execute asynchronously by setting is_async: true:

  • The action is queued in the background_action table
  • The UI returns immediately so the user can continue working
  • A hosted background worker polls the queue (every 2 seconds) and executes the action
  • The user receives a notification when the job completes
  • Progress is published over Server-Sent Events

Use async mode for imports, bulk emails, PDF generation, or anything that takes more than a few seconds.

Number Sequences

Entities can define automatic number sequences for document numbering:

{
  "numberSequence": {
    "column": "invoice_no",
    "pattern": "{prefix}{yyyy}-{seq:3}",
    "resetPeriod": "year"
  }
}

When a new record is created and the target column is empty, the platform auto-generates the next number. The prefix is resolved from folder-scoped module settings.

Pattern placeholders: {yyyy}, {yy}, {mm}, {dd}, {seq:N} (N = zero-padded width).

Triggers

Triggers fire actions automatically in response to data changes. Configure them in logic/triggers.json:

EventFires on
insertNew record created
updateAny field on the record changed
deleteRecord deleted
status_changeA specific field’s value changed (compares old vs new)
anyAll of the above

A trigger can run synchronously (inside the originating transaction) or asynchronously (async: true — queued as a background job after the transaction commits). See Triggers & Webhooks for the full reference.

/ was this helpful?

Stuck on something?
Tell us.

We read every message and update the docs based on what readers ask. The fastest way to improve the docs is to write to us.