Automation scripts

An automation script (further referred to as script) is a piece of code that allows you to implement custom business logic. Automation scripts are written in plain JavaScript with the support for node packages.

As of 2021.3, with the introduction of workflows, automation scripts are disabled by default.

Take a look at our samples (Low-Code Platform Developer Guide  Automation scripts  Samples) and the sample project to get started.

See whether you can implement your logic using Workflows. A workflow is able to execute automation scripts

There are two categories of automation scripts; server scripts and client scripts.

Server scripts are executed in the Corteza Corredor server.

Use server scripts when:
  • working with sensitive data,

  • communicating with external APIs,

  • it shouldn’t be possible for the script to be interrupted by the user.

Examples of usage:
  • create additional records based on the current data,

  • send email notifications,

  • run statistic analysis.

Client scripts are executed in the client’s browser (user agent).

Use client scripts when:
  • you need to interact with the user,

  • are performing data validation,

  • inserting default values.

Examples of usage:
  • prompt the user to confirm the form submission,

  • validate the form submitted by the user,

  • redirect the user after they’ve submitted the form,

  • open an external webpage.

Client scripts are less secure as they run in the users browser. Any baked-in credentials are easily accessible, the execution is easily terminated.

Which one should you use? If you need to interact with the user (show notification, request confirmations), use client scripts, elsewhere use server scripts.

File structure

To start writing automation scripts, you must first define the appropriate file structure.

The three main parts:
  1. package.json defines metadata as well as your dependencies.

  2. /server-scripts contains a set of automation scripts that are executed on the Corredor server.

  3. /client-scripts contains a set of automation scripts that will be executed inside the web application.

    • Each sub-directory inside /client-scripts defines a bundle. When a web application loads, it fetches the bundle that is assigned to it (this is done automatically).

Both /server-scripts and /client-scripts assume that all underlying files are automation scripts with valid signatures. When defining a file with utility functions, move it under /shared or define a /util (or similar) at the root of the project.

An example filestructure containing all of the available parts:
package.json
...
/server-scripts
  ...
/client-scripts
  /admin
      ...
  /compose
      ...
  /shared
      ...

…​ indicates that you are free to structure your files as you see fit. We do recommend you group your automation scripts based on their content, for example, scripts working with leads should go under the /Lead directory.

/admin and /compose contain scripts specific to each web application (as discussed earlier).

/shared contains code that client scripts can reuse.

Automation script

It is only possible to define one automation script per file.

This is how a valid automation script looks like:
{
  // A short label describing this script
  label: '...',

  // A longer description of what it does. Don\'t go over board.
  description: '...',

  // This controls script-level security, such as the invoking user.
  // We cover some details a bit later.
  security: {...},

  // This function returns a list of triggers that specify when the script should be ran.
  // We cover some details a bit later.
  triggers: (t) {...},

  // This is the code that is ran when the script is executed.
  // We cover some details a bit later.
  exec: (args, ctx) {...};
}
You can use this template to cover most cases of usage:
export default {
  label: "label goes here",
  description: "description goes here",

  // Use the ones you need, delete the rest
  triggers ({ before, after, on, at }) {
    return before('event goes here')
      .where('constraint goes here')
      // Add/remove constraints here
  },

  // remove async if you aren't doing any async operations
  // use object destructuring for args and ctx
  async exec(args, ctx) {
    // Code goes here
  },
}

Execution arguments

Execution arguments (the args parameter of the exec function) contain the core data that your logic is able to work with. The data differs based on the event that triggered the automation script. To examplify; user related events define what user is involved, while record related events define the involved record and the module.

Refer to Resources and events for a complete reference of what data each event exposes.

Arguments to a client-script are provided via references to the original objects, meaning that any change to the argument parameter reflects back to the original object.

Arguments to a server-script are provided as a copy of the original object, meaning that the changes are not reflected on the original object.

Execution context

Execution context (the ctx parameter of the exec function) contains contextual information and utilities that can be used within script execution.

Table 1. Execution of context parameters:

ctx.console

The console parameter defines a logger.

When executing client scripts, this is the built-in window.console; when executing server scripts, this is a Pino logger.

ctx.log

ctx.log is an alias for ctx.console

ctx.$authUser

$authUser is a reference to the invoking user.

ctx.SystemAPI

SystemAPI is the Corteza API client.

ctx.ComposeAPI

ComposeAPI is the Corteza Low Code API client.

ctx.System

System defines a set of helper methods to work with the core system

ctx.Compose

Compose defines a set of helper methods to work with Corteza Low Code resources.

ctx.ComposeUI

ComposeUI defines a set of helper methods to work with Corteza Low Code user interface.

ctx.frontendBaseURL

frontendBaseURL defines the base URL address that the front-end web applications are running. This is useful when generating URL addresses that point to the Corteza applications (such as a link to the newly created lead).

Execution result

Refer to the execution flow details sub-section for details on how the execution result affects the system.

DevNote document iterators and their special Aborted error

exec (args, ctx) {
  throw new Error('Aborted')
  // OR
  return false
}

The execution is terminated

The automation script is terminated when an error occurs. This normally causes the original operation to also terminate.

An example:
export default {
  trigger (t) {...}

  exec (args, ctx) {
    throw new Error('Oh no, something went wrong')
  }
}

The execution is successful

In any other cases the automation script is successful. The returned value is a zero value, if it is either null or undefined.

An example:
export default {
  trigger (t) {...}

  exec ({ $record }, ctx) {
    return $record
  }
}

Automation triggers

Automation triggers (further referred to as triggers) control the timing of the execution of a specific automation script.

Automation triggers are evaluated in an isolated context that doesn’t allow any external data (variables or imports).

This will not work:
const MOD_NAME = 'Contact'

export default {
  triggers ({ on }) {
    return on('manual')
      .for(MOD_NAME) // 👈 we're referencing the constant here
  },
  exec (args, ctx) {...},
}
The three main parts of a trigger:
  1. an event that specifies what system events the trigger reacts to,

  2. a resource that specifies what system resource the trigger reacts for,

  3. a constraint that specifies how the event needs be presented as in order for the trigger to react.

Table 2. Available trigger types:

These are explicitly triggered by pressing a button.

Use explicit triggers when you wish to manually initiate an action, for example an OAuth authentication flow, redirection to an external resource, or data export.

These are implicitly triggered and based on system events.

Use implicit triggers when you wish for an action to be automatically performed when triggered by another action or process; such as sending an email when you register a new user or adding a changelog entry when the content changes.

Refer to resources and events for a complete list of events you can listen for.

Deferred triggers can only be used in server scripts and require explicit security context.

The system triggers these at a certain point in the future; either periodically (define with cron expressions), or at a timestamp (use ISO 8601, YYYY-MM-DDThh:mm:ssZ format).

Use deferred triggers when you want an action to periodically repeat or be performed at a certain point in the future. Examples of such use are recurring payments or sending holiday newsletters to your subscribers.

The scheduler acts once per minute, therefore that is the maximum accuracy Corteza supports.

Sink triggers can only be used in server scripts and require explicit security context.

These are triggered by the system when it receives a request; either HTTP or email.

Use sink triggers when you want to respond to requests; such as webhooks for external services or custom API endpoints, for example capturing data from external forms, tracking external document changes, and capturing payments.

It is recommended you use the REST API interface whenever possible.

Defining a resource

To define what resource the trigger should react for (such as module, user, role), we use the conveniently named .for('resource:goes:here') method.

An example of specifying a resource:
triggers ({ before }) {
  return before('create', 'update')
    // This will trigger for a compose record resource
    .for('compose:record')
},

Refer to resources and events for a complete list of available resources and supported events.

Defining a constraint

Refer to resource constraints for a list of available constraint properties for each resource.

To define how an event should be presented as (such as the module name, user email, role handle), we use the .where(property, operator, value) method or a variation of it.

To elaborate:
  • When applying two arguments to the method, the first one specifies the property and the second one specifies the value. The default equality operator is used.

  • When specifying three arguments to the method, the first one specifies the property, the second one specifies the comparison operator and the third one specifies the value.

An example of chaining constraints:
triggers ({ before }) {
  return before('create', 'update')
    .for('compose:record')
    .where('module', 'Lead')
    .where('namespace', 'crm')
},
Table 3. Available comparison operators:

Equals (default)

  • eq

  • =

  • ==

  • ===

Not equals

  • not eq

  • ne

  • !=

  • !==

Partial equals

  • like

Supported wildcards:
  • one or more characters: %, *,

  • one character: _, ?.

Partial not equals

  • not like

Supported wildcards:
  • one or more characters: %, *,

  • one character: _, ?.

Regex equals

  • ~

Regex not equals

  • !~

Conventions

Use object destructuring

Object destructuring helps you shorten the code.

Example:
triggers (t) {
  return t.after('create')
    .for('compose:record')
    .where('module', 'super_secret_module')
},

triggers ({ after }) { // 👈 this thing
  return after('create')
    .for('compose:record')
    .where('module', 'super_secret_module')
},

Use constraints

Loose constraints may lead to unwanted side effects such as running the script when a record in a different namespace is created.

Example:
triggers (t) {
  return t.after('create')
    .for('compose:record')
    .where('module', 'super_secret_module')
},

triggers ({ after }) {
  return after('create')
    .for('compose:record')
    .where('module', 'super_secret_module')
    .where('namespace', 'super_secret_namespace') // 👈 this thing
},

Security context

The invoking user

Deferred and sink scripts require you to specify the security context as the invoker is not known.

The invoking user is a person who performs an action that triggers the script execution. To examplify; you press a button, therefore you are the invoking user.

By specifying the invoking user, the automation script may access some resources that the actual invoking user may not have access to, such as personal client information.

You are only able to set the invoking user for server scripts.

An example of defining an invoking user:
export default {
  trigger (t) {...}

  security: 'some-user-identifier-here',

  exec (args, ctx) {...}
}

You can use the user’s handle, email or ID as the user identifier. We suggest you use an email or a handle.

It is a good idea to create a new system user the purpose of which is that of script execution wherever that is needed.

Allowing and denying script execution

Security context lets you prevent specific users from performing specific operations. Each user is attributed a role that specifies the degree of control they can operate with. To examplify; you can prevent regular users from signing documents or sending quotes.

Use these properties when defining the context:
  • allow specifies which roles are permitted to access the automation script,

  • deny specifies which roles are not permitted to access the automation script,

This is only available for explicit scripts. It is ignored for any other script types

An example of permitting access:
export default {
  trigger (t) {...}

  security: {
    allow: ['administrator', 'superuser'],
  },

  exec (args, ctx) {...}
}
An example of denying access:
export default {
  trigger (t) {...}

  security: {
    deny: ['client', 'lead'],
  },

  exec (args, ctx) {...}
}

An example setup

The file structure (the sources are listed below):
/ .gitignore
/ .eslintrc.js
/ .mocharc.js
/ package.json

/ server-scripts
    / Sample.js
    / Sample.test.js
    / ...
/ client-scripts
    / ....
gitignore:
.vscode
node_modules
.nyc_output
coverage
yarn-error.log
eslintrc.js:
module.exports = {
  root: false,
  env: {
    node: true,
    es6: true,
  },
  extends: [
    'standard',
  ],
}
mocharc.js:
module.exports = {
  require: [
    'esm',
  ],
  'full-trace': true,
  bail: true,
  recursive: true,
  extension: ['.test.js'],
  spec: [
    'client-scripts/**/*.test.js',
    'server-scripts/**/*.test.js',
  ],
  'watch-files': [ 'src/**' ],
}
package.json:
{
  "scripts": {
    "lint": "eslint {server-scripts,client-scripts}/**/* --ignore-pattern *.test.js",
    "test:unit": "mocha",
    "test:unit:cc": "nyc mocha"
  },
  "devDependencies": {
    "chai": "^4.2.0",
    "eslint": "^6.8.0",
    "eslint-config-standard": "^14.1.0",
    "eslint-plugin-import": "^2.18.2",
    "eslint-plugin-node": "^10.0.0",
    "eslint-plugin-promise": "^4.2.1",
    "eslint-plugin-standard": "^4.0.1",
    "esm": "^3.2.25",
    "mocha": "^7.0.1",
    "nyc": "^14.1.1",
    "sinon": "^8.1.1"
  },
  "nyc": {
    "all": true,
    "reporter": [
      "lcov",
      "text"
    ],
    "include": [
      "client-scripts/**/*.js",
      "server-scripts/**/*.js"
    ],
    "exclude": [
      "**/*.test.js"
    ],
    "check-coverage": true,
    "per-file": true,
    "branches": 0,
    "lines": 0,
    "functions": 0,
    "statements": 0
  }
}
Sample.js
export default {
  /* istanbul ignore next */
  trigger ({ before }) {
    return before('create')
  },

  exec () {
    return 'Hello World!'
  }
}

Pay attention to the following part:

// vv this line here vv
/* istanbul ignore next */
trigger ({ before }) {
  return before('create')
},

istanbul ignore next excludes the following function from the coverage report.

Sample.test.js
import { expect } from 'chai'
import Sample from './Sample'

describe(__filename, () => {
  describe('Sample exec result', () => {
    it('should return a string', () => {
      expect(Sample.exec()).to.eq("HelloWorld")
    })
  })
})
The above package.json defines three scripts:
  • lint lints the code using the default ES6 standard (can be configured; see here),

  • test:unit runs unit tests defined inside .test.js files (can be configured in the .mocharc.js file),

  • test:unit:cc runs unit tests with code coverage.

The code coverage report gets generated into the coverage directory. For a HTML report inspect the coverage/lcov-report directory.

Usually the http-server package is used to help with this, but a simple "Open in <browser name here>" suffices.

`http-server coverage/lcov-report`