Power Fx advanced reference
reference Facts, options, APIs
On this page
- Rules for writing CluedIn Power Fx formulas
- How formulas are evaluated
- Rule scopes and context variables
- The record schema
- Custom function quick reference
- GetVocabularyKeyValue
- VocabularyKeyExists
- SetVocabularyKeyValue
- SetEntityProperty
- AddTag
- FuzzyMatch
- LoadEntityByEntityCode
- LoadIncomingRelationships
- LoadOutgoingRelationships
- GetVersionBranches
- Patterns
- Common mistakes
- Errors and operational behaviour
- Configuration
- Formula editor endpoints
- Spark execution has a smaller function set
- Extending the function library
- Corrections to older descriptions
- Related articles
This is the complete reference for Power Fx in CluedIn rules: how a formula is evaluated, which context variables each rule scope supplies, the shape of the records those variables hold, and every CluedIn custom function with its parameters, return value, side effects and failure behaviour. It is written to be precise enough that a person - or an AI assistant such as Claude or ChatGPT - can write a correct formula from it without trial and error.
For an introduction with simple examples, start with Power Fx formulas. Read this page when a formula needs to compose several actions, read related records or version history, handle missing or malformed data deliberately, or run on Spark.
This reference describes the rule engine implementation as reviewed in October 2026 (Power Fx packages 1.8.1). What is available in a given installation depends on its release and configuration. Validate formula binding, scope variables, vocabulary mappings, error handling and the final record in the target environment before promoting a rule.
Rules for writing CluedIn Power Fx formulas
These rules summarise the rest of the page. Each one prevents a mistake that evaluates without an error but silently does the wrong thing.
- A condition must return a Boolean. Any other result type is logged and treated as
false. Do not wrap a comparison inIf(..., true, false). - An action must end with the record to apply. The engine writes the final value of the formula back to the current record. A scalar, a table, or the original
Entityafter a setter call discards the change. - Chain setters by nesting, not with
;. Pass the record returned by one setter into the next:AddTag(SetVocabularyKeyValue(Entity, "k", "v"), "t"). Nesting is always correct; a;sequence such asSetEntityProperty(...); Entitysilently loses the change. - Every branch of an action
Ifreturns a record. UseEntityas the “do nothing” branch. - Use the variable names of the scope.
Entityin data part, golden record and survivorship rules;GoldenRecordandDataPartin survivorship action conditions -Entitydoes not exist there. - Read vocabulary values with
GetVocabularyKeyValue, not with dot paths.Entity.Properties.customer.statusis invalid;Propertiesis a table ofKey/Valuerows. - Vocabulary values are text. Convert with
Value,DateValueorDateTimeValuebefore numeric or date comparison, and format withText(...)before writing a number or date back. - A missing value is
Blank(). Test withIsBlank, default withCoalesce. - Use current field names:
BusinessDomain(notEntityType),PrimaryIdentifier(notOriginEntityCode),Identifiers(notCodes). - Field names passed to
SetEntityPropertyare exact and case-sensitive, and some fields are read-only. A blocked write returns the original record without an error. - Use vocabulary setters for properties,
SetEntityPropertyfor top-level metadata. - Entity codes are full serialized codes such as
/Person#Somewhere:customer-123- entity type, origin and value - not a bare business identifier or a GUID. - Lookups cost data store reads. Bind a loaded record or relationship table once with
With. - Setters on a loaded record do not save it. Only the current record is written back.
- Use only the ten CluedIn functions listed on this page plus standard Power Fx functions. There are no CluedIn functions for HTTP calls, SQL, creating or deleting entities, editing edges, or removing tags.
- On Spark, the function set is smaller and key or property names must be string literals.
How formulas are evaluated
Power Fx is Microsoft’s strongly typed, Excel-style formula language. The host decides which functions and objects a formula can use. In CluedIn, formulas can use the standard Power Fx function library, the context variables described below, and the CluedIn custom functions. Power Apps controls, connectors and data sources are not available. See the Microsoft Power Fx overview for the language itself.
The engine enables the PowerFxV1 feature set, Set, mutation functions, JSON functions and regular expression functions. Examples on this page use comma argument separators and dot decimals.
Conditions return a Boolean
A condition - in a rule filter or a survivorship action condition - decides whether a record matches. A true result matches. A successful evaluation that returns anything other than a Boolean is logged and treated as false. An evaluation error is different: it is raised as an exception and logged in the rule’s execution log, not quietly turned into false.
Right(Entity.DisplayName, 1) = "t"
GetVocabularyKeyValue(Entity, "user.firstName") & " " &
GetVocabularyKeyValue(Entity, "user.lastName") =
GetVocabularyKeyValue(Entity, "user.fullName")
& joins text and = compares values, so the second formula is already a Boolean.
Actions return the record to apply
A formula action evaluates the formula and maps its final result back onto the current record’s metadata, provided that result is a record. A scalar or table result gives the engine nothing to write back.
SetVocabularyKeyValue(Entity, "user.fullName",
GetVocabularyKeyValue(Entity, "user.firstName") & " " &
GetVocabularyKeyValue(Entity, "user.lastName"))
The setter returns the record with its updated Properties table, and because it is the final value of the formula, that record is applied. This matters most with SetEntityProperty, which builds and returns a new record rather than changing the record it was given. See Carry the returned record forward.
Blank values and explicit conversions
A missing vocabulary value is Blank(). Vocabulary values are always text, so convert them before comparing numbers or dates. A failed conversion is an evaluation error, not false.
If(IsBlank(GetVocabularyKeyValue(Entity, "customer.age")),
false,
Value(GetVocabularyKeyValue(Entity, "customer.age")) >= 18)
This handles a missing age, but non-numeric text such as "unknown" still fails in Value. When malformed data should count as a non-match, catch the error explicitly:
IfError(Value(GetVocabularyKeyValue(Entity, "customer.age")) >= 18, false)
IfError catches Power Fx error values, such as a failed conversion or the NotFound error from LoadEntityByEntityCode. It does not catch .NET exceptions thrown by a function, which are listed per function below.
Rule scopes and context variables
The scope of a rule decides which variables the formula receives and which custom functions are registered. A function being registered in a scope and a particular record supporting it are separate questions: a function can be available yet return an empty result for a record that lacks the metadata it needs.
| Scope | Variables | What they hold |
|---|---|---|
| Data part | Entity | The current data part |
| Entity (golden record) | Entity | The current golden record |
| Survivorship | Entity | The golden record context supplied for survivorship |
| Survivorship action condition | GoldenRecord, DataPart | The golden record and the candidate data part, separately |
In a survivorship action condition, Entity does not exist:
GetVocabularyKeyValue(DataPart, "customer.status") = "Active"
For how these scopes map onto the rule types in the UI, see Rule types.
Function availability by scope
| Function | Data part | Entity | Survivorship | Survivorship action condition |
|---|---|---|---|---|
GetVocabularyKeyValue | Yes | Yes | Yes | Yes |
VocabularyKeyExists | Yes | Yes | Yes | Yes |
FuzzyMatch | Yes | Yes | Yes | Yes |
LoadEntityByEntityCode | Yes | Yes | Yes | Yes |
GetVersionBranches | No | Yes | Yes | Yes |
SetVocabularyKeyValue | Yes | Yes | No | No |
SetEntityProperty | Yes | Yes | No | No |
AddTag | Yes | Yes | No | No |
LoadIncomingRelationships | No | Yes | No | No |
LoadOutgoingRelationships | No | Yes | No | No |
“Yes” means the function is registered in that scope. Setters and AddTag belong in actions; the engine does not enforce a separate read-only function list for conditions, so do not rely on a registration error to keep a setter out of a filter.
GetVersionBranches(GoldenRecord) is meaningful in a survivorship action condition; GetVersionBranches(DataPart) returns an empty table because a data part is not a golden record. The relationship loaders need both Entity scope and a record backed by a real CluedIn entity.
The Spark engine registers fewer functions - see Spark execution.
The record schema
CluedIn converts its native metadata into Power Fx records and tables. The field names below are the names a formula uses at runtime.
| Field | Power Fx type | Meaning |
|---|---|---|
DisplayName, Name | Text or blank | Presentation name and underlying name |
BusinessDomain | Text | The business domain (entity type), for example /Person |
PrimaryIdentifier | Text | The origin entity code, serialized |
Description, Revision | Text or blank | Description and revision metadata |
CreatedDate, ModifiedDate, DiscoveryDate | Text or blank | Dates as text in round-trip (O) format |
Properties | Table of Key, Value (text) | Vocabulary key values |
Tags, Aliases | Single-column text tables | Each row has a Value field |
Identifiers | Table | Code, Key, Origin, Type, Value text fields |
Uri | Text or blank | Serialized URI |
DocumentMimeType, DocumentFileName, Encoding | Text or blank | Document metadata |
DocumentSize | Number or blank | Document size |
ProviderDefinitionId | GUID or blank | Provider definition identifier |
OutgoingEdges, IncomingEdges | Tables | Relationship edges carried on the record - see below |
_metadataId | GUID | Internal identity used by the runtime; do not set it |
Golden records additionally expose IsShadowEntity, IsExternalData and IsDeleted (Boolean, possibly blank), ProviderDefinitionIds (single-column GUID table), and Providers and VocabulariesUsed (single-column text tables).
When the result is written back, BusinessDomain maps to the native EntityType, PrimaryIdentifier to OriginEntityCode, and Identifiers to Codes. Formulas must use the runtime names; Entity.EntityType, Entity.OriginEntityCode and Entity.Codes do not exist.
First(Entity.Tags).Value = "Reviewed"
CountRows(Entity.Identifiers) > 1
DateTimeValue(Entity.ModifiedDate) > DateTimeValue("2025-01-01")
Guard empty tables before using First, and handle blank or invalid date text where it can occur.
The formula editor’s IntelliSense uses a separate sample schema. It describes dates as date-time values and omits IncomingEdges, while at runtime dates are text and both edge tables exist. When the editor and the runtime disagree, the runtime schema on this page is the one that applies.
Properties are rows, not fields
Entity.Properties is a table whose rows have Key and Value text fields. A vocabulary key name containing dots is a string stored in Key, not a nested path.
LookUp(Entity.Properties, Key = "customer.status", Value)
This reads the literally stored key. GetVocabularyKeyValue first resolves the vocabulary definition and its destination mapping, so it can intentionally read a different stored key. Use the custom function whenever you want CluedIn’s vocabulary semantics - which is almost always.
Edge rows on the record
Entity.OutgoingEdges and Entity.IncomingEdges rows have these text fields:
| Field | Meaning |
|---|---|
EdgeType | Serialized edge type |
ToReferenceCode, ToReferenceName, ToReferenceEntityId | Destination entity code, name and ID |
FromReferenceCode, FromReferenceName, FromReferenceEntityId | Source entity code, name and ID |
Loaded relationships have a different shape
LoadIncomingRelationships and LoadOutgoingRelationships query the primary data store and return rows with only two text fields, EdgeType and EntityCode, where EntityCode identifies the entity on the other end. They do not return the seven-column edge rows above, and they do not return the related records themselves.
| Source | EntityCode / codes identify | Fields |
|---|---|---|
LoadIncomingRelationships(Entity) | The source of each incoming edge | EdgeType, EntityCode |
LoadOutgoingRelationships(Entity) | The destination of each outgoing edge | EdgeType, EntityCode |
Entity.OutgoingEdges | Both ends, in separate fields | Seven edge fields |
Loading can find relationships that are not carried on the record, at the cost of a data store read. A Filter applied to the loaded table runs after loading; it does not narrow the query.
Custom function quick reference
CluedIn adds ten function names. All parameters are required. SetEntityProperty has five overloads, one per value type; every other function has one signature.
| Function | Signature | Returns | Scopes |
|---|---|---|---|
GetVocabularyKeyValue | (record, keyText) | Text or blank | All |
VocabularyKeyExists | (record, keyText) | Boolean | All |
SetVocabularyKeyValue | (record, keyText, valueText) | Updated record | Data part, Entity |
SetEntityProperty | (record, propertyText, value) | Rebuilt record, or error | Data part, Entity |
AddTag | (record, tagText) | Updated record | Data part, Entity |
FuzzyMatch | (leftText, rightText) | Number from 0 to 1 | All |
LoadEntityByEntityCode | (entityCodeText) | Golden record, or error | All |
LoadIncomingRelationships | (record) | Table of EdgeType, EntityCode | Entity |
LoadOutgoingRelationships | (record) | Table of EdgeType, EntityCode | Entity |
GetVersionBranches | (record) | Table of branches | Entity, Survivorship, Survivorship action condition |
Record arguments. Pass the scope’s context variables, or records returned by CluedIn functions. A record you build yourself with { ... } is accepted by the type checker but is not equivalent: the functions expect CluedIn’s internal table wrappers and, for history and relationships, a registered runtime identity.
“Updated record” is the runtime value. It is not a success flag, not a saved database object, and not a promise that every field will persist. Only the final record of an action is written back, and only to the current record.
Vocabulary mappings. The vocabulary readers and the vocabulary setter resolve a key through the organization’s vocabulary definitions, including its destination mapping, on every call. Register the vocabulary key - and any mapping - before relying on these functions. See Vocabulary keys.
GetVocabularyKeyValue
Reads a vocabulary value from a record, after resolving the vocabulary definition and its destination mapping.
GetVocabularyKeyValue(record, keyText)
| Parameter | Type | Description |
|---|---|---|
record | Record | A CluedIn context record, or a record returned by a CluedIn function |
keyText | Text | The full vocabulary key name, such as customer.status |
Returns the matching value as text. Returns blank when the vocabulary key is not defined, when the mapped key has no row, or when Properties is not a CluedIn properties table. A blank result does not say which of these happened.
Effect none; reads the vocabulary definitions and the record.
GetVocabularyKeyValue(Entity, "customer.status") = "Active"
Coalesce(GetVocabularyKeyValue(Entity, "customer.region"), "Unknown")
The second formula is a value, not a condition; use it inside a comparison or a setter.
Failure behaviour. The function expects at most one matching row. If Properties contains duplicate rows for the key, it throws instead of returning the first. Failures of the vocabulary service are not converted to blank.
VocabularyKeyExists
Tests whether the record has a row for a registered vocabulary key, after destination mapping. It tests presence, not whether the value is useful.
VocabularyKeyExists(record, keyText)
| Parameter | Type | Description |
|---|---|---|
record | Record | A CluedIn record containing Properties |
keyText | Text | The full registered vocabulary key name |
Returns true when the mapped key has at least one row; otherwise false. An undefined vocabulary key returns false, even if a row with that raw key exists.
Effect none.
VocabularyKeyExists(Entity, "customer.email")
A row with an empty value still counts as present. To require a populated value:
VocabularyKeyExists(Entity, "customer.email") &&
!IsBlank(GetVocabularyKeyValue(Entity, "customer.email"))
With duplicate rows, VocabularyKeyExists returns true while GetVocabularyKeyValue throws. Not available on Spark.
SetVocabularyKeyValue
Updates the value of a vocabulary key in the record’s Properties, or appends it if absent.
SetVocabularyKeyValue(record, keyText, valueText)
| Parameter | Type | Description |
|---|---|---|
record | Record | The CluedIn record to update - normally Entity |
keyText | Text | The full vocabulary key name |
valueText | Text | The text to store; format numbers and dates explicitly with Text |
Returns the record with the updated Properties. If Properties is not a CluedIn properties table, returns the record unchanged.
Effect updates the existing row for the mapped key, or appends a new row.
SetVocabularyKeyValue(Entity, "customer.status", "Active")
SetVocabularyKeyValue(Entity, "customer.reviewDate",
Text(DateValue("2026-10-22"), "yyyy-mm-dd"))
SetVocabularyKeyValue(Entity, "finance.salaryGrade",
If(Value(GetVocabularyKeyValue(Entity, "finance.salary")) >
Value(GetVocabularyKeyValue(Entity, "finance.targetSalary")),
"Above Target", "Below Target"))
The value is always stored as text; the setter does not change the vocabulary key’s data type.
Undefined keys. If the key has no vocabulary definition, the setter appends a row using the raw keyText - it does not create the definition. The readers will still return blank and false for that key. Repeating the write can append duplicate rows rather than updating the first one. Register the key before treating this function as a reliable upsert.
The setter does not delete rows, does not treat blank as “delete”, and does not save any other record.
SetEntityProperty
Sets a top-level metadata field and returns a rebuilt record. Use it for fields such as DisplayName, Description or BusinessDomain. Use SetVocabularyKeyValue for vocabulary values.
SetEntityProperty(record, propertyText, value)
| Parameter | Type | Description |
|---|---|---|
record | Record | The CluedIn record - normally Entity |
propertyText | Text | The exact, case-sensitive runtime field name from the record schema |
value | Text, Number, Boolean, DateTime or GUID | The new value; the overload is chosen by its type |
Returns a rebuilt record on success; the original record if the field is read-only (with a warning in the log, but no error); a NotFound error of severity Severe if the field does not exist. A record that cannot be converted throws.
SetEntityProperty(Entity, "DisplayName", "Ben")
SetEntityProperty(Entity, "BusinessDomain", "/Person")
SetEntityProperty(Entity, "DocumentSize", 1024)
Match the field’s type. Five overloads do not mean every field accepts five types. Date fields are text at runtime, so set them with formatted text; a value of the wrong type can become blank or be ignored when the record is written back.
SetEntityProperty(Entity, "ModifiedDate", Text(DateValue("2026-10-22"), "yyyy-mm-dd"))
Read-only fields
A write to any of these fields is blocked: the function logs a warning and returns the original record. A successful evaluation is therefore not proof that the field changed.
| Blocked runtime field | Native property |
|---|---|
ProviderDefinitionId | ProviderDefinitionId |
Properties | Properties |
OutgoingEdges | OutgoingEdges |
Identifiers | Codes |
PrimaryIdentifier | OriginEntityCode |
DiscoveryDate | DiscoveryDate |
IsShadowEntity | IsShadowEntity |
IsExternalData | IsExternalData |
IsDeleted | IsDeleted |
The list is not a complete persistence policy. Edges are never written back, and VocabulariesUsed is skipped, so a field missing from this table is not guaranteed to be writable.
Table fields cannot be set. SetEntityProperty has no table overload, so it cannot replace Aliases, Tags, Identifiers, Properties or the edge tables. Use AddTag for tags, the vocabulary setter for properties, and the Add Alias and Remove Alias rule actions for aliases.
Carry the returned record forward
Because SetEntityProperty returns a new record, the next function must receive that record and the formula must end with it.
AddTag(
SetEntityProperty(Entity, "DisplayName", "Reviewed customer"),
"Reviewed")
Do not write:
SetEntityProperty(Entity, "DisplayName", "Reviewed customer");
Entity
The formula finishes with the original Entity, so the change to DisplayName is discarded.
AddTag
Adds one tag to the record, skipping it if an identical tag already exists.
AddTag(record, tagText)
| Parameter | Type | Description |
|---|---|---|
record | Record | A CluedIn record with a Tags field |
tagText | Text | The tag to add |
Returns the record with the updated Tags table.
AddTag(Entity, "Reviewed")
If(GetVocabularyKeyValue(Entity, "customer.status") = "Active",
AddTag(Entity, "Active customer"),
Entity)
Comparison. Duplicate detection is exact and case-sensitive, with no trimming: Reviewed and reviewed are different tags. Empty text is not rejected. Normalise the input with Trim or Proper if your tag taxonomy needs it.
Failure behaviour. Throws if the record has no Tags field or cannot be converted. There is no RemoveTag function. On Spark, a missing Tags field is silently ignored instead.
FuzzyMatch
Returns a similarity score between two strings, from 0 (no similarity) to 1 (identical), based on the Sift4 edit distance. It compares two values; it does not find or merge records.
FuzzyMatch(leftText, rightText)
| Parameter | Type | Description |
|---|---|---|
leftText | Text | The first string |
rightText | Text | The second string |
Returns a Number. Identical strings return 1. Otherwise the score is Max(0, 1 - distance / Max(Len(leftText), Len(rightText))), where distance is the Sift4 distance with a maximum offset of 2.
Effect local calculation only.
FuzzyMatch(
Lower(Trim(GetVocabularyKeyValue(Entity, "customer.legalName"))),
Lower(Trim(GetVocabularyKeyValue(Entity, "customer.tradingName")))
) >= 0.85
The 0.85 threshold is illustrative; tune it against records you know should and should not match.
Edge cases. The function does no case or whitespace normalisation - do it yourself, as above. Two empty strings return 1; one empty string returns 0. Not available on Spark.
LoadEntityByEntityCode
Loads a golden record from the primary data store by its entity code.
LoadEntityByEntityCode(entityCodeText)
| Parameter | Type | Description |
|---|---|---|
entityCodeText | Text | A full serialized entity code: /<BusinessDomain>#<Origin>:<Value> |
Returns the full golden record, with every field in the record schema. If no entity has that code, returns a NotFound error of severity Moderate.
Effect one read from the primary data store per call. The loaded record is registered with the runtime, so it can be passed to GetVocabularyKeyValue and GetVersionBranches.
LoadEntityByEntityCode("/Person#Somewhere:customer-123").DisplayName
With(
{customer: LoadEntityByEntityCode("/Person#Somewhere:customer-123")},
GetVocabularyKeyValue(customer, "customer.status") = "Active"
)
A code is built from the business domain, origin and value. A business identifier on its own, or a database GUID, is not an entity code. Codes can be assembled from vocabulary values:
SetVocabularyKeyValue(Entity, "product.exchangeRate",
GetVocabularyKeyValue(
LoadEntityByEntityCode("/ExchangeRate#CurrencyCode:" &
GetVocabularyKeyValue(Entity, "product.localCurrencyCode")),
"currency.exchangeRate"))
See Identifiers for how entity codes are formed.
Missing entities. To treat a missing entity as a non-match rather than an error:
IfError(
GetVocabularyKeyValue(
LoadEntityByEntityCode("/Person#Somewhere:customer-123"),
"customer.status") = "Active",
false)
Failure behaviour. A malformed code and data store failures are thrown, not returned as NotFound, and IfError does not catch them.
No cross-record saves. Setters applied to a loaded record change only the value in the formula. The action writes its final record back to the current record, never to the loaded one. Read from loaded records; write to Entity.
LoadIncomingRelationships
Loads the relationships that point to the record. The record is the destination; each returned EntityCode is the source.
LoadIncomingRelationships(record)
| Parameter | Type | Description |
|---|---|---|
record | Record | A record backed by a real CluedIn entity - normally Entity in Entity scope |
Returns a table of EdgeType (text) and EntityCode (text, the source entity’s code). Returns an empty table when there are no relationships, or when the record is not backed by a real entity.
Scope Entity only.
CountRows(LoadIncomingRelationships(Entity)) > 0
CountRows(Filter(LoadIncomingRelationships(Entity), EdgeType = "/WorksFor")) > 0
The EdgeType value must match the serialized edge type stored in your data. A table on its own is not a valid condition; reduce it to a Boolean with CountRows, IsEmpty or similar.
Cost and timeout. All rows are loaded before the table is returned; there is no paging, row limit or edge-type filter on the query. Loading is limited by Rules.PowerFx.LoadRelationsTimeoutInMilliseconds (default 10000 ms). When the limit is detected the function returns a Timeout error of severity Moderate. The limit is checked while edges are read, so it is not a hard guarantee against a data store call that hangs. Other data store failures are thrown.
LoadOutgoingRelationships
Loads the relationships that point from the record. The record is the source; each returned EntityCode is the destination.
LoadOutgoingRelationships(record)
Parameters, return shape, scope, timeout and failure behaviour are the same as LoadIncomingRelationships, with EntityCode taken from the destination.
With(
{relationships: LoadOutgoingRelationships(Entity)},
CountRows(Filter(relationships, EdgeType = "/WorksFor")) > 0 &&
CountRows(relationships) < 50
)
With loads the table once for several checks within the formula. It does not cache between records or rule runs.
To read a related record, pass the code on:
With(
{employer: First(Filter(LoadOutgoingRelationships(Entity), EdgeType = "/WorksFor"))},
!IsBlank(employer) &&
GetVocabularyKeyValue(LoadEntityByEntityCode(employer.EntityCode), "organization.country") = "DK"
)
Each LoadEntityByEntityCode adds another data store read. The returned rows are neither ordered nor deduplicated. See Golden record relations.
GetVersionBranches
Returns the version history branches already held for a golden record, with each branch’s versions as a nested table. It does not query the data store for history.
GetVersionBranches(record)
| Parameter | Type | Description |
|---|---|---|
record | Record | A golden record: Entity in Entity or survivorship scope, GoldenRecord in a survivorship action condition, or a record from LoadEntityByEntityCode |
Scopes Entity, Survivorship and Survivorship action condition.
Returns a table of branches:
| Branch field | Type | Meaning |
|---|---|---|
Name | Text | The branch name |
Parts | Table | The branch’s versions, newest first |
Each row of Parts:
| Part field | Type | Meaning |
|---|---|---|
Date | Text | Version date in round-trip (O) format |
Branch | Text | Branch name, or empty text |
ChangeType | Text | Serialized change type |
IsHead | Boolean | Whether this is the head version of the branch |
InputSource | Text | The data part’s input source, or empty text |
EntityData | Record | The original data part, in the record schema |
ProcessedEntityData | Record | The processed data part, in the record schema |
EntityData and ProcessedEntityData can be passed to GetVocabularyKeyValue. They are data parts, so passing them to GetVersionBranches returns an empty table.
Empty results. The table is empty when the record is not a golden record, is not backed by a real entity, or has no branches. An empty table does not on its own mean “no history”. Branches are not sorted; only Parts is ordered.
First(First(GetVersionBranches(Entity)).Parts).EntityData.Name
In real rules, pick a branch by name and guard against empty tables:
With(
{crm: LookUp(GetVersionBranches(Entity), Name = "crm")},
!IsBlank(crm) && !IsEmpty(crm.Parts) &&
GetVocabularyKeyValue(First(crm.Parts).EntityData, "customer.status") = "Active"
)
The branch name crm is illustrative. See History for how branches relate to sources.
Choose which versions to compare
A branch row is not a data part, so GetVocabularyKeyValue(ThisRecord, ...) over the branch table reads nothing. Decide which versions a rule means, and reach into Parts accordingly. These are three different comparisons and can give different answers.
| Comparison | Versions considered | Expression over each branch |
|---|---|---|
| Latest per branch | The newest version in every branch | First(Parts) |
| Branch heads | Versions marked as the head of their branch | Filter(Parts, IsHead) |
| All versions | Every historical version in every branch | Parts |
// Latest per branch: the highest revenue among each branch's newest version
Max(GetVersionBranches(Entity),
Value(GetVocabularyKeyValue(First(Parts).EntityData, "company.revenue")))
// Branch heads
Max(GetVersionBranches(Entity),
Max(Filter(Parts, IsHead), Value(GetVocabularyKeyValue(EntityData, "company.revenue"))))
// All versions
Max(GetVersionBranches(Entity),
Max(Parts, Value(GetVocabularyKeyValue(EntityData, "company.revenue"))))
Inside the inner Max or Filter, field names such as EntityData and IsHead refer to the part row. Use ProcessedEntityData instead of EntityData to compare the values after processing rather than as they arrived. Max ignores blank values, and Value fails on non-numeric text.
Patterns
Compare the latest value across branches
Does the newest version in every branch agree on a value?
With(
{branches: GetVersionBranches(Entity)},
CountRows(branches) > 1 &&
CountRows(
Distinct(
ForAll(branches,
GetVocabularyKeyValue(First(Parts).EntityData, "customer.status")),
Value
)
) = 1
)
Use ProcessedEntityData instead of EntityData to compare processed versions. Blank values also collapse into one distinct value, so add a non-blank check if agreement must be on a populated value.
Apply several changes in one action
AddTag(
SetVocabularyKeyValue(
SetEntityProperty(Entity, "DisplayName", "Reviewed customer"),
"customer.reviewStatus", "Complete"),
"Reviewed")
Read it inside out: each call receives the previous call’s record, and the outermost call returns the record that is applied. The changes are not one transaction - validate the final record.
Update the current record based on a related one
With(
{related: LoadEntityByEntityCode("/Person#Somewhere:customer-123")},
If(GetVocabularyKeyValue(related, "customer.status") = "Active",
AddTag(Entity, "Related active customer"),
Entity)
)
The loaded record is only read; the action returns Entity in both branches. Wrap the lookup in IfError if the related entity may not exist.
Normalise a value only when it is present
If(IsBlank(GetVocabularyKeyValue(Entity, "customer.email")),
Entity,
SetVocabularyKeyValue(Entity, "customer.email",
Lower(Trim(GetVocabularyKeyValue(Entity, "customer.email")))))
Flag a pattern with a regular expression
If(IsMatch(GetVocabularyKeyValue(Entity, "customer.email"), "^[^@\s]+@[^@\s]+\.[^@\s]+$"),
Entity,
AddTag(Entity, "Invalid email"))
Regular expressions are limited by Rules.PowerFx.RegExTimeoutInMilliseconds.
Common mistakes
| Wrong | Why | Correct |
|---|---|---|
Entity.Properties.customer.status | Properties is a table, not a nested record | GetVocabularyKeyValue(Entity, "customer.status") |
Entity.EntityType, Entity.OriginEntityCode, Entity.Codes | Older names | Entity.BusinessDomain, Entity.PrimaryIdentifier, Entity.Identifiers |
SetEntityProperty(Entity, "DisplayName", "X"); Entity | Ends with the unchanged record | SetEntityProperty(Entity, "DisplayName", "X") |
SetEntityProperty(Entity, "DisplayName", "X"); AddTag(Entity, "t") | AddTag starts from the original record, so the new display name is lost | AddTag(SetEntityProperty(Entity, "DisplayName", "X"), "t") |
If(cond, AddTag(Entity, "t")) | The false branch returns blank, not a record | If(cond, AddTag(Entity, "t"), Entity) |
If(x > 5, true, false) | Redundant | x > 5 |
GetVocabularyKeyValue(Entity, "order.total") > 100 | Compares text with a number | Value(GetVocabularyKeyValue(Entity, "order.total")) > 100 |
SetVocabularyKeyValue(Entity, "order.total", 100) | The value parameter is text | SetVocabularyKeyValue(Entity, "order.total", Text(100)) |
SetEntityProperty(Entity, "displayname", "X") | Field names are case-sensitive | SetEntityProperty(Entity, "DisplayName", "X") |
SetEntityProperty(Entity, "IsDeleted", true) | Read-only; silently ignored | Not settable from a formula |
SetEntityProperty(Entity, "Aliases", ...) | Aliases is a table; there is no table overload | The Add Alias rule action |
GetVocabularyKeyValue(ThisRecord, "k") over GetVersionBranches(Entity) | A branch row has only Name and Parts | GetVocabularyKeyValue(First(Parts).EntityData, "k") |
GetVocabularyKeyValue(First(LoadOutgoingRelationships(Entity)), "k") | A relationship row has only EdgeType and EntityCode | GetVocabularyKeyValue(LoadEntityByEntityCode(First(LoadOutgoingRelationships(Entity)).EntityCode), "k") |
GetVocabularyKeyValue(Entity, "customer.status") in a survivorship action condition | Entity does not exist in that scope | GetVocabularyKeyValue(DataPart, "customer.status") |
LoadEntityByEntityCode("5452407DH") | Not a full entity code | LoadEntityByEntityCode("/Customer#Erp:5452407DH") |
SetVocabularyKeyValue(LoadEntityByEntityCode(code), "k", "v") | The loaded record is never saved | Write to Entity; update the other record with its own rule |
LoadIncomingRelationships(Entity) in a data part rule | Entity scope only | Use it in a golden record rule |
Errors and operational behaviour
The engine distinguishes three outcomes. Diagnose them separately in the rule’s execution log.
- A Power Fx error value at the top level - from a failed conversion,
NotFoundorTimeout- is raised as a Power Fx error. - Any other exception during evaluation is raised as an evaluation exception.
- A successful non-Boolean condition result is logged and treated as
false.
| Situation | Behaviour |
|---|---|
| Vocabulary key not defined, read | GetVocabularyKeyValue returns blank; VocabularyKeyExists returns false |
| Vocabulary key has no row | Blank / false |
Properties is not a CluedIn properties table | Readers return blank / false; the setter returns the record unchanged |
| Duplicate rows for a key | GetVocabularyKeyValue throws; VocabularyKeyExists returns true |
SetEntityProperty on a field that does not exist | NotFound error, severity Severe |
SetEntityProperty on a read-only field | Warning logged; original record returned |
LoadEntityByEntityCode finds nothing | NotFound error, severity Moderate |
| Relationship loading exceeds its timeout | Timeout error, severity Moderate |
| History or relationships on a record without runtime identity | Empty table |
| A value cannot be converted when written back | Logged and skipped; the field keeps its old value |
Write-back is not a transaction
After evaluation, the final record is mapped onto the current record field by field. Conversion failures are logged and skipped; there is no all-or-nothing validation or rollback. A formula that evaluates successfully can still leave a field unchanged - check the result, especially for dates, numbers, URIs and Booleans.
Keep data store work visible
LoadEntityByEntityCode and the relationship loaders read the data store inside formula evaluation, for every record the rule processes. The vocabulary functions also resolve definitions on every call. Bind repeated results with With, and keep lookups to what the business rule needs. There is no single global evaluation deadline; regular expressions, relationship loading and the editor’s evaluation endpoint each have their own limit.
Configuration
| Setting | Default | Effect |
|---|---|---|
Rules.PowerFx.RegExTimeoutInMilliseconds | 1000 ms | Timeout for regular expression functions |
Rules.PowerFx.MaxExpressionLength | 10000 characters | Maximum formula length; 0 disables the limit. Applies to formulas parsed without explicit parser options, such as conditions |
Rules.PowerFx.LoadRelationsTimeoutInMilliseconds | 10000 ms | Timeout checked by the relationship loaders |
Formula editor endpoints
The Rule Builder’s formula editor uses these endpoints. Each takes a scope query parameter, which defaults to Entity.
| Method and route | Purpose |
|---|---|
GET /api/powerfx/context | Returns a sample context for the scope |
POST /api/powerfx/lsp | Language server requests (IntelliSense, diagnostics); the body is forwarded as-is |
POST /api/powerfx/eval | Evaluates an expression against a supplied context |
The context field of an evaluation request is a JSON string, not a nested object:
{
"context": "{\"Entity\":{\"DisplayName\":\"Test\"}}",
"expression": "Right(Entity.DisplayName, 1) = \"t\""
}
The endpoint parses, checks and evaluates with a two-second limit, and returns the result, tokens and parse information. Errors are returned as content with HTTP 200. The endpoint does not set up the rule runtime or registered records, so the vocabulary, lookup, relationship and history functions cannot be validated with it. Test those through a real rule run.
Spark execution has a smaller function set
Rules executed on Spark use a separate compatibility checker and row evaluator. A function available in Entity scope is not automatically available on Spark.
| Function | Spark actions | Spark conditions |
|---|---|---|
GetVocabularyKeyValue | Yes | Yes |
SetVocabularyKeyValue | Yes | No |
SetEntityProperty (all overloads) | Yes | No |
AddTag | Yes | No |
VocabularyKeyExists | No | No |
FuzzyMatch | No | No |
LoadEntityByEntityCode | No | No |
LoadIncomingRelationships | No | No |
LoadOutgoingRelationships | No | No |
GetVersionBranches | No | No |
Additional Spark requirements:
- The key or property name passed to
GetVocabularyKeyValue,SetVocabularyKeyValueandSetEntityPropertymust be a string literal."customer." & "status"is rejected, because mappings and columns are resolved before the rows are distributed. - A condition must compile to a Boolean.
- The checker binds against a record containing only
PropertiesandTags; the row evaluator also carriesNameandEntityType. Other fields are not available.
Behaviour also differs from the standard engine:
| Situation | Standard engine | Spark |
|---|---|---|
| Vocabulary key with no mapping | GetVocabularyKeyValue returns blank | Falls back to the raw key |
SetEntityProperty on a missing field | NotFound error | Returns the original record |
AddTag on a record without Tags | Throws | No-op |
Extending the function library
Custom functions are .NET classes derived from ExpressionRuleEngineReflectionFunctionBase. At startup, CluedIn scans its assemblies for non-abstract subclasses and registers them; when a formula engine is created for a scope, every function whose IsCompatibleWithScope accepts that scope is added to it.
| Responsibility | What to implement |
|---|---|
| Name and signature | Pass the formula name, return type and parameter types to the base constructor |
| Invocation | An Execute method, as expected by Power Fx ReflectionFunction |
| Runtime services | The base class sets ConfigType to ExpressionRuleEngineRuntimeConfig, which gives access to the execution context. Set it to null if the function needs none, as FuzzyMatch does |
| Scope | IsCompatibleWithScope for the rule scopes the function supports |
| Data | Return records and tables with stable field names, and register runtime metadata if other functions must recognise the records |
| Errors | Return explicit Power Fx errors, or document exceptions and empty results |
A separate CluedIn.PowerFx.CustomFunctions package contains a second implementation of LoadEntityByEntityCode that returns an empty record, not NotFound, for a missing entity. It is the same formula name, not an additional function. Do not install both without checking which is registered; the behaviour documented on this page is the core engine’s.
Corrections to older descriptions
Earlier material - including the beta-era overview of Power Fx expression rules, and content that AI assistants may have learned from - describes Power Fx in CluedIn differently. Where they conflict, this page is current.
| Older description | Current behaviour |
|---|---|
| Four custom functions | Ten function names, including fuzzy matching, entity lookup, relationship loading, key existence and version branches |
| Power Fx SDK 1.2 | Power Fx 1.8.1 |
A single Entity context | Scope-specific; GoldenRecord and DataPart in survivorship action conditions |
EntityType, OriginEntityCode, Codes | BusinessDomain, PrimaryIdentifier, Identifiers |
| Formulas cannot read other records | LoadEntityByEntityCode and the relationship loaders read the data store |
| A missing value is empty or null | GetVocabularyKeyValue returns Blank(); VocabularyKeyExists tests presence |
| Planned functions for HTTP, queries, entity creation and so on | Not implemented; the ten functions on this page are the complete CluedIn set |
Related articles
- Power Fx formulas - introduction and examples
- Rule types - which rules use which scope
- Creating a rule - where formulas go in the Rule Builder, and how to read the execution log
- Vocabulary keys - the keys formulas read and write
- Identifiers - how entity codes are formed
- History - the branches
GetVersionBranchesreturns