Changed Elements

Diffing pipelines & strategies

This reference describes every strategy and pipeline step available when configuring a custom diffing pipeline.

For more guidance on strategies, output configuration, and submitting a diffing plan, see Diffing Plan Overview.

A pipeline is an ordered list of steps. Each step receives a collection of changed instances, transforms it, and passes the results to the next step.

Key pipeline rules:

  1. Maintain correct step order. Each pipeline step may rely on fields produced by previous steps. Make sure that any step reading a field comes after the step that creates it. If an expected field is missing, pipeline validation fails.
  2. ECInstanceId is always kept. The element id (ECInstanceId) cannot be removed from any record, not even by filtering steps.
  3. Missing prerequisites are inserted automatically. If a step lists another step as a prerequisite, the required step is added automatically (only once, even if needed by multiple steps). This applies only to declared step prerequisites, not field dependencies. If a step needs a field that isn’t available, validation still fails. For example:
    • If you specify only { "name": "drop-duplicate-updates" }, the mandatory compute-changes prerequisite step is inserted automatically before it.
    • If you add { "name": "add-model-id" } alongside other steps that depend on it earlier in the pipeline, the shared prerequisite is inserted just once, regardless of how many steps depend on it.

Key concepts used throughout this documentation:

  • $meta — per-change metadata added by compute-changes (includes the change operation and stage).
  • ECInstanceId / ECClassId — the element's id and its class id.
  • classFullName — the human-readable class name, e.g., Generic:PhysicalObject.

Pipeline steps

The following sections explain each pipeline step, including its function, input and output fields, configuration options, recommended use cases, and example scenarios.

compute-changes

Reads the changeset range and emits the raw changed instances.

What it does

Computes the source change data. It consolidates changes across the requested changeset range and adds change metadata ($meta) for each changed instance. This is typically the first step in a pipeline.

Technical detail: Byte-stream properties (e.g. GeometryStream, binary properties) are always abbreviated to only say its size, e.g. "{\"bytes\":81}".

Contract

  • Adds:
    • ECInstanceId
    • ECClassId
    • $meta

Why use it

Required for pipelines that process live changeset data. This step introduces change data into the pipeline and is typically the starting point for custom diffing workflows.

Examples

Usage in a pipeline
// Standard opening step — followed by drop-duplicate-updates in almost every pipeline
{ "name": "compute-changes" }
// Skip the "old" stage records at the source instead of adding a separate drop-duplicate-updates step later
{ "name": "compute-changes", "config": { "skipOldStageRecords": true } }
What inputs and outputs look like

Example input: none — this is the source step, so there's no prior instance to transform. It reads the changeset range instead.

Example output (an inserted element):

{
  "ECInstanceId": "0x1",
  "ECClassId": "0xc1",
  "$meta": { "op": "Inserted", "stage": "New" },
  "exampleChangedProperty": "ExampleChangedValue",
  "GeometryStream": "{\"bytes\":81}"
}

An updated element instead produces two records — one per stage:

[
  { "ECInstanceId": "0x2", "ECClassId": "0xc1", "$meta": { "op": "Updated", "stage": "Old" }, "exampleChangedProperty": "OldValue", "GeometryStream": "{\"bytes\":64}" },
  { "ECInstanceId": "0x2", "ECClassId": "0xc1", "$meta": { "op": "Updated", "stage": "New" }, "exampleChangedProperty": "NewValue", "GeometryStream": "{\"bytes\":81}" }
]

drop-duplicate-updates

Collapses the two records of an updated element down to one.

What it does

For an updated element, compute-changes emits two records — the "old" state and the "new" state. This step keeps the "new" record and drops the "old" one, so each updated element appears exactly once. It removes whole records (rows), not fields.

Contract

  • Reads:
    • $meta.op
    • $meta.stage
  • Prerequisites:
    • compute-changes

Why use it

Use when you need one record per changed element, instead of before/after copies. It keeps counts accurate and avoids double-processing updated elements downstream.

Examples

Usage in a pipeline
{
  "pipeline": [
    { "name": "compute-changes" },
    { "name": "drop-duplicate-updates" },
    { "name": "add-class-full-name" }
  ],
  "output": { "format": "basic" }
}

A lightweight pipeline that reports one row per changed element — no before/after duplicates — with its class name attached.

What inputs and outputs look like

Example input: the two stage records compute-changes emitted for an updated element:

[
  { "ECInstanceId": "0x2", "ECClassId": "0xc1", "$meta": { "op": "Updated", "stage": "Old" }, "exampleChangedProperty": "OldValue" },
  { "ECInstanceId": "0x2", "ECClassId": "0xc1", "$meta": { "op": "Updated", "stage": "New" }, "exampleChangedProperty": "NewValue" }
]

Example output (only the "New" record survives):

{
  "ECInstanceId": "0x2",
  "ECClassId": "0xc1",
  "$meta": { "op": "Updated", "stage": "New" },
  "exampleChangedProperty": "NewValue"
}

add-class-full-name

Adds the readable class name for each element.

What it does

Resolves each instance's ECClassId to a human-readable class name and adds it as classFullName (for example, Generic:PhysicalObject).

Contract

  • Reads:
    • ECClassId
  • Adds:
    • classFullName

Why use it

Use when your application needs to display or group results by class name. For example to display What kind of thing changed.

Examples

Usage in a pipeline
{
  "pipeline": [
    { "name": "compute-changes" },
    { "name": "drop-duplicate-updates" },
    { "name": "add-class-full-name" },
    { "name": "filter-fields", "config": { "keepOnly": ["ECInstanceId", "ECClassId", "classFullName", "$meta"] } }
  ],
  "output": { "format": "basic", "idEncoding": "decimal" }
}

A UI-friendly change list: each row carries the element id, its class name (e.g. Generic:PhysicalObject), and the change operation — nothing else.

What inputs and outputs look like

Example input:

{
  "ECInstanceId": "0x1",
  "ECClassId": "0xc1",
  "exampleChangedProperty": "ExampleChangedValue"
}

Example output after processing this step:

{
  "ECInstanceId": "0x1",
  "ECClassId": "0xc1",
  "exampleChangedProperty": "ExampleChangedValue",
  "classFullName": "Generic:PhysicalObject"
}

filter-fields

Keeps or removes fields on every instance.

What it does

Controls the shape of the output by keeping only selected fields, or removing selected fields.

Note: ECInstanceId is always kept (in order to preserve an element's identity).

Contract

  • Adds:
    • the fields named in keepOnly
  • Removes:
    • the fields named in remove, or all fields except those in keepOnly
  • Config: provide at most one of:
    • keepOnly (string[]) — keep only these fields (plus ECInstanceId), drop everything else.
    • remove (string[]) — remove exactly these fields, keep everything else.

Why use it

Use this step to reduce the size of the output and return only the data that is relevant to your workflow.

Examples

Usage in a pipeline
// Whitelist: keep only enough to identify what changed, drop all property and geometry data
{ "name": "filter-fields", "config": { "keepOnly": ["ECInstanceId", "ECClassId", "$meta"] } }
// Denylist: keep everything except the one field you do not want to ship (e.g. a large geometry blob)
{ "name": "filter-fields", "config": { "remove": ["GeometryStream"] } }

Use keepOnly when you know exactly which fields consumers need; use remove when you want to keep everything else by default and only trim a known offender.

{
  "pipeline": [
    { "name": "compute-changes" },
    { "name": "filter-fields", "config": { "remove": ["GeometryStream"] } }
  ]
}

Full property-level comparison — every field intact except the (often large) geometry stream.

{
  "pipeline": [
    { "name": "compute-changes" },
    { "name": "drop-duplicate-updates" },
    { "name": "filter-fields", "config": { "keepOnly": ["ECInstanceId", "ECClassId", "$meta"] } }
  ],
  "output": { "format": "basic" }
}

The smallest possible change feed: just which elements changed and how (inserted/updated/deleted) — no property data at all.

What inputs and outputs look like

Example input:

{
  "ECInstanceId": "0x1",
  "ECClassId": "0xc1",
  "UserLabel": "Wall-A12",
  "GeometryStream": "<binary blob>",
  "exampleChangedProperty": "ExampleChangedValue",
  "$meta": { "op": "Updated", "stage": "New" }
}

Example output with { "keepOnly": ["ECInstanceId", "ECClassId", "$meta"] }exampleChangedProperty is dropped along with everything else not listed:

{
  "ECInstanceId": "0x1",
  "ECClassId": "0xc1",
  "$meta": { "op": "Updated", "stage": "New" }
}

Example output with { "remove": ["GeometryStream"] } on the same input — everything else, including exampleChangedProperty, is untouched:

{
  "ECInstanceId": "0x1",
  "ECClassId": "0xc1",
  "UserLabel": "Wall-A12",
  "exampleChangedProperty": "ExampleChangedValue",
  "$meta": { "op": "Updated", "stage": "New" }
}

drop-non-elements

Removes instances that are not elements.

What it does

Filters out relationships and other non-element instances. Only classes derived from BisCore:Element are retained. It removes entire rows (instances), not fields.

Contract

  • Reads:
    • ECClassId

Why use it

Use it when you only care about changes to elements.

Examples

Usage in a pipeline
{
  "pipeline": [
    { "name": "compute-changes" },
    { "name": "drop-duplicate-updates" },
    { "name": "drop-non-elements" }
  ]
}

Use this when a changeset range includes relationship changes you do not want surfaced — for example, when consumers only expect to see physical or graphical elements.

What inputs and outputs look like

Example input (an element alongside a relationship row):

[
  { "ECInstanceId": "0x1", "ECClassId": "0xc1", "exampleChangedProperty": "ExampleChangedValue", "$meta": { "op": "Updated", "stage": "New" } },
  {
    "ECInstanceId": "0x9",
    "ECClassId": "0x9c",
    "SourceECInstanceId": "0x1",
    "TargetECInstanceId": "0x5",
    "$meta": { "op": "Inserted", "stage": "New" }
  }
]

Example output (the relationship row, 0x9, is dropped entirely):

[
  { "ECInstanceId": "0x1", "ECClassId": "0xc1", "exampleChangedProperty": "ExampleChangedValue", "$meta": { "op": "Updated", "stage": "New" } }
]

drop-non-geometric-elements

Removes instances that are not geometric elements.

What it does

Filters out instances whose class does not derive from BisCore:GeometricElement — for example, non-graphical elements, definition elements, and relationships. It removes entire instances.

Contract

  • Reads:
    • ECClassId

Why use it

Use it when you only care about changes that can be visualized, such as physical or graphical elements, and want to discard non-graphical changes.

Examples

Usage in a pipeline
{
  "pipeline": [
    { "name": "compute-changes" },
    { "name": "drop-duplicate-updates" },
    { "name": "drop-non-geometric-elements" }
  ]
}
What inputs and outputs look like

Example input (a geometric element alongside a relationship, which is also non-geometric):

[
  { "ECInstanceId": "0x1", "ECClassId": "0xc1", "exampleChangedProperty": "ExampleChangedValue", "$meta": { "op": "Updated", "stage": "New" } },
  {
    "ECInstanceId": "0x9",
    "ECClassId": "0x9c",
    "SourceECInstanceId": "0x1",
    "TargetECInstanceId": "0x5",
    "$meta": { "op": "Inserted", "stage": "New" }
  }
]

add-model-id

Ensures every element has a valid model identifier.

What it does

Adds the modelId field when it is missing or attempts to resolve if it is empty.

Contract

  • Reads:
    • ECInstanceId
    • ECClassId
  • Adds:
    • modelId

Why use it

Use when you need to organize or filter changes by model id.

Examples

Usage in a pipeline
{
  "pipeline": [
    { "name": "compute-changes" },
    { "name": "drop-duplicate-updates" },
    { "name": "add-model-id" }
  ]
}

Guarantees every element in the result carries a real modelId.

What inputs and outputs look like

Example input (no modelId yet):

{
  "ECInstanceId": "0x1",
  "ECClassId": "0xc1",
  "exampleChangedProperty": "ExampleChangedValue"
}

Example output after resolving the model from the iModel:

{
  "ECInstanceId": "0x1",
  "ECClassId": "0xc1",
  "exampleChangedProperty": "ExampleChangedValue",
  "modelId": "0x20"
}

Combining pipeline steps

Combine steps in the order they should process the data. Declared prerequisites will be added automatically if missed, and duplicate steps are included only once. However, the order of the steps you specify still matters because required fields must be available before a step reads them.

A typical lightweight change list pipeline might look like this: compute changes, remove old-state records, add the full class name, and return only the selected fields.

{
  "pipeline": [
    { "name": "compute-changes" },
    { "name": "drop-duplicate-updates" },
    { "name": "add-class-full-name" },
    { "name": "filter-fields", "config": { "keepOnly": ["ECInstanceId", "ECClassId", "classFullName", "$meta"] } }
  ],
  "output": { "format": "basic", "idEncoding": "decimal" }
}
Example input & output of the above pipeline

Example instances (starting from the output of the compute-changes step):

[
  { "ECInstanceId": "0x2", "ECClassId": "0xc1", "$meta": { "op": "Updated", "stage": "Old" }, "exampleChangedProperty": "OldValue" },
  { "ECInstanceId": "0x2", "ECClassId": "0xc1", "$meta": { "op": "Updated", "stage": "New" }, "exampleChangedProperty": "NewValue" }
]

Example output after the full pipeline runs — deduplicated, with a class name and exampleChangedProperty trimmed off by filter-fields:

{
  "ECInstanceId": "0x2",
  "ECClassId": "0xc1",
  "classFullName": "Generic:PhysicalObject",
  "$meta": { "op": "Updated", "stage": "New" }
}

A custom pipeline can combine filtering with a model lookup for each element. This example pipeline removes duplicate update records and non-element instances, then adds the model id for each remaining element:

{
  "pipeline": [
    { "name": "compute-changes" },
    { "name": "drop-duplicate-updates" },
    { "name": "drop-non-elements" },
    { "name": "add-model-id" }
  ],
  "output": { "format": "basic" }
}
Example input & output of the above pipeline

Example instance (an inserted element alongside a relationship row):

[
  { "ECInstanceId": "0x1", "ECClassId": "0xc1", "exampleChangedProperty": "ExampleChangedValue", "$meta": { "op": "Inserted", "stage": "New" } },
  {
    "ECInstanceId": "0x9",
    "ECClassId": "0x9c",
    "SourceECInstanceId": "0x1",
    "TargetECInstanceId": "0x5",
    "$meta": { "op": "Inserted", "stage": "New" }
  }
]

Example output after the full pipeline runs — the relationship row is dropped and the element gains a resolved modelId:

[
  {
    "ECInstanceId": "0x1",
    "ECClassId": "0xc1",
    "exampleChangedProperty": "ExampleChangedValue",
    "$meta": { "op": "Inserted", "stage": "New" },
    "modelId": "0x20"
  }
]

Need complete change-detection results? Use the VersionCompare strategy to return element type, operation, model, parent, and property-level changes. You do not need to assemble these results manually from individual pipeline steps. See Choosing a Strategy for available strategies.

Custom pipelines are best for focused transformations and lightweight change feeds.

Predefined strategies

A strategy in a diffing plan is a predefined pipeline with a default output configuration.

Basic and Full strategies are composed entirely of documented pipeline steps, making them useful starting points for custom pipelines. VersionCompare uses additional internal processing and cannot be recreated using pipeline steps alone.

Strategy
Steps
Default Output
Basic
compute-changesdrop-duplicate-updatesadd-class-full-namefilter-fields
{ "format": "basic", "idEncoding": "decimal" }
Full
compute-changes
(none — passthrough)
VersionCompare
Fixed internal pipeline (not reproducible via a custom pipeline)
{ "format": "versionCompare" }

Basic

Produces a lightweight change list containing: element id, class name, and operation only.

{
  "pipeline": [
    { "name": "compute-changes" },
    { "name": "drop-duplicate-updates" },
    { "name": "add-class-full-name" },
    { "name": "filter-fields", "config": { "keepOnly": ["ECInstanceId", "ECClassId", "classFullName", "$meta"] } }
  ],
  "output": { "format": "basic", "idEncoding": "decimal" }
}
Example input/output

Example input (the raw changeset: an inserted element, plus an updated element's two stage records):

[
  { "ECInstanceId": "0x1", "ECClassId": "0xc1", "$meta": { "op": "Inserted", "stage": "New" } },
  { "ECInstanceId": "0x2", "ECClassId": "0xc1", "$meta": { "op": "Updated", "stage": "Old" } },
  { "ECInstanceId": "0x2", "ECClassId": "0xc1", "$meta": { "op": "Updated", "stage": "New" } }
]

Example output (the basic format: one minimal { id, classFullName, operation } record per element, ids decimal-encoded per idEncoding):

[
  { "id": "1", "classFullName": "Generic:PhysicalObject", "operation": "Inserted" },
  { "id": "2", "classFullName": "Generic:PhysicalObject", "operation": "Updated" }
]

Equivalent shorthand: { "strategy": "Basic" }.

Full

The complete change record for each changed instance, including change metadata.

{
  "pipeline": [
    { "name": "compute-changes" }
  ]
}
Example input/output

Example input:

{
  "ECInstanceId": "0x1",
  "ECClassId": "0xc1",
  "UserLabel": "Wall-A12",
  "GeometryStream": "{\"bytes\":81}",
  "$meta": { "op": "Inserted", "stage": "New" }
}

Example output (passthrough — every field intact):

{
  "ECInstanceId": "0x1",
  "ECClassId": "0xc1",
  "UserLabel": "Wall-A12",
  "GeometryStream": "{\"bytes\":81}",
  "$meta": { "op": "Inserted", "stage": "New" }
}

Full has no default output block — results pass through with every field intact.

Equivalent shorthand: { "strategy": "Full" }.

VersionCompare

The same detailed change information returned by the Version Compare V2 API, including: changed elements with their change type, operation, model information, parent relationships, and property-level differences.

VersionCompare uses an internal pipeline that is not exposed through custom pipeline steps.

{ "strategy": "VersionCompare" }
Example input/output

Example input: none to hand-construct — VersionCompare reads the changeset range itself via its internal pipeline, the same way compute-changes does for custom pipelines.

Example output (the versionCompare format: a columnar envelope, one array slot per changed element):

{
  "changedElements": {
    "elements": ["0x2"],
    "classIds": ["0xc1"],
    "opcodes": [23],
    "type": [1],
    "modelIds": ["0x20"],
    "parentIds": ["0"],
    "parentClassIds": ["0"],
    "properties": [["UserLabel"]]
  }
}

Default output: { "format": "versionCompare" }.

Using a strategy's pipeline as a starting point

Strategies can be used as steps in a custom pipeline. When encountered, the strategy expands to its complete underlying pipeline before processing continues.

For example, you might start with the Basic pipeline and add add-model-id:

{
  "pipeline": [
    { "strategy": "Basic" },
    { "name": "add-model-id" }
  ]
}

which is equivalent to:

{
  "pipeline": [
    { "name": "compute-changes" },
    { "name": "drop-duplicate-updates" },
    { "name": "add-class-full-name" },
    { "name": "filter-fields", "config": { "keepOnly": ["ECInstanceId", "ECClassId", "classFullName", "$meta"] } },
    { "name": "add-model-id" }
  ]
}
Example input/output

Example input (the two stage records for an updated element):

[
  { "ECInstanceId": "0x2", "ECClassId": "0xc1", "$meta": { "op": "Updated", "stage": "Old" } },
  { "ECInstanceId": "0x2", "ECClassId": "0xc1", "$meta": { "op": "Updated", "stage": "New" } }
]

Example output (Basic's steps run first, then add-model-id adds modelId; since this custom pipeline has no output block, results pass through with every remaining field intact):

{
  "ECInstanceId": "0x2",
  "ECClassId": "0xc1",
  "classFullName": "Generic:PhysicalObject",
  "$meta": { "op": "Updated", "stage": "New" },
  "modelId": "0x20"
}

If you need to change or remove one of Basic's or Full's steps, copy its full pipeline from above into your request's pipeline array and edit it directly — strategies can only be used whole or inlined, not partially overridden. VersionCompare can only be used whole (as a strategy or inlined), since its internal pipeline is not expressible as a custom pipeline.

Was this page helpful?