Skip to main content

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.

Where this applies

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:

ExpressionResult
@Relation.AssembliesExiststrue if the instance has at least one Assemblies link
@Relation.Assemblies.CountCount — the number of Assemblies links
@Relation.Assemblies.First.StatusThe value of the custom attribute Status on the first related instance
@Relation.Assemblies.First.StateA basic property (here the lifecycle state) of the first related instance
@Relation.Assemblies.Link.QuantityThe 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.

tip

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.

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.

Determinism

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.XX at the end reads the attribute X.
  • @Relation.A.First.X.CountX followed by an accessor reads the relationship X.

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.

Which side is the instance?

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.