How to create user-defined functions in Azure Digital Twins

Important

A new version of the Azure Digital Twins service has been released. In light of the new service's expanded capabilities, the original Azure Digital Twins service (described in this documentation set) has been retired.

To view the documentation for the new service, visit the active Azure Digital Twins Documentation.

User-defined functions enable users to configure custom logic to be executed from incoming telemetry messages and spatial graph metadata. Users can also send events to predefined endpoints.

This guide walks through an example demonstrating how to detect and alert on any reading that exceeds a certain temperature from received temperature events.

In the examples below, YOUR_MANAGEMENT_API_URL refers to the URI of the Digital Twins APIs:

https://YOUR_INSTANCE_NAME.YOUR_LOCATION.azuresmartspaces.net/management/api/v1.0
Name Replace with
YOUR_INSTANCE_NAME The name of your Azure Digital Twins instance
YOUR_LOCATION The region your instance is hosted on

Client library reference

Functions available as helper methods in the user-defined functions runtime are listed in the client library reference document.

Create a matcher

Matchers are graph objects that determine what user-defined functions run for a given telemetry message.

  • Valid matcher condition comparisons:

    • Equals
    • NotEquals
    • Contains
  • Valid matcher condition targets:

    • Sensor
    • SensorDevice
    • SensorSpace

The following example matcher evaluates to true on any sensor telemetry event with "Temperature" as its data type value. You can create multiple matchers on a user-defined function by making an authenticated HTTP POST request to:

YOUR_MANAGEMENT_API_URL/matchers

With JSON body:

{
  "id": "3626464-f39b-46c0-d9b0c-436aysj55",
  "name": "Temperature Matcher",
  "spaceId": "YOUR_SPACE_IDENTIFIER",
  "conditions": [
    {
      "id": "ag7gq35cfu3-e15a-4e9c-6437-sj6w68sy44s",
      "target": "Sensor",
      "path": "$.dataType",
      "value": "\"Temperature\"",
      "comparison": "Equals"
    }
  ]
}
Value Replace with
YOUR_SPACE_IDENTIFIER Which server region your instance is hosted on

Create a user-defined function

Creating a user-defined function involves making a multipart HTTP request to the Azure Digital Twins Management APIs.

Note

Multipart requests typically require three pieces:

  • A Content-Type header:
    • application/json; charset=utf-8
    • multipart/form-data; boundary="USER_DEFINED_BOUNDARY"
  • A Content-Disposition:
    • form-data; name="metadata"
  • The file content to upload

Content-Type and Content-Disposition will vary depending on use scenario.

Multipart requests can be made programmatically (through C#), through a REST client, or tool such as Postman. REST client tools may have varying levels of support for complex multipart requests. Configuration settings may also vary slightly from tool to tool. Verify which tool is best suited for your needs.

Important

Multipart requests made to the Azure Digital Twins Management APIs typically have two parts:

  • Blob metadata (such as an associated MIME type) that's declared by Content-Type and/or Content-Disposition
  • Blob contents which include the unstructured contents of a file to be uploaded

Neither of the two parts is required for PATCH requests. Both are required for POST or create operations.

The Occupancy Quickstart source code contains complete C# examples demonstrating how to make multipart requests against the Azure Digital Twins Management APIs.

After the matchers are created, upload the function snippet with the following authenticated multipart HTTP POST request to:

YOUR_MANAGEMENT_API_URL/userdefinedfunctions

Use the following body:

--USER_DEFINED_BOUNDARY
Content-Type: application/json; charset=utf-8
Content-Disposition: form-data; name="metadata"

{
  "spaceId": "YOUR_SPACE_IDENTIFIER",
  "name": "User Defined Function",
  "description": "The contents of this udf will be executed when matched against incoming telemetry.",
  "matchers": ["YOUR_MATCHER_IDENTIFIER"]
}
--USER_DEFINED_BOUNDARY
Content-Disposition: form-data; name="contents"; filename="userDefinedFunction.js"
Content-Type: text/javascript

function process(telemetry, executionContext) {
  // Code goes here.
}

--USER_DEFINED_BOUNDARY--
Value Replace with
USER_DEFINED_BOUNDARY A multipart content boundary name
YOUR_SPACE_IDENTIFIER The space identifier
YOUR_MATCHER_IDENTIFIER The ID of the matcher you want to use
  1. Verify that the headers include: Content-Type: multipart/form-data; boundary="USER_DEFINED_BOUNDARY".

  2. Verify that the body is multipart:

    • The first part contains the required user-defined function metadata.
    • The second part contains the JavaScript compute logic.
  3. In the USER_DEFINED_BOUNDARY section, replace the spaceId (YOUR_SPACE_IDENTIFIER) and matchers (YOUR_MATCHER_IDENTIFIER) values.

  4. Verify that the JavaScript user-defined function is supplied as Content-Type: text/javascript.

Example functions

Set the sensor telemetry reading directly for the sensor with data type Temperature, which is sensor.DataType:

function process(telemetry, executionContext) {

  // Get sensor metadata
  var sensor = getSensorMetadata(telemetry.SensorId);

  // Retrieve the sensor value
  var parseReading = JSON.parse(telemetry.Message);

  // Set the sensor reading as the current value for the sensor.
  setSensorValue(telemetry.SensorId, sensor.DataType, parseReading.SensorValue);
}

The telemetry parameter exposes the SensorId and Message attributes, corresponding to a message sent by a sensor. The executionContext parameter exposes the following attributes:

var executionContext = new UdfExecutionContext
{
    EnqueuedTime = request.HubEnqueuedTime,
    ProcessorReceivedTime = request.ProcessorReceivedTime,
    UserDefinedFunctionId = request.UserDefinedFunctionId,
    CorrelationId = correlationId.ToString(),
};

In the next example, we log a message if the sensor telemetry reading surpasses a predefined threshold. If your diagnostic settings are enabled on the Azure Digital Twins instance, logs from user-defined functions are also forwarded:

function process(telemetry, executionContext) {

  // Retrieve the sensor value
  var parseReading = JSON.parse(telemetry.Message);

  // Example sensor telemetry reading range is greater than 0.5
  if(parseFloat(parseReading.SensorValue) > 0.5) {
    log(`Alert: Sensor with ID: ${telemetry.SensorId} detected an anomaly!`);
  }
}

The following code triggers a notification if the temperature level rises above the predefined constant:

function process(telemetry, executionContext) {

  // Retrieve the sensor value
  var parseReading = JSON.parse(telemetry.Message);

  // Define threshold
  var threshold = 70;

  // Trigger notification 
  if(parseInt(parseReading) > threshold) {
    var alert = {
      message: 'Temperature reading has surpassed threshold',
      sensorId: telemetry.SensorId,
      reading: parseReading
    };

    sendNotification(telemetry.SensorId, "Sensor", JSON.stringify(alert));
  }
}

For a more complex user-defined function code sample, read the Occupancy quickstart.

Create a role assignment

Create a role assignment for the user-defined function to run under. If no role assignment exists for the user-defined function, it won't have the proper permissions to interact with the Management API or have access to perform actions on graph objects. Actions that a user-defined function may perform are specified and defined via role-based access control within the Azure Digital Twins Management APIs. For example, user-defined functions can be limited in scope by specifying certain roles or certain access control paths. For more information, read the Role-based access control documentation.

  1. Query the System API for all roles to get the role ID you want to assign to your user-defined function. Do so by making an authenticated HTTP GET request to:

    YOUR_MANAGEMENT_API_URL/system/roles
    

    Keep the desired role ID. It will be passed as the JSON body attribute roleId (YOUR_DESIRED_ROLE_IDENTIFIER) below.

  2. objectId (YOUR_USER_DEFINED_FUNCTION_ID) will be the user-defined function ID that was created earlier.

  3. Find the value of path (YOUR_ACCESS_CONTROL_PATH) by querying your spaces with fullpath.

  4. Copy the returned spacePaths value. You'll use that below. Make an authenticated HTTP GET request to:

    YOUR_MANAGEMENT_API_URL/spaces?name=YOUR_SPACE_NAME&includes=fullpath
    
    Value Replace with
    YOUR_SPACE_NAME The name of the space you wish to use
  5. Paste the returned spacePaths value into path to create a user-defined function role assignment by making an authenticated HTTP POST request to:

    YOUR_MANAGEMENT_API_URL/roleassignments
    

    With JSON body:

    {
      "roleId": "YOUR_DESIRED_ROLE_IDENTIFIER",
      "objectId": "YOUR_USER_DEFINED_FUNCTION_ID",
      "objectIdType": "YOUR_USER_DEFINED_FUNCTION_TYPE_ID",
      "path": "YOUR_ACCESS_CONTROL_PATH"
    }
    
    Value Replace with
    YOUR_DESIRED_ROLE_IDENTIFIER The identifier for the desired role
    YOUR_USER_DEFINED_FUNCTION_ID The ID for the user-defined function you want to use
    YOUR_USER_DEFINED_FUNCTION_TYPE_ID The ID specifying the user-defined function type (UserDefinedFunctionId)
    YOUR_ACCESS_CONTROL_PATH The access control path

Tip

Read the article How to create and manage role assignments for more information about user-defined function Management API operations and endpoints.

Send telemetry to be processed

The sensor defined in the spatial intelligence graph sends telemetry. In turn, the telemetry triggers the execution of the user-defined function that was uploaded. The data processor picks up the telemetry. Then, an execution plan is created for the invocation of the user-defined function.

  1. Retrieve the matchers for the sensor the reading was generated from.
  2. Depending on what matchers were evaluated successfully, retrieve the associated user-defined functions.
  3. Execute each user-defined function.

Next steps