Enterprise Feature
This feature is available only to customers with an enterprise Platform subscription. Contact us to discuss enabling it on your account.
Reactor lets you execute 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, which is created the first time a script is uploaded, including any required dependencies.
Reactor scripts are written in Node.js and can use NPM packages through a conventional package.json
file. They're automatically provided with the latest version of evrythng-extended.js through a global EVT
object and app
(EVT.App
) scope for requests. You must not remove this basic dependency, or the script could fail to execute.
After they're configured configured, Reactor scripts 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 you to execute complex logic 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 the async/await script example below for more details.
Reactor Walkthrough
See the Reactor Walkthrough for an in-depth walkthrough of the Reactor, including example use cases and patterns for implementation.
SDK Version
As of 27 May 2021, new Reactor scripts support the latest evrythng.js SDK version (evrythng@^6.0.3). To use it, enable Global Reactor Rules, and then update the 'dependencies' to use
"evrythng": "^6.0.0"
instead ofevrythng-extended
.
Legacy SDK Version
Legacy reactor scripts can use up to evrythng-extended.js v4.7.2. For new Reactor scripts, we recommend using the new evrythng.js SDK.
Node Versions
The version of Node that a Reactor script uses depends on when the script was created, as new LTS versions are adopted. Use
process.version
to see which version is being used.Current version: Node v10.x
Until 9 January 2020: Node v8.12
Until 1 May 2019: Node v6.10.2
Until 13 April 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
Event Types
Debugging
Filters
Limits
Testing Scripts Locally
Reactor Script API
Reactor Scheduler API
Reactor Logs API
Example Scripts
Global Reactor Rules
Scripts
We provide two ways to upload Reactor scripts to EVRYTHNG Platform:
- The simple API allows a script file and an optional NPM dependencies descriptor file (
package.json
) to be uploaded in an update request body. - The bundle API lets you upload a full Node.js project as a
.zip
file.
We'll only consider the bundle API in detail here because the simple API comprises merely sending the script content or editing through the Dashboard on the Application Details page. You can update the script through the API or through 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 specify any other dependencies of the Reactor script.
Notes
By default we include
evrythng-extended
in thepackage.json
file when an application is first created. You must not remove this or the Reactor could stop working. We provide it to allow you to see which version you're using, and update if necessary.You must not include any
node_modules
in the bundle. Thepackage.json
file can specify these dependencies.
devDependencies
inpackage.json
are ignored by the Reactor when a bundle is uploaded and installed.
If the file isn't provided or no dependency to evrythng-extended.js
is specified, the latest version of evrythng-extended.js
is automatically included. We strongly advise you to state which specific version of the library you want to use. This avoids problems when new versions are released.
Event Types
This section lists the types of events that can trigger a Reactor script.
Important
All event handlers must call
done()
immediately before exiting. This tells the Platform that everything is complete and that no other callbacks are pending. This also commits the log lines that have been recorded usinglogger
and makes them available through the Dashboard and the API.When an action or resource update occurs that triggers a Reactor script, all application Reactor scripts within the resource's project scope are 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. Iffunction
in the schedule uploaded is a custom function name, that name is invoked instead, but only if it has been exported from the script.
onActionCreated
This event occurs every time an action is created and is visible to the application. The event
supplied to the handler is:
.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 event occurs every time the properties of a product have been changed. The event
supplied to the handler is shown below.
If multiple updates to a property are provided in one update request, the behavior changes slightly. See 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 event 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 doesn't execute.
The event
supplied to the handler is shown below.
If multiple updates to a property are provided in one update request, the behavior changes slightly. See 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 is 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 occurred.
{
"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 occurred.",
"minimum": 1508761430000
}
}
}
Multiple Property Updates
If an update request includes multiple changes to a particular Thng or product property when the onThngPropertiesChanged
or onProductPropertiesChanged
callback is invoked, an allChanges
property is included. This is an array containing all the updates for that property change request.
The oldValue
, newValue
and timestamp
properties at the changed key level reflect the 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 specify the version you want to use). It's pre-configured with the application's Trusted Application API Key.
The table below describes the global variables in 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 isn't called, no logs are shown. |
logger | The logger available to use to output logs. console.log() isn't visible in output logs. Available methods are .info() , .error() , and .warn() . Logs don't appear in the Dashboard unless done() is called in all cases. |
evrythng-extended.js
also forwards the X-Evrythng-Reactor
header, which protects you against inadvertently creating a recursive loop (see Limits: Recursion).
If you re-configure evrythng-extended.js
or use an alternate way to communicate with the API, be sure you forward this header to prevent recursive loops.
Debugging
You can create log messages from the Reactor script. A logger
instance is globally defined 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 are available for up to seven 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 you 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 isn't a good practice because the Reactor script execution cost is non-trivial.
The recommended solution is to use the Reactor filters annotation syntax. The example below runs 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're 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 described on the Filters page. Filters can be defined anywhere in the script, but we recommend placing each directly above the corresponding callback function definition. If using the bundle
Reactor type, filters must 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 logical AND operator can be used similarly:
// @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);
}
If for any reason the filter definitions aren't extracted from your Reactor script, no error is thrown. Reactor script functions are 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 you can use in the Reactor filter body, although not all of them are available in any particular 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 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 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 aremon
,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's distance from the scan in meters.
Limits
Execution Timeout
When triggered, a Reactor script must complete within a certain amount of time. If this isn't the case, the execution is aborted. See below for time constraints.
Recursion
A Reactor script could trigger itself by performing an operation against the REST API. This causes the script to trigger again (directly or indirectly). We provide a mechanism 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 triggers itself again, and so on.
We limit the depth of such recursive calls and the total time allowed for the execution since the original event.
Note
This mechanism won't protect a user who intentionally defeats this safeguard.
Build Timeout
The time required to download and package all the dependencies must not exceed the Build Timeout (see below). The build time depends on a lot of external factors. 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 through the REST API instead of through the application details page in the Dashboard. You can view the status of an upload in progress and 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 the resource was created.
.updatedAt (integer, read-only)
Timestamp 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 the resource was created.",
"readOnly": true,
"minimum": 0
},
"updatedAt": {
"type": "integer",
"description": "Timestamp 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
To update a reactor script, send a PUT request to the /projects/applications/reactor/script endpoint with the projectId
and applicationId
in the path and a new ReactorScriptDocument in the body.
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 file=@./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
Besides the three standard event entry points into a Reactor script, you can schedule any developer-defined function to be called at a later time. When the function is called, it's passed a predefined event object assigned at the time of scheduling.
The scheduled event can be a one-time execution (delayed, scheduled, or immediate), or it can be a repetitive event, defined by a cron
expression. The format of this expression is:
"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 sample 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
orcron
is allowed, or none.We can only guarantee minute-precision invocation for
executeAt
, but smaller granularities usually execute as required.If
executeAt
isn't in the future, the event is executed immediately.We perform some validation of cron expressions and allow the lowest granularity of 2 minutes. Cron time is in UTC.
By default, we assume the
function
name isonScheduledEvent
. We export anonScheduledEvent
function if it's defined in the script. If you're using another function, it must be explicitly exported.If you override
module.exports
and omit your schedule function handler (including the defaultonScheduledEvent
), the handler isn't called.
Create a Reactor Schedule
To create a Reactor schedule, send a POST request to the projects/applications/reactor/schedules endpoint with the projectId
and applicationId
in the path and a ReactorScheduleDocument
in the body 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
To get all the current schedules of the specified application, send a GET request to the projects/applications/reactor/schedules endpoint with the projectId
and applicationId
in the path.
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
To get a single Reactor schedule, send a GET request to the projects/applications/reactor/schedules endpoint with the projectId
, applicationId
, and scheduleId
in the path.
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
To update the fields of an existing Reactor schedule document, send a PUT request to the projects/applications/reactor/schedules endpoint with the projectId
, applicationId
, and scheduleId
in the path and the ReactorScheduleDocument in the body.
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
To delete a Reactor schedule, send a DELETE request to the projects/applications/reactor/schedules endpoint with the projectId
, applicationId
, and scheduleId
in the path.
Note
A one-time scheduled event (using
executeAt
instead ofcron
) is 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 created by using the logger
object in the script itself can be read through the EVRYTHNG API. The Reactor logs are created automatically after execution of the Reactor script, as long as done()
was called to mark the end of script execution. If done
isn't called, logs aren't created.
Note
Reactor log entries are available for up to seven days after they're 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
To get the Reactor logs, send a GET request to the projects/applications/reactor/logs endpoint with the projectId
and applicationId
in the path.
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 use a filtered query to get logs of a specific level:
GET /projects/:projectId/applications/:applicationId/reactor/logs
?filter=logLevel=error
Authorization: $OPERATOR_API_KEY
Delete the Reactor Logs
To delete the Reactor logs, send a DELETE request to the projects/applications/reactor/logs endpoint with the projectId
and applicationId
in the path.
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 Read the Reactor Logs.
Testing Scripts Locally
To test the Reactor script on your computer, we provide the reactor-testing
GitHub repository. This repository lets you mock up all types of events to test how the script executes and behaves locally before using it on the Platform. To get started, follow the instructions in the repository's README.md
file.
Example Scripts
Starter Script Example
The example script below contains sample 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 or newer (see the note about Node versions at the top of this page), you can take advantage of async
/await
language features to delegate safely calling done()
and catching errors using the reactor-runasync
helper function package. For example:
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 is to run outside a project. This means they are run for every action created and Thng/product property updated in the entire account. Global Reactor rules run as an Operator, not as a Trusted Application. Therefore, a global operator
SDK scope object is available to use.
Creating the Global Rules Project
By default, the global rules project is not created in the account. To create a global rules project, send a GET 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 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!');
}
});