Create a Basic Reactor Script

In this section, we walk through creating a Reactor script from scratch. This script achieves a specific purpose—tracking the locations and quantities of scans on Thngs. This is a good example of manipulating related Platform resources in reaction to another event, one of the most common use cases of a Reactor script.

This script takes the form of the single-file pattern because it's short and self-contained. All the information required to carry out this task is included in each action, if a Thng is the target.

The available event types used are:

  • onActionCreated() - On each scan, counts the total scans and accumulates cities for the target Thng. Also adds a tag to show it has been included in this scenario.
  • onScheduledEvent() - Finds all tagged Thngs and resets the counter properties at midnight each day.

Beginning the Script

The first step in creating a Reactor script is to create a project and then an application inside that project. You can do this through the Dashboard or the API directly. In this walkthrough, you will edit and upload the script through the application resource page on the Dashboard.

With the application in place, click the ‘Edit’ icon in the top-right corner of the ‘Reactor’ section of the application page. Enter the following to begin the script:

const onActionCreated = async (event) => {
  logger.info('Action created!');
  done();
};

const onScheduledEvent = async (event) => {
  logger.info('Scheduled event!');
  done();
};

The Platform calls these event handler functions in each applicable case. When a Thng’s QR code is scanned, onActionCreated() is called. After a Reactor schedule is set up, onScheduledEvent() is called at midnight.


Tracking Scans

Starting with onActionCreated(), read the full resource for the Thng that was scanned using the ID stored in the action to read its tags. With the evrythng.js SDK included in Reactor, this is straightforward:

const onActionCreated = async (event) => {
  try {
    // Read the Thng
    const thng = await app.thng(event.action.thng).read();
    logger.info(JSON.stringify(thng));
  } catch (e) {
    logger.error(e.message || e.errors[0]);
  }
  
  done();
};

At this point, the script reads and prints the Thng. The next step is to read the cities it's been scanned in (if any) and add the city for this action if it isn't already included:

const onActionCreated = async (event) => {
  try {
    // Read the Thng
    const thng = await app.thng(event.action.thng).read();

    // Add the city if it isn’t already there
    const scanCities = thng.properties.scan_cities || [];
    const { city } = event.action.context;
    if (!scanCities.includes(city)) {
      scanCities.push(city);
    }
  } catch (e) {
    logger.error(e.message || e.errors[0]);
  }
  
  done();
};

The final step is to update the Thng resource with this new information and add an item to its tags. Using evrythng.jspromises and Promise.all(), this is easy to do. The final form of onActionCreated() looks like this:

const onActionCreated = async (event) => {
  try {
    // Read the Thng
    const thng = await app.thng(event.action.thng).read();
    
    // Add the city if it isn’t already there
    const scanCities = thng.properties.scan_cities || [];
    const { city } = event.action.context;
    if (!scanCities.includes(city)) {
      scanCities.push(city);
    }

    // Update the properties and add a tag
    await Promise.all([
      thng.property().update([
        { key: 'scan_count',  value: (thng.properties.scan_count || 0) + 1 }, 
        { key: 'scan_cities', value: scanCities },
      ]),
      thng.update({ tags: ['Walkthrough'] })
    ]);
  } catch (e) {
    logger.error(e.message || e.errors[0]);
  }
  
  done();
};

After both the properties update promise and the tags update promise are resolved, the final .then() is called, and the script ends with a call to done() and a catch to catch any errors.


Resetting Scans

In this scenario we clear the total scans for all Thngs with the ‘Walkthrough’ tags at the start of each day. In a more complete integration, a total count could be transmitted to a third party or stored on the Thng for historical reasons. That use case is beyond the scope of this walkthrough, but it's a good simple example of a useful Reactor script.

The Reactor scheduler API makes this easy by using a simple cron expression to call the onScheduledEvent() Reactor script handler at 00:00 on every day of the month.

Make the cURL request shown below to enable this schedule before setting up the script to handle the callbacks. Substitute your account Operator API Key and the appropriate project and application IDs.

curl -H "Content-Type: application/json" \
  -H "Authorization: $OPERATOR_API_KEY" \
  -X POST 'https://api.evrythng.com/projects/:projectId/applications/:applicationId/reactor/schedules' \
  -d '{ 
    "function": "onScheduledEvent", 
    "event": {}, 
    "cron": "0 0 0 * * ?", 
    "description": "Run every day at midnight", 
    "enabled": true 
  }'

To enable a callback on this schedule, implement onScheduledEvent() in the Reactor script, alongside the existing code. From the basic handler you added earlier, create an evrythng.js iterator to iterate through all Thngs in the application scope, and filter by only those with the ‘Walkthrough’ tag:

const onScheduledEvent = await (event) => {
  try {
    // Get an iterator for all Thngs with 'Walkthrough' tag
    const params = { filter: 'tags=Walkthrough' };
    const iterator = app.thng().iterator({ params });
  } catch (e) {
    logger.error(e.message || e.errors[0]);
  }
  
  done();
};

Using the EVT.Utils.forEachAsync function with the iterator, it's easy to reset all concerned Thngs to remove their scan count and the cities in which they were scanned. To do this, get a filtered list of Thngs and map them to promises that can be executed together to perform the updates.

After this is done, the final form of the onScheduledEvent() handler is as follows:

const onScheduledEvent = async (event) => {
  try {
    // Get an iterator for all Thngs with 'Walkthrough' tag
    const params = { filter: 'tags=Walkthrough' };
    const iterator = app.thng().iterator({ params });

    // Read the Thngs
    const thngs = [];
    await EVT.Utils.forEachAsync(iterator, (page) => {
      page.forEach(thng => thngs.push(thng));
    })

    // Map updates to promises and execute
    const promises = thngs.map(thng => thng.property().update([
      { key: 'scan_count',  value: 0 }, 
      { key: 'scan_cities', value: [] }
    ]));
    await Promise.all(promises);
  } catch (e) {
    logger.error(e.message || e.errors[0]);
  }
  
  done();
};

Conclusion

When a Thng in the project is scanned, it accumulates a count of scans and the distinct cities in which it's scanned as properties. At the beginning of each day, both of these properties are reset.

As an additional exercise you could extend the script to persist the daily totals in the Thng structure (beyond property histories). In the next section, a more complex Reactor script is built, including multiple files and a strong separation pattern based on action types.