📘

Enterprise Feature

This feature is only available to customers with an enterprise Platform subscription. Please get in touch to discuss enabling it on your account.

Reactor allows execution of custom code in reaction to a supported event, such as when an action is created, or a Thng's property value is changed. Every application can have a Reactor script attached, and is created the first time a script is uploaded, including any required dependencies.

Reactor scripts are written in Node.js and may use NPM packages via a conventional package.json file. They are automatically provided with the latest version of evrythng-extended.js via a global EVT object and app (EVT.App) scope for requests. You should not remove this basic dependency, or the script may fail to execute.

Once configured, Reactor scripts will run whenever an action is created, a Thng/product property changes value, or a scheduled event is triggered, depending on which callbacks appear in the script. This allows complex logic to occur in the context of virtually any Platform resource update, driving rich integrations and experiences.

Scripts that make use of the async/await language features can be greatly simplified by using the reactor-runasync helper function - see async/await script example below for more details.

📘

Reactor Walkthrough

Check out the Reactor Walkthrough for an in-depth walkthrough of Reactor including example use-cases and patterns for implementation.

📘

SDK Version

As of 27th May 2021, new Reactor scripts will support the latest evrythng.js SDK version ([email protected]^6.0.3). To use it, enable Global Reactor Rules, then update the 'dependencies' to use "evrythng": "^6.0.0" instead of evrythng-extended.

❗️

Legacy SDK Version

Legacy reactor scripts may use up to and including evrythng-extended.js v4.7.2. For new Reactor scripts, it is recommended to use the new evrythng.js SDK.

📘

Node Versions

The version of Node a Reactor script uses depends on when it was created, as new LTS versions are adopted. Use process.version to find which is being used.

Current version: Node v10.x
Until January 9th 2020: Node v8.12
Until May 1st 2019: Node v6.10.2
Until April 13th 2017: Node v4.3


API Status
General Availability:
/projects/:projectId/applications/:applicationId/reactor/script
/projects/:projectId/applications/:applicationId/reactor/script/status
/projects/:projectId/applications/:applicationId/reactor/schedules
/projects/:projectId/applications/:applicationId/reactor/schedules/:scheduleId
/projects/:projectId/applications/:applicationId/reactor/logs


Scripts

There are two ways to upload Reactor scripts to EVRYTHNG Platform.

  1. The simple API allows a script file and an optional NPM dependencies descriptor file (aka package.json) to be uploaded in an update request body.
  2. The bundle API which allows the upload of a full Node.js project as a .zip file.

We'll only consider the bundle API in detail here, since the simple API consists of merely sending the script content, or editing through the Dashboard on the Application Details page. You can update the script through the API or via the Dashboard.

Bundle Structure

Bundled scripts are provided in the form of a .zip file with the following structure.

bundle.zip
 - main.js
 - package.json
 - (otherfile.js)
 - (otherfolder/)
     - (anotherfile.js)
     ...
 ...

package.json is a standard NPM dependencies package.json file, and can be used to specify any other dependencies of the Reactor script.

📘

Notes

  • By default we include evrythng-extended in the package.json file when an application is first created. You must not remove this, or the Reactor may stop working. It is provided to allow you to see which version you are using, and update if necessary.

  • You must not include any node_modules in the bundle. The package.json file can be used to specify these dependencies.

  • devDependencies in package.json are ignored by Reactor when a bundle is uploaded and installed.

If the file is not provided, or no dependency to evrythng-extended.js is specified, the latest version of evrythng-extended.js will be automatically included. It is strongly advised to explicitly state which specific version of the library you want to use to avoid problems when new versions are released.


Event Types

This section lists the types of events a Reactor script can be launched in response to.

🚧

Important

  • All event handlers must call done() just before exiting. This will tell the Platform that everything is complete and that we are not waiting on any other pending callback. This will also commit the log lines that have been recorded using logger, and make them available via the Dashboard and the API.

  • When an action or resource update occurs that will trigger a Reactor script, all application Reactor scripts within the resource's project scope will run for that event type, if applicable.

  • onActionCreated - When an action is created.

  • onProductPropertiesChanged - When one or more properties of a product have changed.

  • onThngPropertiesChanged - When one or more properties of a Thng have changed.

  • onScheduledEvent - When a scheduled event has occurred. If function in the schedule uploaded is a custom function name, that name will be invoked instead, but only if it has been exported from the script.


onActionCreated

This occurs every time an action is created and is visible to the application. The event supplied to the handler is as follows:

.action (ActionDocument)

.triggerApp (string, read-only)
    The triggering application.

.triggerUser (string, read-only)
    The triggering Application User, if any.

.triggerUserAnonymous (boolean, read-only)
    `true` if the triggering Application User is anonymous, 
    `false` otherwise.
{
  "type": "object",
  "description": "The event parameter passed to an `onActionCreated` event callback.",
  "properties": {
    "action": { "$ref": "ActionDocument" },
    "triggerApp": {
      "type": "string",
      "description": "The triggering application.",
      "pattern": "^[abcdefghkmnpqrstwxyABCDEFGHKMNPQRSTUVWXY0123456789]{24}$",
      "readOnly": true
    },
    "triggerUser": {
      "type": "string",
      "description": "The triggering Application User, if any.",
      "pattern": "^[abcdefghkmnpqrstwxyABCDEFGHKMNPQRSTUVWXY0123456789]{24}$",
      "readOnly": true
    },
    "triggerUserAnonymous": {
      "type": "boolean",
      "description": "`true` if the triggering Application User is anonymous, `false` otherwise.",
      "readOnly": true
    }
  }
}
{
  "action": {
    "id": "Una8MNVreDswQKwRahUhdymr",
    "createdAt": 1510919501111,
    "customFields": null,
    "tags": null,
    "scopes": {
      "users": [
        "10ff93f061029f0a20273005"
      ],
      "projects": [
        "all"
      ]
    },
    "timestamp": 1510919501111,
    "type": "implicitScans",
    "user": "10ff93f061029f0a20273005",
    "location": {
      "place": null,
      "latitude": 51.4333,
      "longitude": 0.1833,
      "position": {
        "type": "Point",
        "coordinates": [
          0.1833,
          51.4333
        ]
      },
      "customFields": null
    },
    "locationSource": "geoIp",
    "device": null,
    "context": {
      "ipAddress": "141.0.154.202",
      "city": "Crayford",
      "region": "England",
      "countryCode": "GB",
      "userAgent": null,
      "referer": null,
      "language": null,
      "userAgentName": "Unknown",
      "userAgentVersion": null,
      "userAgentType": null,
      "deviceType": null,
      "device": null,
      "operatingSystemName": "Unknown",
      "operatingSystemFamily": null,
      "operatingSystemProducer": null,
      "operatingSystemVersion": null,
      "timeZone": "Europe/London"
    },
    "reactions": [
      {
        "type": "redirection",
        "customFields": null,
        "redirectUrl": "https://google.com",
        "redirectionContext": {
          "constants": {
            "constant_key": "constant_value"
          }
        }
      }
    ],
    "createdByProject": "10ff93f061029f0a20273005",
    "createdByApp": "10ff93f061029f1a20273005",
    "createdByThng": null,
    "identifiers": null,
    "thng": "UFqMRDbaqm8EEMRaRF5h7cEg",
    "product": "U2E9hmyCVg8atpRwwXgTqcrt",
    "shortId": null,
    "shortDomain": null
  },
  "triggerApp": "10ff93f061029f1a20273005",
  "triggerUser": "10ff93f061029f0a20273005",
  "triggerUserAnonymous": true
}

Reactor Filters: thng.*, product.*, user.*, time, Time, action.*, Location, place.*.

See also: ActionDocument


onProductPropertiesChanged

This occurs every time the properties of a product have been changed. The event supplied to the handler is as shown below.

If multiple property updates are provided in one update request the behavior is slightly different. See the Multiple Property Updates for more information.

.product (ProductDocument)

.changes (object)
    Object containing a key for each changed property, with 
    value of a `ReactorPropertyChangeDocument`.

.triggerApp (string, read-only)
    The triggering application.

.triggerUser (string, read-only)
    The triggering Application User, if any.

.triggerUserAnonymous (boolean, read-only)
    `true` if the triggering Application User is anonymous, 
    `false` otherwise.
{
  "type": "object",
  "description": "The event parameter passed to an `onProductPropertiesChanged` event callback.",
  "properties": {
    "product": { "$ref": "ProductDocument" },
    "changes": {
      "type": "object",
      "description": "Object containing a key for each changed property, with value of a `ReactorPropertyChangeDocument`."
    },
    "triggerApp": {
      "type": "string",
      "description": "The triggering application.",
      "pattern": "^[abcdefghkmnpqrstwxyABCDEFGHKMNPQRSTUVWXY0123456789]{24}$",
      "readOnly": true
    },
    "triggerUser": {
      "type": "string",
      "description": "The triggering Application User, if any.",
      "pattern": "^[abcdefghkmnpqrstwxyABCDEFGHKMNPQRSTUVWXY0123456789]{24}$",
      "readOnly": true
    },
    "triggerUserAnonymous": {
      "type": "boolean",
      "description": "`true` if the triggering Application User is anonymous, `false` otherwise.",
      "readOnly": true
    }
  }
}
{
  "changes": {
    "is_packed": {
      "oldValue": true,
      "newValue": false,
      "timestamp": 1510930465583
    }
  },
  "product": {
    "id": "UGxqfDDpeg8aQKRaahWKKcSb",
    "createdAt": 1495029355783,
    "customFields": {
      "key": "value"
    },
    "tags": [
      "some",
      "tags"
    ],
    "scopes": {
      "users": [
        "all"
      ],
      "projects": [
        "UmxHK6K8BXsa9KawRh4bTbqc",
        "UmSqCDt5BD8atKRRagdqUnAa"
      ]
    },
    "updatedAt": 1510055403742,
    "brand": null,
    "categories": null,
    "properties": {
      "test": 84,
      "is_packed": false
    },
    "description": "Example description text",
    "fn": "Example Product",
    "name": "Example Product",
    "photos": null,
    "url": null,
    "identifiers": {
      "key": "value"
    }
  },
  "triggerApp": null,
  "triggerUserAnonymous": false,
  "triggerUser": null
}

Reactor Filters: product.*, time, Property.

See also: ProductDocument, ReactorPropertyChangeDocument


onThngPropertiesChanged

This occurs every time the properties of a Thng have been changed.

📘

Note

If the new value is the same as the old value, this event will not fire.

The event supplied to the handler is as shown below.

If multiple property updates are provided in one update request the behavior is slightly different. See the Multiple Property Updates for more information.

.thng (ThngDocument)

.changes (object)
    Object containing a key for each changed property, with 
    value of a `ReactorPropertyChangeDocument`.

.triggerApp (string, read-only)
    The triggering application.

.triggerUser (string, read-only)
    The triggering Application User, if any.

.triggerUserAnonymous (boolean, read-only)
    `true` if the triggering Application User is anonymous, 
    `false` otherwise.
{
  "type": "object",
  "description": "The event parameter passed to an `onThngPropertiesChanged` event callback.",
  "properties": {
    "thng": { "$ref": "ThngDocument" },
    "changes": {
      "type": "object",
      "description": "Object containing a key for each changed property, with value of a `ReactorPropertyChangeDocument`."
    },
    "triggerApp": {
      "type": "string",
      "description": "The triggering application.",
      "pattern": "^[abcdefghkmnpqrstwxyABCDEFGHKMNPQRSTUVWXY0123456789]{24}$",
      "readOnly": true
    },
    "triggerUser": {
      "type": "string",
      "description": "The triggering Application User, if any.",
      "pattern": "^[abcdefghkmnpqrstwxyABCDEFGHKMNPQRSTUVWXY0123456789]{24}$",
      "readOnly": true
    },
    "triggerUserAnonymous": {
      "type": "boolean",
      "description": "`true` if the triggering Application User is anonymous, `false` otherwise.",
      "readOnly": true
    }
  }
}
{
  "changes": {
    "is_online": {
      "oldValue": true,
      "newValue": false,
      "timestamp": 1510855251930
    }
  },
  "thng": {
    "id": "UnRKTWQ9MQtBhsaRah2sCBsg",
    "createdAt": 1510680063133,
    "customFields": {},
    "tags": null,
    "scopes": {
      "users": [
        "all"
      ],
      "projects": [
        "UmxHK6K8BXsa9KawRh4bTbqc"
      ]
    },
    "updatedAt": 1510763814549,
    "name": "Example Thng",
    "description": null,
    "location": null,
    "product": "UHaqwfVEq3PYEMwwa2Apyc7h",
    "properties": {
      "~connected": false,
      "is_online": false
    },
    "identifiers": {},
    "collections": null,
    "batch": null,
    "createdByTask": null
  },
  "triggerApp": null,
  "triggerUserAnonymous": false,
  "triggerUser": null
}

Reactor Filters: thng.*, product.*, time, Property.

See also: ThngDocument,
ReactorPropertyChangeDocument


onScheduledEvent

When a scheduled event occurs, the only argument provided to the callback will be the event that was specified in the Reactor schedule resource when it was created.

ReactorScheduleDocument.event
{
  "foo": "bar"
}

See also: ReactorScheduleDocument


ReactorPropertyChangeDocument Data Model

This object contains the changes that occurred as an object of keys, each containing the old and new values.

.oldValue (string|number|object)
    The previous property value.

.newValue (string|number|object)
    The new property value

.timestamp (integer)
    The time that the update occured.
{
  "type": "object",
  "description": "Object describing the property changes for this event.",
  "properties": {
    "oldValue": { "description": "The previous property value." },
    "newValue": { "description": "The new property value" },
    "timestamp": {
      "type": "integer",
      "description": "The time that the update occured.",
      "minimum": 1508761430000
    }
  }
}

Multiple Property Updates

If there is more than one update to a given Thng or product property in an update request, when the onThngPropertiesChanged or onProductPropertiesChanged callback is invoked an allChanges property is included. This will be an array containing all the updates for that property change request.

The oldValue, newValue and timestamp properties at the changed key level will reflect the single latest property update by timestamp. An example is shown below:

{
  "changes": {
    "startup_count": {
      "allChanges": [
        {
          "oldValue": 47,
          "newValue": 48,
          "timestamp": 1511347362000
        },
        {
          "oldValue": 48,
          "newValue": 49,
          "timestamp": 1511347466000
        },
        {
          "oldValue": 49,
          "newValue": 50,
          "timestamp": 1511348089000
        }
      ],
      "oldValue": 47,
      "newValue": 50,
      "timestamp": 1511348089000
    }
  },
  "thng": {
    "id": "UG95xVhAVMPrQraRaDYmrafd",
    "createdAt": 1509626996555,
    "updatedAt": 1510760476081,
    "name": "Smart Device Thng",
    "properties": {
      "startup_count": 47,
      "lastInteraction": "Guest User"
    }
  },
  "triggerUserAnonymous": false,
  "triggerUser": null,
  "triggerApp": null
}

Script API

Reactor scripts are packaged with the latest version of evrythng-extended.js (unless you explicitly specify the version you want to use). It is pre-configured with the application's Trusted Application API Key.

The table below describes the global variables made available to you within the Reactor script context:

Name

Description

EVT

Preconfigured instance of evrythng-extended.js module. See SDK documentation.

app

Preconfigured instance of the EVT.App scope. See SDK documentation.

Note: Uniquely inside a Reactor script, the application's Trusted Application API Key can be read from the app.apiKey property.

done

Function that must be called when the script has finished, so that logs can be output. If this is not called, no logs will be shown.

logger

The logger available to use to output logs. console.log() will not be visible in output logs. Available methods are .info(), .error(), and .warn(). Logs will not appear in the Dashboard unless done() is called in all cases.

evrythng-extended.js will also forward the X-Evrythng-Reactor header, which is used to protect you against inadvertently create a recursive loop (see Limits: Recursion).

If you re-configure evrythng-extended.js or use an alternate way to communicate with the API, make sure you forward this header to prevent any possible recursive loops.


Debugging

You can create log messages from the Reactor script. A logger instance is globally defined and available anywhere in the Reactor script. Use the following functions available to create log messages of differing levels of severity.

Function

Desciption

logger.debug(message);

Write a log message with debug level

logger.info(message);

Write a log message with info level

logger.warn(message);

Write a log message with warning level

logger.error(message);

Write a log message with error level

📘

Note

Reactor log entries will be available for up to seven (7) days after they are generated.


Filters

In most cases you don't want your Reactor scripts to be executed on every event (action creation or property change) but only if some conditions are met. For example, suppose we only want to react to action creation of type scans and ignore the others. One obvious solution is to use an if construction within the Reactor script.

// BAD - Wasteful, scripts runs even if type is not 'scans'

function onActionCreated(event) {
  if(event.action.type !== 'scans') return done();

  logger.debug('Scan action created');
  done();
}

The problem with this example is that for non-scan actions the Reactor script is still executed but doing nothing useful. It is not a good practice since the Reactor script execution cost is non-trivial.

The recommended solution is to use the Reactor filters annotation syntax, an example of which is shown below to run the script only when the action's type is scans.

// GOOD - @filter means this is only called for action type 'scans' or '_Offer'

// @filter(onActionCreated) action.type=scans,_Offer
function onActionCreated(event) {
  logger.debug('Action created');
  done();
}

Reactor filters functionality is limited compared to full javascript but they are evaluated on an earlier step before a script is executed, and so are much less costly.


Syntax

Reactor filters are defined as Javascript comments (both // and /* */ styles are supported). The filter syntax is as described on the Filters page. Filters can be defined anywhere in the script, however, it is a good idea to place each directly above the corresponding callback function definition. If using the bundle Reactor type, filters should be defined in main.js file.

📘

Note

Unlike regular Platform filters, the logical OR operator is supported in Reactor filters for valid fields. Do this using the pipe (|) symbol. For example:

// @filter(onActionCreated) action.type=_Exec|action.customFields.filtered=true

The normal logical AND operator can be used as well in the following manner:

// @filter(onActionCreated) action.type=_Exec&action.customFields.filtered=true

The templates for filtering the three main callback types are shown below.

// @filter(onActionCreated) action.type=scans
function onActionCreated(event) {
  doSomething(event)
    .catch(err => logger.error(err.message || err.errors[0]))
    .then(done);
}

// @filter(onProductPropertiesChanged) propertyChangeNew.used=*
function onProductPropertiesChanged(event) {
  doSomething(event)
    .catch(err => logger.error(err.message || err.errors[0]))
    .then(done);
}

// @filter(onThngPropertiesChanged) propertyChangeNew.temp_c=*
function onThngPropertiesChanged(event) {
  doSomething(event)
    .catch(err => logger.error(err.message || err.errors[0]))
    .then(done);
}

We will do our best to extract filter definitions from your Reactor script. However, if we failed for any reason, no error is raised. Reactor script functions will be executed as if the filter has not been defined.

📘

Note

Only one filter per function is allowed.


Filter Parameters

This is a list of parameters that can be used in the Reactor filter body, although not all of them are be available in a given context. See the Event Types section to see which are available for which callback.

Thng

  • thng.id
    The action Thng’s ID.

  • thng.identifiers.{identifierName}
    The action Thng’s identifiers.

  • thng.name
    The action Thng’s name.

  • thng.customFields.{customFieldName}
    The action Thng’s custom fields.

  • thng.tags
    The action Thng’s tags. Lists of tags will match for any of the listed tags (an OR condition).

Product

  • product.id
    The action product’s ID.

  • product.identifiers.{identifierName}
    The action product’s identifiers.

  • product.name
    The action product’s name.

  • product.customFields.{customFieldName}
    The action product’s custom fields.

  • product.tags
    The action product’s tags. Lists of tags will match for any of the listed tags (an OR condition).

User

  • user.gender
    The action user’s gender.

  • user.age
    The action user’s years of age.

  • user.customFields.{customFieldName}
    The action user’s custom fields.

  • user.locale
    The action user’s locale.

Time

  • time
    Number of milliseconds elapsed since Epoch at the time the action occurred.

  • timeOfDay
    Number of milliseconds elapsed since midnight in the action’s timezone.

  • dayOfWeek
    Day of the week in the action’s timezone. Possible values are mon, tue, wed, thu, fri, sat, sun.

  • dayOfMonth
    Day of the month in the action’s timezone. Possible values are 1 to 31.

  • month
    Month in the action’s timezone. Possible values are 1 to 12.

  • year
    Year in the action’s timezone.

Action

  • action.customFields.{customFieldName}
    The action’s custom fields.

  • action.type
    The action type

Property

  • propertyChangeOld.{propertyKey}
    The old value of the property that changed.

  • propertyChangeNew.{propertyKey}
    The new value of the property that changed.

Location

  • timezone
    The timezone where the action occurred. See the list of possible timezones.

  • location.lat
    The location latitude in degrees where the action occurred.

  • location.lon
    The location longitude in degrees where the action occurred.

  • country
    ISO 3166-1 alpha-2 country code where the action occurred

Place

  • place.id
    The place’s ID.

  • place.tags
    The place’s tags.

  • place.customFields.{customFieldName}
    The place’s custom fields.

  • place.distance
    The place distance in meters from the scan.


Limits

Execution Timeout

When triggered, a Reactor script must complete within a certain amount of time. If this is not the case, the execution is aborted. See below for time constraints.

Recursion

Given that a Reactor script may trigger itself by doing an operation against the REST API, which causes the script to trigger again (directly, or indirectly), there is a mechanism in place to protect the user from inadvertently causing an infinite recursion loop.

For example, if a script is triggered by the creation of an action, and if that script creates an action during its execution, this will trigger itself again, and so on.

We therefore limit the depth of such recursive calls as well as the total time allowed for the execution since the original event.

📘

Note

This is provided on a best effort basis. It won't protect a user from intentionally defeating this safe-guard.

Build Timeout

The time required to download and package all the dependencies must not exceed the Build Timeout (see below). The build time inevitably depends on a lot of external factors, thus the timeout here is provided for guidance only.

Script Size

The scripts before and after build are limited in size.

Limit

Value

Execution Timeout

60 seconds

Recursion Timeout

120 seconds

Recursion Max Depth

5 calls deep

Build Timeout

5 minutes

Script Size Before Build

2 MB

Script Size After Build

10 MB


Reactor Script API

The Reactor Script API allows you to view and replace the Reactor script for an application via the REST API instead of via the application details page in the Dashboard. You can also view the status of an upload in progress, and also view and replace the manifest declaring the script's dependencies.

Jump To↓

Update the Reactor Script
Read the Reactor Script
Read the Reactor Script Status


ReactorScriptDocument Data Model

.createdAt (integer, read-only)
    Timestamp when the resource was created.

.updatedAt (integer, read-only)
    Timestamp when the resource was updated.

.type (string, read-only, one of 'simple', 'bundle')
    The type of Reactor script.

.script (string, required)
    The Reactor script body.

.manifest (string)
    The Reactor script manifest, in a format similar to Node's 
    `package.json`.
{
  "type": "object",
  "description": "Object containing a Reactor script and associated metadata.",
  "required": ["script"],
  "properties": {
    "createdAt": {
      "type": "integer",
      "description": "Timestamp when the resource was created.",
      "readOnly": true,
      "minimum": 0
    },
    "updatedAt": {
      "type": "integer",
      "description": "Timestamp when the resource was updated.",
      "readOnly": true,
      "minimum": 0
    },
    "type": {
      "type": "string",
      "description": "The type of Reactor script.",
      "enum": [ "simple", "bundle" ],
      "readOnly": true
    },
    "script": {
      "type": "string",
      "description": "The Reactor script body."
    },
    "manifest": {
      "type": "string",
      "description": "The Reactor script manifest, in a format similar to Node's `package.json`."
    }
  }
}
{
  "createdAt": 1504776062986,
  "updatedAt": 1504776062986,
  "type": "simple",
  "script": "// When an action is created\nfunction onActionCreated(event) {\n  logger.info('Action created:\\n' + JSON.stringify(event));\n  done();\n}\n\n// When a Thng's properties have changed\nfunction onThngPropertiesChanged(event) {\n  logger.info('Thng properties changed:\\n' + JSON.stringify(event));\n  done();\n}\n\n// When a product's properties have changed\nfunction onProductPropertiesChanged(event) {\n  logger.info('Product properties changed:\\n' + JSON.stringify(event));\n  done();\n}\n\n// When a Reactor Schedule runs\nfunction onScheduledEvent(event) {\n  logger.info('Scheduled event:\\n' + JSON.stringify(event));\n  done();\n}",
  "manifest": "{\n    \"dependencies\": {\n        \"evrythng-extended\": \"^4.1.0\"\n    }\n}"
}

Update the Reactor Script

PUT a new ReactorScriptDocument to update the script associated with an applicationId.

Simple Request

PUT /projects/:projectId/applications/:applicationId/reactor/script
Content-Type: application/json
Authorization: $OPERATOR_API_KEY

ReactorScriptDocument (subset)
curl -i -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Authorization: $OPERATOR_API_KEY" \
  -X PUT 'https://api.evrythng.com/projects/U2meqbNWegsaQKRRaDUmpssr/applications/UF3Vqb7D6G8EhMwaRYgQ2pFc/reactor/script' \
  -d '{
  "script":"function onActionCreated(event) {\n logger.debug(\u0027Hello World!\u0027);\n done();}"
  }'
const projectId = '';
const applicationId = '';
const payload = { 
  script: 'function onActionCreated(event) {\n logger.debug(\'Hello World!\');\n done();}' 
};

operator.project(projectId).application(applicationId)
  .reactorScript()
  .update(payload)
  .then(console.log);

Bundle Request

PUT /projects/:projectId/applications/:applicationId/reactor/script
Authorization: $OPERATOR_API_KEY
Content-Type: multipart/form-data

ReactorBundleArchiveContent
curl -i \
  -H "Content-Type: multipart/form-data" \
  -H "Authorization: $OPERATOR_API_KEY" \
  -X PUT 'https://api.evrythng.com/projects/U2meqbNWegsaQKRRaDUmpssr/applications/UF3Vqb7D6G8EhMwaRYgQ2pFc/reactor/script' \
  -F [email protected]/bundle.zip

Response

HTTP/1.1 200 OK
Content-type: application/json

{
  "createdAt":1424859509100,
  "updatedAt":1424859509100,
  "type":"simple",
  "script":"function onActionCreated(event) {\n logger.debug('Hello World!');\n done();}"
}
HTTP/1.1 200 OK
Content-type: application/json

{
  "createdAt":1424859509100,
  "updatedAt":1424859509100,
  "type":"bundle"
}

Read the Reactor Script

Read the Reactor script associated with an application.

GET /projects/:projectId/applications/:applicationId/reactor/script
Authorization: $OPERATOR_API_KEY
curl -H "Authorization: $OPERATOR_API_KEY" \
  -X GET 'https://api.evrythng.com/projects/U2meqbNWegsaQKRRaDUmpssr/applications/UF3Vqb7D6G8EhMwaRYgQ2pFc/reactor/script'
const projectId = 'U2meqbNWegsaQKRRaDUmpssr';
const applicationId = 'UF3Vqb7D6G8EhMwaRYgQ2pFc';

operator.project(projectId).application(applicationId)
  .reactorScript()
  .read()
  .then(console.log);
HTTP/1.1 200 OK
Content-Type: application/json

{
  "createdAt": 1480523297824,
  "updatedAt": 1495012802848,
  "type": "simple",
  "script": "function onActionCreated(event) {\n  console.log(JSON.stringify(event));\n}",
  "manifest": "{\n    \"dependencies\": {\n        \"evrythng-extended\": \"^4.1.0\"\n    }\n}"
}

ReactorScriptStatusDocument Data Model

.updating (boolean, read-only)
    If the Reactor script is updating at the moment.

.error (string, read-only)
    Error message if failed to update the Reactor script.
{
  "type": "object",
  "description": "Object describing the status of the Reactor script.",
  "properties": {
    "updating": {
      "type": "boolean",
      "description": "If the Reactor script is updating at the moment.",
      "readOnly": true
    },
    "error": {
      "type": "string",
      "description": "Error message if failed to update the Reactor script.",
      "readOnly": true
    }
  }
}

Read the Reactor Script Status

Read the status of a Reactor script update in progress.

GET /projects/:projectId/applications/:applicationId/reactor/script/status
Authorization: $OPERATOR_API_KEY
curl -H "Authorization: $OPERATOR_API_KEY" \
  -X GET 'https://api.evrythng.com/projects/U2meqbNWegsaQKRRaDUmpssr/applications/UF3Vqb7D6G8EhMwaRYgQ2pFc/reactor/script/status'
const projectId = 'U2meqbNWegsaQKRRaDUmpssr';
const applicationId = 'UF3Vqb7D6G8EhMwaRYgQ2pFc';

operator.project(projectId).application(applicationId)
  .reactorScript()
  .status()
  .read()
  .then(console.log);
HTTP/1.1 200 OK
Content-type: application/json

{
  "updating": false
}

Reactor Scheduler API

Jump To ↓

Create a Reactor Schedule
Read all Reactor Schedules
Read a Single Reactor Schedule
Update a Reactor Schedule
Delete a Reactor Schedule

In addition to the three standard event entry points into a Reactor script, any developer-defined function can be scheduled to be called at a later time. When the function is called, it is passed a pre-defined event object assigned at the time of scheduling.

The scheduled event can be either one-shot execution (delayed, scheduled, or immediate), or it can be a repetitive event, defined by a cron expression. The format of this expression is as follows:

"cron": "*  *  *  *  *  *"
         |  |  |  |  |  |
         |  |  |  |  |  day of the week (1 - 7)
         |  |  |  |  month of the year (1 - 12)
         |  |  |  day of the month (1 - 31)
         |  |  hour of the day (0 - 23)
         |  minute past the hour (0 - 59)
         second past the minute (0 - 59)

See this tutorial for more format details and special options available. The list below shows some example cron expressions for common scenarios:

  • "0 0 12 * * ?"
    Run at 12 noon every day, every month, regardless of the day of the week.

  • "0 0/30 * * * ?"
    Run at 0 and 30 minutes past the hour, every hour, every day, every month, regardless of the day of the week.

  • "0 0 0 1 * ?"
    Run at midnight on the first day of the month, every month, regardless of the day of the week.

ReactorScheduleDocument Data Model

.event (object, required)
    Object to be passed as the parameter to the callback when it 
    is invoked.

.id (string, read-only)
    The ID of this resource.

.createdAt (integer, read-only)
    Timestamp when the resource was created.

.updatedAt (integer, read-only)
    Timestamp when the resource was updated.

.function (string)
    The name of the exported function to invoke.

.executeAt (integer)
    Unix timestamp (milliseconds) describing when to execute the 
    scheduled event.

.cron (string)
    Cron expression describing the execution interval.

.description (string)
    Friendly description of this resource.

.enabled (boolean)
    If the schedule is enabled. Defaults to `true`.
{
  "additionalProperties": false,
  "type": "object",
  "description": "An object describing a Reactor schedule. Either 'cron' or 'executeAt' is required.",
  "required": ["event"],
  "properties": {
    "event": {
      "type": "object",
      "description": "Object to be passed as the parameter to the callback when it is invoked."
    },
    "id": {
      "type": "string",
      "description": "The ID of this resource.",
      "pattern": "^[abcdefghkmnpqrstwxyABCDEFGHKMNPQRSTUVWXY0123456789]{24}$",
      "readOnly": true
    },
    "createdAt": {
      "type": "integer",
      "description": "Timestamp when the resource was created.",
      "readOnly": true,
      "minimum": 0
    },
    "updatedAt": {
      "type": "integer",
      "description": "Timestamp when the resource was updated.",
      "readOnly": true,
      "minimum": 0
    },
    "function": {
      "type": "string",
      "description": "The name of the exported function to invoke."
    },
    "executeAt": {
      "type": "integer",
      "description": "Unix timestamp (milliseconds) describing when to execute the scheduled event.",
      "minimum": 0
    },
    "cron": {
      "type": "string",
      "description": "Cron expression describing the execution interval."
    },
    "description": {
      "type": "string",
      "description": "Friendly description of this resource."
    },
    "enabled": {
      "type": "boolean",
      "description": "If the schedule is enabled. Defaults to `true`."
    }
  }
}
{
  "id": "U3BA3hCeeD8wtKwwwXcdhcNh",
  "createdAt": 1492010433622,
  "updatedAt": 1492010433622,
  "event": {
    "region": "europe"
  },
  "function": "onScheduledEvent",
  "executeAt": 1492010673000,
  "description": "Example Reactor schedule",
  "enabled": true
}

📘

Notes

  • Only one of executeAt or cron is allowed, or none.

  • We can only guarantee minute-precision invocation for executeAt, but smaller granularities will generally execute as required.

  • If executeAt is not in the future, then the event will be executed immediately.

  • We perform some validation of cron expressions, and allow a lowest granularity of 2 minutes. Cron time is in UTC.

  • By default we assume the function name is onScheduledEvent. We will take care of exporting an onScheduledEvent function if it is defined in the script. If you are using another function, it should be explicitly exported.

  • If you override module.exports and omit your schedule function handler (including the default onScheduledEvent) then the handler will not be called.


Create a Reactor Schedule

Create a new Reactor schedule by providing a ReactorScheduleDocument describing how it should behave.

POST /projects/:projectId/applications/:applicationId/reactor/schedules
Content-Type: application/json
Authorization: $TRUSTED_APPLICATION_API_KEY

ReactorScheduleDocument
curl -i -H "Content-Type: application/json" \
  -H "Authorization: $TRUSTED_APPLICATION_API_KEY" \
  -X POST 'https://api.evrythng.com/projects/U2meqbNWegsaQKRRaDUmpssr/applications/UF3Vqb7D6G8EhMwaRYgQ2pFc/reactor/schedules' \
  -d '{ 
    "function": "onScheduledEvent", 
    "event": {
      "region": "europe"
    }, 
    "cron": "0 0 * * * ?", 
    "description": "Example Reactor schedule", 
    "enabled": true 
  }'
const payload = { 
  function: 'onScheduledEvent', 
  event: {
    region: 'europe'
  }, 
  cron: '0 0 * * * ?',
  description: 'Example Reactor schedule',
  enabled: true 
};

trustedApp.reactorSchedule().create(payload)
  .then(console.log);
HTTP/1.1 201 Created
Content-Type: application/json
Location: https://api.evrythng.com/projects/U2meqbNWegsaQKRRaDUmpssr/applications/UF3Vqb7D6G8EhMwaRYgQ2pFc/reactor/schedules/U3BA3hCeeD8wtKwwwXcdhcNh

{
  "id": "U3BA3hCeeD8wtKwwwXcdhcNh",
  "createdAt": 1492010433622,
  "updatedAt": 1492010433622,
  "event": {
    "region": "europe"
  },
  "function": "onScheduledEvent",
  "cron": "0 0 * * * ?",
  "description": "Example Reactor schedule",
  "enabled": true
}

Read all Reactor Schedules

Read all the current schedules of the specified application.

GET /projects/:projectId/applications/:applicationId/reactor/schedules
Authorization: $TRUSTED_APPLICATION_API_KEY
curl -H "Authorization: $TRUSTED_APPLICATION_API_KEY" \
  -X GET 'https://api.evrythng.com/projects/U2meqbNWegsaQKRRaDUmpssr/applications/UF3Vqb7D6G8EhMwaRYgQ2pFc/reactor/schedules'
trustedApp.reactorSchedule()
  .read()
  .then(console.log);
HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "id": "UE9Bt3CyM4gd87g4rHYSBqTc",
    "createdAt": 1475672402665,
    "updatedAt": 1475672402665,
    "event": {
      "region": "europe"
    },
    "function": "onScheduledEvent",
    "cron": "0 0 * * * ?",
    "description": "Example Reactor schedule",
    "enabled": true
  }
]

Read a Single Reactor Schedule

Read a single Reactor schedule with its corresponding schedule ID.

GET /projects/:projectId/applications/:applicationId/reactor/schedules/:scheduleId
Authorization: $TRUSTED_APPLICATION_API_KEY
curl -H "Authorization: $TRUSTED_APPLICATION_API_KEY" \
  -X GET 'https://api.evrythng.com/projects/U2meqbNWegsaQKRRaDUmpssr/applications/UF3Vqb7D6G8EhMwaRYgQ2pFc/reactor/schedules/UE9Bt3CyM4gd87g4rHYSBqTc'
const scheduleId = 'UE9Bt3CyM4gd87g4rHYSBqTc';

trustedApp.reactorSchedule(scheduleId).read()
  .then(console.log);
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "UE9Bt3CyM4gd87g4rHYSBqTc",
  "createdAt": 1475672402665,
  "updatedAt": 1475672402665,
  "event": {
    "region": "europe"
  },
  "function": "onScheduledEvent",
  "cron": "0 0 * * * ?",
  "description": "Example Reactor schedule",
  "enabled": true
}

Update a Reactor Schedule

Update the fields of an existing Reactor schedule document by its schedule ID.

PUT /projects/:projectId/applications/:applicationId/reactor/schedules/:scheduleId
Content-Type: application/json
Authorization: $TRUSTED_APPLICATION_API_KEY

ReactorScheduleDocument (subset)
curl -i -H "Content-Type: application/json" \
  -H "Authorization: $TRUSTED_APP_APPI_KEY" \
  -X PUT 'https://api.evrythng.com/projects/U2meqbNWegsaQKRRaDUmpssr/applications/UF3Vqb7D6G8EhMwaRYgQ2pFc/reactor/schedules/UE9Bt3CyM4gd87g4rHYSBqTc' \
    -d '{ 
      "enabled": false 
    }'
const scheduleId = 'UE9Bt3CyM4gd87g4rHYSBqTc';
const payload = { enabled: false };

trustedApp.reactorSchedule(scheduleId).update(payload)
  .then(console.log);
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "UE9Bt3CyM4gd87g4rHYSBqTc",
  "createdAt": 1475672402665,
  "updatedAt": 1475674411237,
  "event": {
    "region": "europe"
  },
  "function": "onScheduledEvent",
  "cron": "0 0 * * * ?",
  "description": "Updated example Reactor schedule",
  "enabled": false
}

Delete a Reactor Schedule

Delete a Reactor schedule using its schedule ID.

📘

Note

A one-shot scheduled event (using executeAt instead of cron) will be deleted after it has elapsed.

DELETE /projects/:projectId/applications/:applicationId/reactor/schedules/:scheduleId
Authorization: $TRUSTED_APPLICATION_API_KEY
curl -H "Authorization: $TRUSTED_APPLICATION_API_KEY" \
  -X DELETE 'https://api.evrythng.com/projects/U2meqbNWegsaQKRRaDUmpssr/applications/UF3Vqb7D6G8EhMwaRYgQ2pFc/reactor/schedules/UE9Bt3CyM4gd87g4rHYSBqTc'
const scheduleId = 'UYQe9FQxM4DUsNDnrnYxdqcc';

trustedApp.reactorSchedule(scheduleId).delete()
  .then(() => console.log('Schedule deleted.'));
HTTP/1.1 200 OK

Reactor Logs API

Jump To ↓

ReactorLogEntryDocument Data Model
Read the Reactor Logs
Delete the Reactor Logs

The Reactor logs emitted by using the logger object in the script itself can be read via the EVRYTHNG API. The Reactor logs are created automatically after execution of the Reactor script providing done() was called to mark the end of script execution. If this is not called, logs will not be created.

📘

Note

Reactor log entries will be available for up to seven (7) days after they are generated.


ReactorLogEntryDocument Data Model

.id (string, read-only)
    The ID of this resource.

.app (string, read-only)
    The ID of the application.

.logLevel (string, read-only, one of 'trace', 'debug', 'info', 'warn', 'error')
    The level of the log message.

.createdAt (integer, read-only)
    Timestamp when the resource was created.

.timestamp (integer, read-only)
    When the log entry appeared

.message (string, read-only)
    The content of the log entry.
{
  "type": "object",
  "description": "A Reactor log entry.",
  "properties": {
    "id": {
      "type": "string",
      "description": "The ID of this resource.",
      "pattern": "^[abcdefghkmnpqrstwxyABCDEFGHKMNPQRSTUVWXY0123456789]{24}$",
      "readOnly": true
    },
    "app": {
      "type": "string",
      "description": "The ID of the application.",
      "readOnly": true,
      "pattern": "^[abcdefghkmnpqrstwxyABCDEFGHKMNPQRSTUVWXY0123456789]{24}$"
    },
    "logLevel": {
      "type": "string",
      "description": "The level of the log message.",
      "enum": [ "trace", "debug", "info", "warn", "error" ],
      "readOnly": true
    },
    "createdAt": {
      "type": "integer",
      "description": "Timestamp when the resource was created.",
      "readOnly": true,
      "minimum": 0
    },
    "timestamp": {
      "type": "integer",
      "description": "When the log entry appeared",
      "readOnly": true
    },
    "message": {
      "type": "string",
      "description": "The content of the log entry.",
      "readOnly": true
    }
  },
  "x-filterable-fields": [ "app", "logLevel", "timestamp" ]
}
{
  "id": "UHRMhAkn6mshhMawwhNE8ftt",
  "createdAt": 1510765450609,
  "logLevel": "info",
  "message": "Action created!",
  "app": "U3pxRQh2eD8RtKwaRgerfQgc",
  "timestamp": 1510765450415
}

Read the Reactor Logs

Reads the Reactor logs.

GET /projects/:projectId/applications/:applicationId/reactor/logs
Authorization: $OPERATOR_API_KEY
curl -H "Authorization: $OPERATOR_API_KEY" \
  -X GET 'https://api.evrythng.com/projects/U2meqbNWegsaQKRRaDUmpssr/applications/UF3Vqb7D6G8EhMwaRYgQ2pFc/reactor/logs'
const projectId = 'U2meqbNWegsaQKRRaDUmpssr';
const applicationId = 'UF3Vqb7D6G8EhMwaRYgQ2pFc';

operator.project(projectId).application(applicationId)
  .reactorLog()
  .read()
  .then(console.log);
HTTP/1.1 200 OK
Content-type: application/json

[
  {
    "id": "UHRMhAkn6mshhMawwhNE8ftt",
    "createdAt": 1510765450609,
    "logLevel": "info",
    "message": "Action created!",
    "app": "U3pxRQh2eD8RtKwaRgerfQgc",
    "timestamp": 1510765450415
  }
]

You can also use a filtered query to obtain logs of a specific level:

GET /projects/:projectId/applications/:applicationId/reactor/logs
        ?filter=logLevel=error
Authorization: $OPERATOR_API_KEY

Delete the Reactor Logs

Deletes the Reactor logs.

DELETE /projects/:projectId/applications/:applicationId/reactor/logs
Authorization: $OPERATOR_API_KEY
curl -H "Authorization: $OPERATOR_API_KEY" \
  -X DELETE 'https://api.evrythng.com/projects/U2meqbNWegsaQKRRaDUmpssr/applications/UF3Vqb7D6G8EhMwaRYgQ2pFc/reactor/logs'
const projectId = 'U2meqbNWegsaQKRRaDUmpssr';
const applicationId = 'UF3Vqb7D6G8EhMwaRYgQ2pFc';

operator.project(projectId).application(applicationId)
  .reactorLog()
  .delete()
  .then(() => console.log('Deleted!'));

To delete the Reactor logs of a specific log level you can use a filtered query like in example above.


Testing Scripts Locally

To test the Reactor script locally from your computer we provide the reactor-testing GitHub repository. This allows mocking up all types of events to test how the script executes and behaves locally on your own computer before using it in the Platform. Follow the instructions in the repository's README.md file to get started.


Example Scripts

Starter Script Example

The example script below contains example implementations the four possible callback types, and serves as a good starting point for any Reactor script.

// When an action is created
function onActionCreated(event) {
  logger.info(`Action created:\n${JSON.stringify(event)}`);
  done();
}

// When a Thng's properties have changed
function onThngPropertiesChanged(event) {
  logger.info(`Thng properties changed:\n${JSON.stringify(event)}`);
  done();
}

// When a product's properties have changed
function onProductPropertiesChanged(event) {
  logger.info(`Product properties changed:\n${JSON.stringify(event)}`);
  done();
}

// When a Reactor Schedule runs
function onScheduledEvent(event) {
  logger.info(`Scheduled event:\n${JSON.stringify(event)}`);
  done();
}

Async/await Script Example

If your script is running with Node 8 and above (see the top of the page for details), you can take advantage of async/await language features to delegate safely calling done() and catching errors using the reactor-runasync helper function package. An example is shown below:

const runAsync = require('reactor-runasync');

// @filter(onActionCreated) action.type=scans
const onActionCreated = event => runAsync(async () => {
  // Read a Thng using await
  const thng = await app.thng(event.action.thng).read();
  logger.info(thng.id);
  
  // Errors handled automatically from async functions
  if (!thng.tags.includes('shipped')) {
    throw new Error('Not a shipped item!');
  }
});

Twilio Example

Send a message using Twilio as a result of action creation.

const TwilioClient = require('twilio');

const sendSMS = (action) => {
  // Application customFields must contain Twilio credentials
  const { accountSid, authToken } = app.customFields;
  if (!accountSid || !authToken) {
    throw new Error('accountSid or authToken customField not defined');
  }
  
  const client = new TwilioClient(accountSid, authToken);
  
  // Action customFields must contain message { to, from, body }
  const { to, from, body } = action.customFields;
  logger.info(`from=${from} to=${to} body=${body}`);
  return client.messages.create({ to, from, body });
};

// @filter(onActionCreated) action.type=_sendSMS
function onActionCreated(event) {
  app.$init
    .then(() => sendSMS(event.action))
    .then(message => logger.info(`message.id=${message.sid}`))
    .catch(err => logger.error(err))
    .then(done);
}
{
  "dependencies": {
    "evrythng-extended": "^4.1.0",
    "twilio": "2.3.0"
  }
}

Global Reactor Rules

Global Reactor rules can be used when Reactor should run outside a project. This means they will be run for every action created and Thng/product property updated in the entire account. Global Reactor rules will run as an Operator, not as a Trusted Application. Therefore a global operator SDK scope object will be available to use.

Creating the Global Rules Project

The global rules project is not created in account by default. To create a global rules project, make a request to the projects API and use the account ID as the project ID parameter:

GET /projects/:accountId
Authorization: $OPERATOR_API_KEY
curl -H "Authorization:$OPERATOR_API_KEY" \
  -X GET https://api.evrythng.com/projects/:accountId

Operator Context

Global rules work exactly the same as project scoped reactor rules except that the rules will now have an Operator context available.

const runAsync = require('reactor-runasync');

// @filter(onActionCreated) action.type=scans
const onActionCreated = event => runAsync(async () => {
  // Read a Thng using await and Operator context
  const thng = await operator.thng(event.action.thng).read();
  logger.info(thng.id);
  
  // Errors handled automatically from async functions run with runAsync
  if (!thng.tags.includes('shipped')) {
    throw new Error('Not a shipped item!');
  }
});