You are reading the documentation for an outdated Corteza release. 2024.9 is the latest stable Corteza release.

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.

Take a look at our samples (Integrator Guide  Automation scripts  Samples) and the sample project to get started.

See whether you can implement your logic using Workflows.

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

These 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.

These 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 automtically).

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
  /auth
      ...
  /admin
      ...
  /compose
      ...
  /messaging
      ...
  /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.

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

/shared contains code that client scripts can reuse.

Script signature

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
  },
}

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 1. 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 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 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, f1or 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 2. 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

Table 3. Available comparison operators:

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

Automation scripts are no exception to the access control system.

The security context allows you to control who can invoke the automation script as well as what access the script should have.

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 performes 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.

This is only available 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`