Relationship Navigation in Expressions (@Relation)
Expression-based validation rules (on lifecycle promote actions) can navigate across relationships to read values from related instances and from the relationship links themselves.
Relationship navigation is supported in lifecycle validation rules and in sequence-code
prefix/suffix formulas. It is intentionally not available in expression attributes, workflow
rules, or authorization rules — the platform rejects it at save time in those places with a clear
message. Sequence codes carry one extra rule: a navigation-decorated code cannot be bound to a
Type's Instance-Name or Tracking-Id generator (those run at create time, before links exist). See
Using @Relation in sequence codes below.
What you can read
Starting from @Relation.<RelationshipName>, you can:
| Expression | Result |
|---|---|
@Relation.Assemblies | Exists — true if the instance has at least one Assemblies link |
@Relation.Assemblies.Count | Count — the number of Assemblies links |
@Relation.Assemblies.First.Status | The value of the custom attribute Status on the first related instance |
@Relation.Assemblies.First.State | A basic property (here the lifecycle state) of the first related instance |
@Relation.Assemblies.Link.Quantity | The value of the edge attribute Quantity on the first relationship link |
First.<attribute> reads any attribute the related instance has — a custom attribute or a
basic property such as Name, State, Created, Modified, Creator, TrackingId, TypeName.
Link.<attribute> reads an attribute defined on the relationship itself.
Multi-hop navigation (up to 3 hops)
You can chain hops to walk a hierarchy. Each .First.<AnotherRelationship> steps onto the next
related instance and continues from there:
@Relation.BuildingToElement.First.ProjectToBuilding.First.ProjectName
Read left-to-right: from an Element, follow BuildingToElement to its Building, then
ProjectToBuilding to that Building's Project, then read the Project's ProjectName. Chains are
limited to 3 hops.
Null-safety and the guard idiom
If a relationship is not linked (or a hop along a chain can't be resolved), the navigation
resolves to nothing (null), and a Count resolves to 0. Comparing null with == is
safe, but comparing it with >, <, >=, <= will fail the rule with an error. Always guard
a navigation before a numeric or date comparison:
@Relation.Assemblies && @Relation.Assemblies.First.Hours > 40
For a deeper chain, guard each level — use .Count > 0 for existence beyond the first hop:
@Relation.A && @Relation.A.First.B.Count > 0 && @Relation.A.First.B.First.Budget > 100000
The leading @Relation.<Relationship> check short-circuits with &&, so the comparison only runs
when the value actually exists.
When saving a rule, the platform warns (in the logs) if you compare a navigation path without a guard. Adopt the guard idiom above and the rule is always safe.
Which related instance is "First"?
First is the related instance on the link with the lowest OrderIndex — nothing else. If
several links share the lowest OrderIndex (or none is set), which one is chosen is
unspecified and may differ between evaluations.
If a rule depends on which related instance First picks, make sure the links have a
well-defined OrderIndex. Without it, First is only reliable when there is a single link.
Names that look alike
Relationship names and attribute names are read based on where they appear in the path, so there is never any ambiguity — even if a tenant has an attribute and a relationship with the same name:
@Relation.A.First.X—Xat the end reads the attributeX.@Relation.A.First.X.Count—Xfollowed by an accessor reads the relationshipX.
Using @Relation in sequence codes
A sequence-code prefix/suffix may use @Relation navigation, and the platform validates the
path at save time exactly as it does for a validation rule. What you cannot do is bind such a code
to a Type's Instance-Name or Tracking-Id generator:
@Relation.WorkOrderToProject.First.ProjectCode + "-WO"
That code is valid as a sequence code, but if you try to select it as the Name or Tracking-Id generator on a Type, the save is rejected:
Sequence code 'WorkOrderProjectSeq' uses @Relation navigation in its prefix/suffix and cannot be used to generate the Instance Name. …
The reason is timing. Name and Tracking-Id are generated the moment an instance is created,
but relationship links are established after create (connect is a separate step in the standard
flow). At create time a @Relation path has no links to read, so it could only ever resolve to
nothing — a leaky, surprising result baked into the instance's identity. Every other use of the
sequence code is fine, because by then you drive it yourself, once the links exist.
Interim pattern — stamp the decorated value after connect
Until the first-class post-connect "generate code" command lands (see Coming soon), use a post-connect program trigger to write the relationship-decorated value onto the instance once its links exist. Two shapes work; pick per your continuity needs.
For both, give the Type a plain (no-@Relation) sequence code for the create-time Name/Tracking
so instances are still born with a valid identifier, then overwrite/augment it after connect.
Option A — compose the value yourself (running-number continuity not required)
You don't even need a @Relation sequence code here: read the related instance's value directly and
compose the final identifier. The provisional create-time number is simply discarded, so use this
when the decorated identifier doesn't need a gap-free running number.
public class WorkOrderPostConnectTrigger : IPostConnectTriggerProgramAsync
{
public IInstanceService InstanceService { get; set; }
public async Task RunAsync(Relation relation)
{
// Work Order is the destination of the Project -> WorkOrder link.
var workOrderId = relation.DestinationInstanceId;
var projectId = relation.SourceInstanceId;
var project = await InstanceService.GetInstanceAsync(projectId, new[] { "ProjectCode" });
var projectCode = project.GetTextAttributeValue("ProjectCode");
var workOrder = await InstanceService.GetInstanceAsync(workOrderId, new[] { "Name" });
workOrder.Name = $"{projectCode}-{workOrder.Name}";
await InstanceService.UpdateInstanceAsync(workOrder, workOrder.Modified);
}
}
Option B — draw a dedicated relationship-decorated sequence code
When you want the decorated identifier to have its own running number, author the @Relation
sequence code (e.g. WorkOrderProjectSeq with the prefix shown above) and draw it from the trigger
with the instance-scoped overload of GetNextValue. The platform resolves the @Relation paths
against that instance — the links exist by the time a post-connect trigger runs — and advances the
code's own sequence:
public class WorkOrderPostConnectTrigger : IPostConnectTriggerProgramAsync
{
public IInstanceService InstanceService { get; set; }
public ISequenceCodeService SequenceCodeService { get; set; }
public async Task RunAsync(Relation relation)
{
var workOrderId = relation.DestinationInstanceId;
// @Relation.WorkOrderToProject.First.ProjectCode resolves against this Work Order,
// because its link to the Project already exists at post-connect time.
var decoratedName = SequenceCodeService.GetNextValue("WorkOrderProjectSeq", workOrderId);
var workOrder = await InstanceService.GetInstanceAsync(workOrderId, new[] { "Name" });
workOrder.Name = decoratedName;
await InstanceService.UpdateInstanceAsync(workOrder, workOrder.Modified);
}
}
The instance-scoped overload is:
// Prorigo.Protrak.API.Services.ISequenceCodeService
string GetNextValue(string sequenceCodeName, Guid instanceId);
It evaluates the code's @Relation navigation against instanceId. Note it resolves @Relation
navigation only — @Instance.<attribute> references in the same prefix/suffix are not hydrated by
this overload, so keep instance-attribute composition in your program code (Option A) and reserve the
sequence code for the relationship-decorated part.
relation.Direction decides which of SourceInstanceId / DestinationInstanceId is the "From" vs
"To" instance. Pick the id of the instance whose identity you're stamping.
Coming soon: first-class post-connect generation
The trigger pattern above is the interim mechanism. A first-class post-connect "generate code" command is planned (Feature 28473); once it ships, the hand-written trigger simply calls that command instead, with the same relationship-decorated sequence code you already authored.
Related
- Rules Configuration Guide — the attribute-filter builder and
@Instance/@Userdynamic values.