Logic (Beta)

Logic is in beta and is not recommended for production applications. There may be breaking changes to the behavior and APIs in future versions based on user feedback.

Pricing and availability may change in the future.

Logic Functions are bits of Javascript code that get called in response to a Logic Trigger, which can be:

  • A Particle event, generated by a device, Particle cloud API, or webhook response
  • A scheduled, periodic time event ("cron")

Logic Functions run in the cloud and are designed to perform short-running calculations such as:

  • Generation of new event (which might be subscribed to by a device, or trigger an integration)
  • Storing data in the Ledger database (in the future)

In particular, Logic Functions consist only of calculations, conditional logic, etc.. Logic Functions cannot directly interact with the outside world (such by REST APIs), and do not maintain state across invocations of the Logic Function.

Logic Functions can, however, trigger an integration (webhook), and also respond to the results from a webhook.

For this beta version:

  • Logic Functions are only available in the developer sandbox (not at the organization level)
  • Maximum number of Logic Functions per sandbox: 20
  • Maximum number of triggers per Logic Function: 5
  • Run logs per function retained: 100
  • Maximum logic run execution time: 30 seconds

A companion feature, Ledger, allows data to be stored in the cloud and synchronized with devices. Only the cloud side of ledger is available in beta now, and the device side will be added in January 2024.

Common use cases

Data transformation

Events to and from devices have a limited size (typically 1024 bytes). Using Logic you can transform data that has been abbreviated or compressed, expanding it and passing it to a webhook. The communication between logic and a webhook is not limited by the 1024 byte event size limitation, making it ideal for expanding compressed data.

While a webhook hook-response cannot trigger another webhook, it can trigger a logic block. This allows much more complex processing of the data coming from a web service than is possible using mustache variables in your hook response template.

There are several use-cases for data transformation:

  • Cloud protocol translation
  • Moving business logic from firmware to the cloud
  • Alerting on abnormal conditions (using an external service)

In addition to bypassing event size limitations, logic events do not currently count as an additional data operations, however a triggering event from a device or hook-response does still count.

Console interface

The Logic feature is typically accessed from the Particle console. It is also possible to access it from the Particle Cloud API, and there will be support for creating Logic Functions in a future version of the Particle CLI.

In the developer sandbox in the console, select the Logic icon.

Logic templates

Using a template is the easiest way to get started:

  • Decode Base64: Decode an event data payload that is encoded using Base64 back into a byte array.
  • Decode Base85: Decode an event data payload that is encoded using Base85 back into a byte array.
  • Reformat JSON data: Reformat and add fields to a JSON event to make it compatible with an external service.

To create a Logic Function from scratch, select the template that matches the type of trigger you want:

  • Event-triggered function: Start with a blank Logic Function that triggers from an event.
  • Scheduled function: Start with a blank Logic Function that triggers on a schedule.

Creating a Logic Function from a template

Using a template is a three step process:

  • Define: Set name and description
  • Execute: Update the Javascript and test
  • Deploy: Set triggers and enable to run in the cloud

Define Logic Function

The name must be unique across Logic Functions in your sandbox. When organization Logic Functions are available, they must be unique in your organization.

The description is for your use to help remember how you are using the Logic Function.

Execute Logic Function

Executing a Logic Function performs operations such as publishing an event. When Ledger is available, you will be able to write to your Ledger database from your Logic Function.

This is the sample code generated in the template, it will be explained in more detail below.

import Particle from 'particle:core';

export default function reformat({ event }) {
  let data;
  try {
      data = JSON.parse(event.eventData);
  } catch (err) {
    console.error("Invalid JSON", event.eventData);
    throw err;
  }
  const reformatted = {
    id: data.i,
    data: {
      value: data.d,
      type: 'Sensor'
    }
  };
  Particle.publish("data-reformatted", reformatted, { productId: event.productId });
}

The next part of interest is the Event Test Data. The field is initially empty and says Sensor Data but if you are transforming JSON, you will typically enter sample data that your device would normally publish. For example:

{"i":12, "d":34.56}

You'll enter the event name in the next step.

Finally, click the Run Code button.

If you have an error in your Javascript code, or in the event data, you will get an error message instead.

Deploy Logic Function

For an event triggered Logic Function, this is where you enter the Trigger event name. Unlike an integration, a Logic Function event name trigger is an exact match, not a prefix match.

You must specify a product to associate the Logic Function with! Logic Functions only work with product devices, not sandbox devices. For the beta, only personal sandbox products can be used (not organization products).

If you add multiple triggers, if any of the triggers occurs, the Logic Function will be called. Each function can have up to 5 triggers. One use case of this is if you have two products, one for the bsom and one for the b5som, but you want to use the same Logic Functions for both. If you need to figure out which product triggered the event, use event.productId which contains the numeric product ID that triggered the event.

When you've successfully deployed the Logic Function:

Testing from console

Since Logic Functions only work with products:

  • From the Products icon in your developer sandbox, open the product that you associated your Logic Function with.
  • Click on the Events icon within your product.
  • Click on the Publish an event icon near the top, to the left of Search for events.
  • For Event name enter the event name you entered in the Deploy step above.
  • For Event data enter the test JSON data. For example:
{"i":12, "d":34.56}

Click the Publish button and you should see both the sensor-test event and the data-reformatted event!

Testing from the Particle CLI

You can publish events using the Particle CLI for testing, but remember to pass the product number, since Logic Functions only work with product devices.

% particle publish --product 1234 'sensor-test' '{"i":12, "d":34.56}'
Published private event: sensor-test to product: 1234

Testing from a device

Of course the real application for this is having a device publish the data.

For example:

char buf[particle::protocol::MAX_EVENT_DATA_LENGTH + 1];
memset(buf, 0, sizeof(buf));
JSONBufferWriter writer(buf, sizeof(buf) - 1);

writer.beginObject();
    writer.name("d").value(sensorData);
    writer.name("i").value(sensorId);
writer.endObject();

Particle.publish("sensor-test", buf);

Remember that Logic Functions only work with product devices. The devices can be product developer devices (or not) and can be claimed or unclaimed.

Scheduled events

When using the scheduled event template, the following code is provided to start:

import Particle from 'particle:core';

export default function job({ functionInfo, trigger, scheduled }) {
  // Add your code here
}

Cron syntax

In the Deploy step of creating a scheduled event, you enter the schedule in cron format.

When scheduling events, cron syntax is used. A chron schedule consists of 4 or 5 space-separated fields for:

Field Range
minute 0 - 59
hour 0 - 23
day of month 1 - 31
month 1 - 12
day of week 0 = Sunday, 1 = Monday, ..., 6 = Saturday (optional)

Additionally, three special characters are allowed:

Character Meaning
* All (wildcard)
, List of options (any of the list)
- Range (inclusive)

This most easily illustrated by examples:

Example Description
* * * * Once per minute
0 * * * * Every hour at minute = 0
0,30 * * * * Every half hour (at minute = 0 and minute = 30)
0 4 * * * Every day at 4:00 UTC (minute = 0, hour = 4)
0 4,8,12,16 * * * Every day at 4:00, 8:00, 12:00, at 16:00 UTC
0 4 * * 1-5 Every weekday at 4:00 UTC (minute = 0, hour = 4, day of week = 1 - 5 or Monday - Friday

You cannot schedule an Logic Function to execute more than one time per minute. This only applies to cron scheduled functions; event triggered functions will be called as often as necessary based on the events that are received by the cloud.

Logic Function coding

The Logic Function code is Javascript. You can use built-in language features such as:

  • calculations
  • if blocks
  • for and while loops
  • functions
  • JSON features like JSON.parse() and JSON.stringify()
  • Standard built-in objects like Math and String
  • console.log (useful for debugging, as the run logs include this log output)
  • Particle Logic Core API (below)
  • Particle Encoding API (below)
  • Vendor packages (below)

This list is not complete! If it's a standard Javascript function it may work and you should just try it. It only takes a few seconds to edit and test from the console.

You cannot use features such as:

  • functions implemented in external packages (no package.json)
  • network access (access to external APIs using things like fetch or axios)
  • long running operations (limit is 20 seconds)
  • asynchronous features like async/await, promises, etc.
  • browser features like the DOM
  • node.js features like Buffer, the file API, etc.

Deep dive into the reformat JSON template

Every template has one or more import statement to make certain features available to your Logic Function. The Particle class from particle:core contains the Particle.publish call that you will frequently use.

import Particle from 'particle:core';

You must define a function to be called to implement the Logic Function. The JSON example uses this declaration, but you may have a comma-separated list of other parameters in some cases.

The function can have any name as long as its declared with export default function.

export default function reformat({ event }) {

This bit of code transforms event.eventData, which is a string, into a JSON object. It includes some error checking, but even if you didn't handle the exception using a try/catch block, the exception would still be caught and displayed in the run logs for your Logic Function.

  let data;
  try {
      data = JSON.parse(event.eventData);
  } catch (err) {
    console.error("Invalid JSON", event.eventData);
    throw err;
  }

Here's where a new object is created based on the original event JSON.

  const reformatted = {
    id: data.i,
    data: {
      value: data.d,
      type: 'Sensor'
    }
  };

In addition to simply changing the "shape" of the object (key names, embedded objects, etc.), you can also perform calculations. For example, this makes the reformatted data.d value the square of the original value using the Javascript Math.pow() (power) method.

  const reformatted = {
    id: data.i,
    data: {
      value: Math.pow(data.d, 2),
      type: 'Sensor'
    }
  };

And, finally, the reformatted object is published. The options (last parameter) specify that the event goes out to the same product that the event came from.

  Particle.publish("data-reformatted", reformatted, { productId: event.productId });
}

Particle Logic Core API

To use the Particle Logic Core API this line is added automatically when using a template:

import Particle from 'particle:core';

Particle.publish

Publishes an event to the Particle cloud. This event can be subscribed to on-device or be used to trigger a webhook.

// PROTOTYPE
export function publish(name: string, data: any | undefined, options: { productId: number, asDeviceId: string }): null;
  • name The name of the event
  • data The particle publish data. This can be a string or an object. If an object, it will be serialized to JSON using JSON.stringify.
  • options
    • productId The event will be sent on this product event stream
    • asDeviceId (optional) If provided, the event will be published as if it came from this device.

When triggering from an event, you will typically publish the event to the same product:

Particle.publish("data-reformatted", reformatted, { productId: event.productId });

When triggering on a schedule, you need to specify which product to send the event to.

A Logic Function can publish more than one event if desired, and is not limited to the once per second average that devices are. However if you are publishing events that devices are subscribed to, you should limit the size and number of events published to devices, as the device will drop events that it is unable to process.

When publishing to a webhook, the data can exceed the normal 1024 bytes event size limit. When sending to a device, however, you must still limit the size of the event data. Events published by Logic do not currently count against your data operations usage.

Using Ledger from Logic

Particle.ledger

The Ledger is available to Logic functions in the Particle core library:

import Particle from 'particle:core';

export default function myLogicFunction() {
  const productLedger = Particle.ledger("product-settings", { productId: 1234 });
  const deviceLedger = Particle.ledger("device-settings", { deviceId: '001122...' });
  const ownerLedger = Particle.ledger("owner-settings");
}

If you are handling an event, you don't need to specify the product ID or device ID, as this is known and available in the event (event.productId and event.deviceId).

import Particle from 'particle:core';

export default function myLogicFunction({ event }) {
  const productLedger = Particle.ledger("product-settings"); 
  const deviceLedger = Particle.ledger("device-settings");
}

Getting a ledger value

Once you have a Ledger object, call deviceLedger.get() on it to retrieve the current data.

Ledger get() returns an object with updatedAt for when the Ledger was last updated and data containing the Ledger data.

If the Ledger name doesn’t exist, Ledger get() will throw an error.

If there is no data for this device, product, or owner, Ledger get() will return an object without updatedAt and an empty object in data.

import Particle from 'particle:core';

export default function myLogicFunction({ event }) {
  const productLedger = Particle.ledger("product-settings"); 
  const deviceLedger = Particle.ledger("device-settings");

  const productDefaults = productLedger.get();
  const deviceOverride = deviceLedger.get();

  // Use data from device Ledger if available, otherwise use product value.
  // In this example, the data field is called threshold
  const threshold = deviceOverride.data.threshold || productDefaults.data.threshold;
  if (!threshold) {
    throw new Error("No threshold set for this device or product");
  }
  // Do something with threshold
}

Another example of getting ledger data:

const thresholds = Particle.ledger("thresholds", { productId: 1234 });
const { data } = thresholds.get();

// check if value is available for this product
if (data) {
  // do something with data.maxiumValue
}

Setting a ledger value

There are two ways to set a Ledger: either replace the entire data of the Ledger, or merge the existing data with additional data. Merging adds new fields that were not previously in the data, and replaces existing fields. The default mode is Particle.REPLACE.

// Previous Ledger data
{
  "a": 123,
  "b": 456
}

// Data to merge
{
  "b": 789,
  "c": "ok"
}

// Merged value
{
  "a": 123,
  "b": 789,
  "c": "ok"
}
import Particle from 'particle:core';

export default function myLogicFunction() {
  const productLedger = Particle.ledger("my-product-ledger");
  const deviceLedger = Particle.ledger("my-device-ledger");
  const ownerLedger = Particle.ledger("my-owner-ledger");

  const newData = { b: 789 };

  productLedger.set(newData); // implicit Particle.REPLACE
  deviceLedger.set(newData, Particle.REPLACE);
  ownerLedger.set(newData, Particle.MERGE);
}

Ledger set() doesn’t return any value. A Logic Function can set a Cloud only Ledger and a Cloud to Device Ledger. Trying to set a Device to Cloud Ledger will throw an error.

Another example of setting data:

const thresholds = Particle.ledger("thresholds", { productId: 1234 });

thresholds.set({ maximumValue: 9000, validation: { voltage: { largerThan: 3.0 }}});

Deleting a ledger

Deleting a Ledger from a Logic Function will only clear the data for the device or product specified. The data for other devices/products will remain. Deleting an owner Ledger removes the data shared across your account.

import Particle from 'particle:core';

export default function myLogicFunction() {
  const productLedger = Particle.ledger("my-product-ledger");
  const deviceLedger = Particle.ledger("my-device-ledger");
  const ownerLedger = Particle.ledger("my-owner-ledger");

  productLedger.delete();
  deviceLedger.delete();
  ownerLedger.delete();
}

Particle encoding API

Particle publish data can consist only of UTF-8 characters and cannot contain binary data. Likewise, JSON does not allow inclusion of binary data directly.

To get around this, an encoding mechanism is typically used, such as:

  • Hexadecimal (hex): Each byte creates two characters 0-9 and a-f, a 16-character alphabet, doubling the size of the data
  • Base64: Every 3 bytes of binary data produces 4 safe ASCII characters out of a set of 64 (RFC 3548)
  • Base85 (Ascii85): Uses an 85-character alphabet, which is even denser (RFC 1924)

While Base85 provides a more dense encoding than Base64, it is not recommended if you are publishing data to be sent to an external server by a webhook. The Base85 alphabet includes the left curly bracket, and the {{ sequence is recognized as a mustache template delimiter during webhook processing, causing the data to be corrupted. The backslash can also cause unexpected data transformation. One use case of Base85, however, is to use that to send the data from the device to the cloud, and then use a Logic Function to expand this to a different format, like Base64 or hex.

If you are interpreting binary data from your Logic Function, you will typically convert the encoded data (Base64 or Base85) to a byte array, so you can access the individual values of each byte (0 - 255) as values in an array. Logic Functions do not support node.js style Buffer objects for binary data.

Using the encoding APIs

To use the Particle encoding API, lines like this are added automatically when using a template:

import Particle from 'particle:core';
import { base85Decode } from 'particle:encoding';

You can substitute other functions like bytesToString for base85Decode in the example above.

You can include a comma-separated list of multiple imports if desired:

import Particle from 'particle:core';
import { bytesToString, stringToBytes } from 'particle:encoding';

base64Encode

Encodes a string or byte array to base64 (RFC 3548).

// PROTOTYPE
export function base64Encode(input: string | number[]): string;

In your device firmware, use the Base64RK library to encode and decode Base64.

base64Decode

Decodes a base64 (RFC 3548) string to a byte array

// PROTOTYPE
export function base64Decode(input: string): number[];

In your device firmware, use the Base64RK library to encode and decode Base64.

base85Encode

Encodes a string or byte array to base85 (RFC1924)

// PROTOTYPE
export function base85Encode(input: string | number[]): string;

In your device firmware, use the Base85 library to encode or decode Base85.

base85Decode

Decodes a base85 (RFC1924) string to a byte array.

// PROTOTYPE
export function base85Decode(input: string): number[];

In your device firmware, use the Base85 library to encode or decode Base85.

bytesToString

Convert a byte array into a UTF-8 string. This should not be used for arbitrary binary data; use Base64 or Base85 for that.

// PROTOTYPE
export function bytesToString(input: number[]): string;

stringToBytes

Converts a string into an array of bytes.

// PROTOTYPE
export function stringToBytes(input: string): number[];

Vendor packages

Additionally, some utility packages are available by importing the following:

Import Package Version Description
vendor:lodash lodash-es 4.17.21 General utility functions
vendor:date-fns date-fns 2.30.0 Date handling
vendor:cbor-x cbor-x 1.5.4 CBOR (Concise Binary Object Representation)
vendor:unishox2 unishox2.siara.cc 1.1.3 Compression of short UTF8 strings

It is not possible to import arbitrary npm or other packages in your Logic Function.

Logic Function context

When your Logic Function is called, it will always include required parameters:

  • functionInfo: information about the function that was called
  • trigger: information about the time the event was triggered

Additionally, one of the following parameters will be populated, based on the type of trigger:

  • event: information about the event that triggered the Logic Function
  • scheduled: information about the schedule that triggered the Logic Function
  • ledgerChange: information about a ledger database change the triggered the Logic Function (future)
// PROTOTYPE
export interface FunctionContext {
    functionInfo: FunctionInfo;
    trigger: TriggerInfo;
    event?: EventInfo;
    scheduled?: ScheduledInfo;
    ledgerChange?: LedgerChangeInfo;
}

Accessing the context

To access just the event, specify it as is done in the event trigger template.

export default function process({ event })

You can also add other parameters in a comma-separated list if you're interested in those values.

export default function process({ functionInfo, trigger, scheduled })

functionInfo - Logic Function context

This object specifies the function that was called:

  • ownerId`: The user that owns this function. In the future, this could be an organization. It contains the user GUID, not the email address.
  • logicFunctionId`: The unique identifier for the function.
// PROTOTYPE
export interface FunctionContext {
    functionInfo: FunctionInfo;
    trigger: TriggerInfo;
    event?: EventInfo;
    scheduled?: ScheduledInfo;
    ledgerChange?: LedgerChangeInfo;
}

trigger - Logic Function context

Information about how the event was triggered (event or scheduled).

  • triggeredAt: The time the trigger occurred in ISO 8601 format (2023-11-07T14:01:57.923225531Z)
  • triggerEventId: The event ID that triggered the event (for event triggers)
// PROTOTYPE
export interface TriggerInfo {
    triggerEventId?: string;
    triggeredAt: string;
}

event - Logic Function context

  • eventName: The event name.
  • eventData: The event data as a string. If you are expecting JSON, you will need to use JSON.parse() to convert event.eventData to an object. This may be omitted if there is no event data.
  • publishedAt: Time in ISO 8601 format when the event was received by the cloud.
  • deviceId: The device ID (24 character hex) that triggered the event.
  • productId: The numeric product ID this trigger is associated with.
// PROTOTYPE
export interface EventInfo {
    publishedAt: string;
    eventName: string;
    eventData?: string;
    deviceId: string;
    productId?: number;
    userId?: string;
}

scheduled - Logic Function context

  • scheduledAt: When the event was scheduled at in ISO 8601 format (2023-11-07T14:01:57.923225531Z).
  • startAt: When the scheduler started scheduling this Logic Function.
  • endAt: When the scheduler will stop scheduling this Logic Function.
// PROTOTYPE
export interface ScheduledInfo {
    scheduledAt: string;
    startAt: string;
    endAt?: string;
}