Changed Elements V3 - Configurable Pipelines Tutorial

Introduction

Changed Elements API V3 creates asynchronous diff jobs for a range of iModel changesets. This tutorial shows how to create and customize those jobs so that the results contain only the change data that your workflow needs.

The examples below demonstrate how to create, monitor, and delete diff jobs, as well as how to configure custom processing pipelines. The examples use placeholder GUIDs for iTwinId and iModelId.

Pre-requisites

  1. All requests require you to pass an Authorization header with a valid access token.
  2. You must have access to your own iTwin and iModel.
  3. Use an HTTP REST client of your choice, such as Postman or curl.

Info

Skill level:

Basic

Duration:

25 minutes

How to use the API

Here's the typical workflow for using the Changed Elements API V3:

  1. POST /diff returns a job in the Queued state.
  2. GET /diff/{id}?iTwinId={iTwinId}&iModelId={iModelId} to poll the job resource until it is Completed. (Alternatively, use webhooks to be notified when the job completes. This is out of scope for this tutorial.)
  3. Fetch the href URL to download the result JSON blob.

In the examples in this tutorial, replace the placeholder GUIDs with your own iTwinId and iModelId, and adjust the changeset range to the one you'd like to inspect.

Note: V3 accepts changeset indices (positive integers) and changeset ids (40-character hashes). We will use indices for simplicity.

Create a diff job

http
POST https://api.bentley.com/changedelements/diff
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json

{
  "iTwinId": "11111111-1111-1111-1111-111111111111",
  "iModelId": "11111111-1111-1111-1111-111111111111",
  "startChangeset": 12,
  "endChangeset": 18,
  "diffingPlan": {
    "pipeline": [
      { "name": "compute-changes" }
    ]
  }
}

Poll until completed and download HREF

http
GET https://api.bentley.com/changedelements/diff/3fa85f64-5717-4562-b3fc-2c963f66afa6?iTwinId=11111111-1111-1111-1111-111111111111&iModelId=11111111-1111-1111-1111-111111111111 HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json

HTTP/1.1 200 OK

{
  "job": {
    "jobId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "status": "Completed",
    "href": "https://example.blob.core.windows.net/results.json?sv=..."
  }
}

GET https://example.blob.core.windows.net/results.json?sv=... HTTP/1.1

Delete a diff job when it is no longer needed

http
DELETE https://api.bentley.com/changedelements/diff/3fa85f64-5717-4562-b3fc-2c963f66afa6?iTwinId=11111111-1111-1111-1111-111111111111&iModelId=11111111-1111-1111-1111-111111111111 HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json

HTTP/1.1 204 No Content

Pipeline steps primer: configuring your diff

Now that we've covered the basics, let's dive into configuring your own diffing pipelines to cater to your specific needs.

A custom diffingPlan contains a pipeline array. Every pipeline needs a compute-changes step: it is the source step that reads and computes the changes over the requested changeset range. You may list it explicitly, as this tutorial does, or it will be inserted automatically as a prerequisite to another step. That is why it always appears in a resolved pipeline JSON.

Here's a list of useful steps you can use in your configuration:

  • drop-duplicate-updates: keeps only the new record for updates.
  • drop-non-elements: removes relationships and other non-elements.
  • filter-fields: keeps or removes fields.
  • add-class-full-name: queries for and adds the full class name to each element.
  • add-model-id: queries for and adds the model id to each element.

See the full documentation for more details.

The optional output object controls the final result, including id encoding, selected output fields, and field names.

Predefined Basic, Full, and VersionCompare strategies are shortcuts for common workflows. They are useful starting points, but this tutorial focuses on custom pipelines. A strategy can also be used as one step inside a pipeline when you want to extend it.

Diffing plan example

json
{
  // ...
  "diffingPlan": {
    "pipeline": [
      { "name": "compute-changes" },
      { "name": "drop-duplicate-updates" },
      { "name": "filter-fields", "config": { /* ... */ } },
      { "name": "add-class-full-name" },
      { "name": "add-model-id" }
    ]
  }
}

Output configuration primer: shaping the final result

While pipeline steps transform the change data as it is processed, the optional output object controls how the final records are written to the result blob. It runs after the last pipeline step.

Here are the options you can use:

  • format: overall shape of the payload. basic produces minimal { id, classFullName, operation } records; versionCompare produces the columnar Version Compare shape. Omit it to return records exactly as the pipeline produced them.
  • idEncoding: how element ids are encoded — hex (0x..., the default) or decimal.
  • keepFields: allow-list of fields to retain in the final records.
  • dropFields: deny-list of fields to remove. Mutually exclusive with keepFields — provide at most one.
  • renameFields: maps an original field name to a new output name. Applied last, after fields are kept or dropped.

Note that the element id field is always preserved, even when not listed in keepFields. When a strategy already defines a default output (for example, Basic), providing your own output replaces that default entirely.

The examples that follow use these options progressively: Example 1 omits output for raw passthrough, Example 2 applies format and idEncoding, and Example 3 shapes the payload with keepFields and renameFields.

Output configuration example

json
{
  // ...
  "diffingPlan": {
    "pipeline": [ /* ... */ ],
    "output": {
      "format": "basic",
      "idEncoding": "decimal",
      "keepFields": [ /* ... */ ],
      "renameFields": { /* ... */ }
    }
  }
}

Example 1: The simplest diff

The smallest valid plan runs only the compute-changes source step with no output block. This produces the simplest output a diff job can have: an array of ChangeInstance records containing all the changed data inside each changed EC instance.

What the diffing plan does

  1. compute-changes: Reads the requested changeset range and emits one record per changed EC instance, carrying every changed property captured in the changesets along with the $meta change metadata (e.g., op and stage).

What the output looks like

  1. With no output configuration, records pass through untouched — all raw property values are preserved. The only exception is byte-stream properties (e.g. GeometryStream): they are always abbreviated to their size, such as "{\"bytes\":81}".
  2. Updated elements produce two records: one for the old state ("stage": "Old") and one for the new state ("stage": "New"). Inserted and deleted elements produce a single record.
  3. Ids use the default hex (0x...) encoding.

Intended workflow

Use this plan when you want the complete change data and prefer to post-process it yourself. The next examples build on this exact plan, progressively trimming and shaping this raw feed.

Create the job

http
POST https://api.bentley.com/changedelements/diff HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json
Content-Type: application/json

{
  "iTwinId": "11111111-1111-1111-1111-111111111111",
  "iModelId": "11111111-1111-1111-1111-111111111111",
  "startChangeset": 12,
  "endChangeset": 18,
  "diffingPlan": {
    "pipeline": [
      { "name": "compute-changes" }
    ]
  }
}

HTTP/1.1 202 Accepted

{
  "job": {
    "jobId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "status": "Queued"
  }
}

Poll for job completion

http
GET https://api.bentley.com/changedelements/diff/3fa85f64-5717-4562-b3fc-2c963f66afa6?iTwinId=11111111-1111-1111-1111-111111111111&iModelId=11111111-1111-1111-1111-111111111111 HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json

HTTP/1.1 200 OK

{
  "job": {
    "status": "Completed",
    "href": "https://example.blob.core.windows.net/example-1.json?sv=..."
  }
}

Download the result blob

json
GET https://example.blob.core.windows.net/example-1.json?sv=...

[
  {
    "ECInstanceId": "0x2",
    "ECClassId": "0x100",
    "$meta": { "op": "Updated", "stage": "Old" },
    "UserLabel": "Wall-A11",
    "Category": { "Id": "0x17", "RelECClassId": "0x6c" },
    "LastMod": "2026-08-30T12:01:33.000Z"
  },
  {
    "ECInstanceId": "0x2",
    "ECClassId": "0x100",
    "$meta": { "op": "Updated", "stage": "New" },
    "UserLabel": "Wall-A12",
    "Category": { "Id": "0x17", "RelECClassId": "0x6c" },
    "LastMod": "2026-09-01T09:14:02.000Z"
  },
  {
    "ECInstanceId": "0x3",
    "ECClassId": "0x100",
    "$meta": { "op": "Inserted", "stage": "New" },
    "UserLabel": "Door-B04",
    "Category": { "Id": "0x17", "RelECClassId": "0x6c" },
    "LastMod": "2026-09-01T10:42:19.000Z"
  }
]

Example 2: Getting high-level changes

Building on Example 1, this plan keeps the same source step but turns the data into a lightweight record per changed element. It removes the old copy emitted for updates, removes non-elements, and adds readable class names. It utilizes the basic output format to do this. Compare the result blob with Example 1: the same two elements now produce just two minimal records.

What the diffing plan does

  1. compute-changes: Same source step as Example 1.
  2. drop-duplicate-updates: Removes the old-state ("stage": "Old") record for each update, keeping only the new one.
  3. drop-non-elements: Removes any instance that does not inherit from the BisCore:Element EC base class, effectively keeping only element instances.
  4. add-class-full-name: Adds human-readable class full names to the results, making it easier to do ECSQL queries or understand what the elements represent.

What the output format does

  1. "format": "basic": Converts the pipeline records into a simplified structure containing only the essential fields: { id, classFullName, operation }. If not provided, the full records from Example 1 would be returned instead.
  2. "idEncoding": "hex": Keeps ids in their usual hex string form: 0x.... You could alternatively use "idEncoding": "decimal" to represent ids as decimal numbers.

Intended workflow

Since the results are very simple, containing only elements, their class names, and their operation, this workflow is ideal for quickly identifying what has changed without providing any information about changed properties or element data. This is very similar to what our predefined Basic strategy provides.

Create the job

http
POST https://api.bentley.com/changedelements/diff HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json
Content-Type: application/json

{
  "iTwinId": "11111111-1111-1111-1111-111111111111",
  "iModelId": "11111111-1111-1111-1111-111111111111",
  "startChangeset": 12,
  "endChangeset": 18,
  "diffingPlan": {
    "pipeline": [
      { "name": "compute-changes" },
      { "name": "drop-duplicate-updates" },
      { "name": "drop-non-elements" },
      { "name": "add-class-full-name" }
    ],
    "output": {
      "format": "basic",
      "idEncoding": "hex"
    }
  }
}

HTTP/1.1 202 Accepted

{
  "job": {
    "jobId": "9c799a9b-9cd7-47e1-93ea-64ed5f5d978f",
    "status": "Queued"
  }
}

Poll for job completion

http
GET https://api.bentley.com/changedelements/diff/9c799a9b-9cd7-47e1-93ea-64ed5f5d978f?iTwinId=11111111-1111-1111-1111-111111111111&iModelId=11111111-1111-1111-1111-111111111111 HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json

HTTP/1.1 200 OK

{
  "job": {
    "status": "Completed",
    "href": "https://example.blob.core.windows.net/example-2.json?sv=..."
  }
}

Download the result blob

json
GET https://example.blob.core.windows.net/example-2.json?sv=...

[
  {
    "id": "0x2",
    "classFullName": "Generic:PhysicalObject",
    "operation": "Updated"
  },
  {
    "id": "0x3",
    "classFullName": "Generic:PhysicalObject",
    "operation": "Inserted"
  }
]

Example 3: A fully tailored diff

This final example integrates all the steps from this tutorial into one plan. It builds on Example 2 by enriching each element with its model id, trimming intermediate fields early, and shaping the final payload by selecting and renaming certain fields as suitable for a downstream system.

What the diffing plan does

  1. compute-changes, drop-duplicate-updates, drop-non-elements, and add-class-full-name: Same source, deduplication, cleanup, and enrichment as Example 2.
  2. add-model-id: Enriches each retained element with the id of its containing model.
  3. filter-fields.keepOnly: Removes all intermediate properties except those named, while always preserving ECInstanceId by design. Trimming early keeps the processed data small.

What the output configuration does

  1. keepFields: Allow-list applied to the final records; anything not listed is removed.
  2. renameFields: Applied last, after field selection, producing the integration-oriented names shown in the result blob (elementId, class, sourceModelId, label). Note that the element id field is always preserved, so renaming ECInstanceId adds the new name alongside it rather than replacing it.

Intended workflow

Use this shape when a downstream consumer needs a small, stable payload with its own naming conventions, without any post-processing on your side.

Create the job

http
POST https://api.bentley.com/changedelements/diff HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json
Content-Type: application/json

{
  "iTwinId": "11111111-1111-1111-1111-111111111111",
  "iModelId": "11111111-1111-1111-1111-111111111111",
  "startChangeset": 12,
  "endChangeset": 18,
  "diffingPlan": {
    "pipeline": [
      { "name": "compute-changes" },
      { "name": "drop-duplicate-updates" },
      { "name": "drop-non-elements" },
      { "name": "add-class-full-name" },
      { "name": "add-model-id" },
      {
        "name": "filter-fields",
        "config": {
          "keepOnly": ["ECInstanceId", "classFullName", "modelId", "UserLabel", "$meta"]
        }
      }
    ],
    "output": {
      "keepFields": ["ECInstanceId", "classFullName", "modelId", "UserLabel", "$meta"],
      "renameFields": {
        "ECInstanceId": "elementId",
        "classFullName": "class",
        "modelId": "sourceModelId",
        "UserLabel": "label"
      }
    }
  }
}

HTTP/1.1 202 Accepted

{
  "job": {
    "jobId": "b7e3a917-0d9d-49c8-a5e1-f0b6ccf3bc6c",
    "status": "Queued"
  }
}

Poll for job completion

http
GET https://api.bentley.com/changedelements/diff/b7e3a917-0d9d-49c8-a5e1-f0b6ccf3bc6c?iTwinId=11111111-1111-1111-1111-111111111111&iModelId=11111111-1111-1111-1111-111111111111 HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json

HTTP/1.1 200 OK

{
  "job": {
    "status": "Completed",
    "href": "https://example.blob.core.windows.net/example-3.json?sv=..."
  }
}

Download the result blob

json
GET https://example.blob.core.windows.net/example-3.json?sv=...

[
  {
    "ECInstanceId": "0x2",
    "elementId": "0x2",
    "class": "Generic:PhysicalObject",
    "sourceModelId": "0x20",
    "label": "Wall-A12",
    "$meta": { "op": "Updated", "stage": "New" }
  },
  {
    "ECInstanceId": "0x3",
    "elementId": "0x3",
    "class": "Generic:PhysicalObject",
    "sourceModelId": "0x20",
    "label": "Door-B04",
    "$meta": { "op": "Inserted", "stage": "New" }
  }
]

Conclusion

You can now create a custom V3 diffing plan, follow its asynchronous job lifecycle, and download a result shaped for a specific consumer. Start with the smallest pipeline that produces the fields you need, then add filtering, enrichment, or output configuration only when it serves that consumer.

Remember the two validation rules that prevent most configuration mistakes: provide exactly one of strategy or pipeline, and do not set both keepFields and dropFields in one output block. Pipeline step names are case-sensitive, whereas strategy names are case-insensitive.

Was this page helpful?