Skip to main content

Auto-Populate Attribute Value Pattern

Overview

This pattern automatically sets attribute values on an instance based on business logic — typically by reading values from a related (reference) instance and copying or transforming them onto the current instance.

It is used when the computation is too complex for a declarative expression attribute, or when values must be fetched from related instances.

When to Use

  • Copy fields from a referenced parent instance to the child on create (e.g., copy client region and category from a Client record to a new Project).
  • Derive a computed value from a related instance's attributes (e.g., calculate days between dates).
  • Conditionally set picklist or boolean values based on related data.
  • Set default attribute values that depend on dynamic data not available at schema configuration time.

Applicable Program Types

Program TypeInterfaceExecution
PreCreate TriggerIPreCreateTriggerProgramAsyncSynchronous — mutate instance before save
PostCreate TriggerIPostCreateTriggerProgramAsyncAsync — call UpdateInstanceAsync after creation
PreUpdate TriggerIPreUpdateTriggerProgramAsyncSynchronous — mutate instance before save
PostUpdate TriggerIPostUpdateTriggerProgramAsyncAsync — call UpdateInstanceAsync after update
PostConnect TriggerIPostConnectTriggerProgramAsyncAsync — copy source → destination on link

Pattern Structure

In a Pre-trigger (mutate the incoming instance)

Use the typed setter methods (SetTextAttributeValue, SetPicklistAttributeValue, etc.) — each creates or updates the attribute in one call.

In a Post-trigger (call UpdateInstanceAsync)

Build a new Instance object with only the attributes to update and call InstanceService.UpdateInstanceAsync.

Fetch both the source and destination instances. Read from source using GetXxxAttributeValue, write to destination using the typed setters, then call UpdateInstanceAsync on the destination.


Code Example — PreCreate: Copy from Referenced Instance

Copies client code and region from the linked Client record onto the Project being created.

using Prorigo.Protrak.API.Contracts;
using Prorigo.Protrak.API.Contracts.Builders;
using Prorigo.Protrak.API.Contracts.Enum;
using Prorigo.Protrak.API.Services;
using Prorigo.Protrak.Programs;
using System.Linq;
using System.Threading.Tasks;

namespace YourNamespace.Programs
{
/// <summary>
/// Type: Pre Create Trigger
/// Configured for: Type Project
/// Trigger: Before project instance creation
/// Summary: Copies ClientCode and Region from the linked Client onto the Project.
/// </summary>
public class AutoPopulateProjectFieldsFromClient : IPreCreateTriggerProgramAsync
{
public IInstanceService InstanceService { get; set; }

private readonly string ATTRIBUTE_CLIENT_REF = "ProjectToClientRef";
private readonly string ATTRIBUTE_CLIENT_CODE = "ClientCode";
private readonly string ATTRIBUTE_REGION = "Region";
private readonly string ATTRIBUTE_CATEGORY = "Category";
private readonly string ATTRIBUTE_PROJECT_CLIENT_CODE = "ProjectClientCode";
private readonly string ATTRIBUTE_PROJECT_REGION = "ProjectRegion";

public async Task<ProgramResult> RunAsync(Instance instance)
{
var clientRef = instance.GetReferenceAttributeValue(ATTRIBUTE_CLIENT_REF);
if (clientRef == null || clientRef.Length == 0)
return new ProgramResult { IsSuccess = true };

var client = await InstanceService.GetInstanceAsync(
clientRef[0].Id,
new[] { ATTRIBUTE_CLIENT_CODE, ATTRIBUTE_REGION, ATTRIBUTE_CATEGORY });

// Read source values
var clientCode = client.GetTextAttributeValue(ATTRIBUTE_CLIENT_CODE);
var region = client.GetPicklistAttributeValue(ATTRIBUTE_REGION)?.FirstOrDefault();

// Write to target instance — typed setters create or update without manual find-or-add
instance.SetTextAttributeValue(ATTRIBUTE_PROJECT_CLIENT_CODE, clientCode);
instance.SetPicklistAttributeValue(ATTRIBUTE_PROJECT_REGION, region);

return new ProgramResult { IsSuccess = true };
}
}
}

After a child record is created, reads a category value from the linked parent and conditionally sets flags on the child.

/// <summary>
/// Type: Post Create Trigger
/// Configured for: Type Checklist
/// Trigger: After checklist instance is created
/// Summary: Sets AuditRequired and IsAbridged flags based on the linked Project's category.
/// </summary>
public class AutoPopulateChecklistFlags : IPostCreateTriggerProgramAsync
{
public IInstanceService InstanceService { get; set; }

private readonly string TYPE_CHECKLIST = "Checklist";
private readonly string ATTRIBUTE_PROJECT_REF = "ProjectToChecklistRef";
private readonly string ATTRIBUTE_CATEGORY = "ProjectCategory";
private readonly string ATTRIBUTE_AUDIT_REQUIRED = "AuditRequired";
private readonly string ATTRIBUTE_IS_ABRIDGED = "IsAbridgedChecklist";
private readonly string PICKLIST_CATEGORY_C = "Category C";

public async Task RunAsync(Guid instanceId)
{
var checklist = await InstanceService.GetInstanceAsync(instanceId, new[] { ATTRIBUTE_PROJECT_REF });
var projectRef = checklist.GetReferenceAttributeValue(ATTRIBUTE_PROJECT_REF);
if (projectRef == null || projectRef.Length == 0) return;

var project = await InstanceService.GetInstanceAsync(projectRef[0].Id, new[] { ATTRIBUTE_CATEGORY });
var category = project.GetPicklistAttributeValue(ATTRIBUTE_CATEGORY)?.FirstOrDefault();
var isCategoryC = category == PICKLIST_CATEGORY_C;

var updated = InstanceBuilder.ForUpdate(instanceId, TYPE_CHECKLIST)
.SetPicklistAttributeValue(ATTRIBUTE_AUDIT_REQUIRED, isCategoryC ? "NA" : "Yes")
.SetBooleanAttributeValue(ATTRIBUTE_IS_ABRIDGED, isCategoryC)
.Build();

await InstanceService.UpdateInstanceAsync(updated, null);
}
}

Code Example — PostConnect: Copy Fields When Two Instances Are Linked

Copies contact details from a source instance (e.g., an Inquiry) to the destination instance (e.g., a Contract) when they are connected.

/// <summary>
/// Type: Post Connect Trigger
/// Configured for: Relation InquiryToContract
/// Trigger: After an Inquiry is linked to a Contract
/// Summary: Copies company name, contact person, email, and phone from Inquiry to Contract.
/// </summary>
public class CopyInquiryDetailsToContractOnConnect : IPostConnectTriggerProgramAsync
{
public IInstanceService InstanceService { get; set; }
public IRelationService RelationService { get; set; }

public async Task RunAsync(Guid relationId)
{
var relation = await RelationService.GetRelationAsync(relationId);

var inquiry = await InstanceService.GetInstanceAsync(relation.SourceInstanceId,
new[] { "InquiryCompanyName", "InquiryContactName", "InquiryEmail", "InquiryPhone" });

var contract = await InstanceService.GetInstanceAsync(relation.DestinationInstanceId,
new[] { "CompanyName", "AuthorizedContactName", "AuthorizedEmail", "PhoneNumber" });

// Copy using typed setters — creates or updates without manual find-or-add
contract.SetTextAttributeValue("CompanyName", inquiry.GetTextAttributeValue("InquiryCompanyName"));
contract.SetTextAttributeValue("AuthorizedContactName", inquiry.GetTextAttributeValue("InquiryContactName"));
contract.SetTextAttributeValue("AuthorizedEmail", inquiry.GetTextAttributeValue("InquiryEmail"));
contract.SetTextAttributeValue("PhoneNumber", inquiry.GetTextAttributeValue("InquiryPhone"));

await InstanceService.UpdateInstanceAsync(contract, contract.Modified);
}
}

Code Example — PreUpdate: Computed Date Difference

When a date attribute changes, computes the number of days elapsed from a reference (baseline) date and sets a derived numeric attribute.

/// <summary>
/// Type: Pre Update Trigger
/// Configured for: Type WorkOrder
/// Trigger: Before update
/// Summary: When MilestoneDate changes, computes and stores the number of days from BaselineDate.
/// </summary>
public class ComputeElapsedDaysOnDateChange : IPreUpdateTriggerProgramAsync
{
public IInstanceService InstanceService { get; set; }

private readonly string ATTRIBUTE_BASELINE_DATE = "BaselineDate";
private readonly string ATTRIBUTE_MILESTONE_DATE = "MilestoneDate";
private readonly string ATTRIBUTE_DAYS_ELAPSED = "DaysFromBaseline";

public async Task<ProgramResult> RunAsync(Instance instance)
{
// Fetch persisted values to detect what changed
var stored = await InstanceService.GetInstanceAsync(instance.Id,
new[] { ATTRIBUTE_BASELINE_DATE, ATTRIBUTE_MILESTONE_DATE });

var baselineDate = stored.GetDateAttributeValue(ATTRIBUTE_BASELINE_DATE);
var oldMilestone = stored.GetDateAttributeValue(ATTRIBUTE_MILESTONE_DATE);
var newMilestone = instance.GetDateAttributeValue(ATTRIBUTE_MILESTONE_DATE);

// Only compute if the milestone changed and baseline is set
if (newMilestone != null && newMilestone != oldMilestone && baselineDate != null)
{
var days = (newMilestone.Value - baselineDate.Value).Days;
instance.SetNumericAttributeValue(ATTRIBUTE_DAYS_ELAPSED, (double?)days);
}

return new ProgramResult { IsSuccess = true };
}
}

Attribute Type Cheat Sheet

Attribute TypeReadTyped Setter
Textinstance.GetTextAttributeValue("X")instance.SetTextAttributeValue("X", "value")
Numeric / Currencyinstance.GetNumericAttributeValue("X")instance.SetNumericAttributeValue("X", 42.0)
Booleaninstance.GetBooleanAttributeValue("X")instance.SetBooleanAttributeValue("X", true)
Date / DateTimeinstance.GetDateAttributeValue("X")instance.SetDateAttributeValue("X", DateTime.UtcNow)
Picklistinstance.GetPicklistAttributeValue("X")instance.SetPicklistAttributeValue("X", "Option")
Userinstance.GetUserAttributeValue("X")instance.SetUserAttributeValue("X", new UserValue[] { ... })
Referenceinstance.GetReferenceAttributeValue("X")instance.SetReferenceAttributeValue("X", new ReferenceValue[] { ... })

See Instance Accessor Methods for the full reference.

Key Services

ServiceMethodPurpose
IInstanceServiceGetInstanceAsyncFetch related or persisted instance to read source values
IInstanceServiceUpdateInstanceAsyncWrite computed values back (in Post-triggers)
IRelationServiceGetRelationAsyncGet source/destination IDs in PostConnect