@prorigo/protrak-forge 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,23 +7,80 @@ that implement Protrak interfaces (PreCreate, PostCreate, PreUpdate, PromoteActi
7
7
 
8
8
  ## How to write Protrak programs
9
9
 
10
- **ALWAYS use the Protrak Forge MCP tools when working with Protrak programs.**
10
+ Default to the Protrak Forge MCP tools, not the filesystem. The cost-of-skipping table at the end
11
+ of this section makes the trade-off concrete; the workflows below tell you which tools to call.
11
12
 
12
- ### Writing a new program:
13
- 1. Call `get_protrak_program_context` with the type name, program type, and description —
14
- it returns the type schema, interface contract, relevant patterns, and similar programs
15
- in ONE call.
16
- 2. Write the C# program following the interface and patterns returned.
17
- 3. Call `validate_protrak_program` to check for errors before committing.
13
+ ### Writing a NEW program:
14
+ 1. Call `get_protrak_program_context(type_name, program_type, description)` returns type
15
+ schema, C# interface contract, coding patterns, similar programs, and available query
16
+ definitions in ONE call.
17
+ 2. Write the C# source.
18
+ 3. Call `validate_protrak_program(code, program_type)` see "What this catches" below.
19
+ 4. Call `generate_protrak_program(program_name, program_type, source)` — writes BOTH
20
+ `Programs/<name>.cs` AND `Programs/<name>.json` (companion metadata required by Protrak's
21
+ import flow). **Do NOT use the Write tool here** — you will skip the .json companion and
22
+ the program will not import.
23
+
24
+ ```
25
+ get_protrak_program_context(type_name='Project', program_type='PreUpdate', description='block status change when invoice unpaid')
26
+ # ... agent writes the C# source ...
27
+ validate_protrak_program(code='<source>', program_type='PreUpdate')
28
+ generate_protrak_program(program_name='ProjectPreUpdate', program_type='PreUpdateTrigger', source='<source>')
29
+ ```
30
+
31
+ ### Editing an EXISTING program:
32
+ 1. Call `get_protrak_program_context(type_name, program_type, program_name='<ExistingName>')`.
33
+ Passing `program_name` switches the tool to refactor mode — response includes
34
+ `existing_source`, `existing_services_used`, `existing_patterns_detected`, and
35
+ `existing_class_name` alongside the usual schema/interface/patterns context.
36
+ **One call** — no need to chain `read_protrak_program` separately.
37
+ 2. Edit the C# source.
38
+ 3. Call `validate_protrak_program(code, program_type)` on the edited source.
39
+ 4. Write the edited source back to the existing `Programs/<name>.cs` file. Do NOT call
40
+ `generate_protrak_program` for refactors — the companion .json already exists.
41
+
42
+ ```
43
+ get_protrak_program_context(type_name='Project', program_type='PreUpdate', program_name='CalculateNextInvoiceDate')
44
+ # ... agent edits existing_source ...
45
+ validate_protrak_program(code='<edited source>', program_type='PreUpdate')
46
+ # Write edited source to existing_file_path
47
+ ```
48
+
49
+ ### What `validate_protrak_program` catches (and what you skip if you don't run it):
50
+
51
+ | Check | What breaks if you ship it |
52
+ | ----------------------------- | ------------------------------------------------ |
53
+ | `.Result` / `.Wait()` | Deadlock under Protrak's async context |
54
+ | `async void` | Exceptions escape silently |
55
+ | Service as private/readonly | Protrak DI requires PUBLIC PROPERTIES — won't inject |
56
+ | Missing interface | PreCreate without `IPreCreateTriggerProgramAsync<T>` — Protrak can't bind it |
57
+ | `*Async()` without `await` | Silent fire-and-forget |
58
+ | Service not declared as prop | Runtime null-ref on first use |
59
+ | Missing namespace | Protrak import rejects the file |
60
+
61
+ These are **Protrak-specific gotchas you cannot catch by reading the code**. Always run
62
+ `validate_protrak_program` after writing or editing.
63
+
64
+ ### Cost of skipping the MCP tools:
65
+
66
+ | Skip | What you miss |
67
+ | ----------------------------- | -------------------------------------------------------------- |
68
+ | `get_protrak_program_context` | Wrong interface, wrong service injection, wrong query patterns |
69
+ | Refactor mode (`program_name`)| Re-derive existing source from disk; miss `services_used` and `patterns_detected` |
70
+ | `validate_protrak_program` | Deadlocks, async-void bugs, DI shape mismatches in production |
71
+ | `generate_protrak_program` | No companion `.json` — Protrak won't import the program |
18
72
 
19
73
  ### Understanding the workspace:
20
- - Call `list_protrak_schema_elements` (optionally with `kind`: `type`, `attribute`,
21
- `lifecycle`, or `relation_type`) to catalog what's defined.
22
- - Call `read_protrak_schema_element` (with `kind` and `name`) for the full JSON of any one
23
- element — for `kind='type'` you get the resolved schema with attributes, lifecycle and
74
+ - `list_protrak_schema_elements` (optionally with `kind`: `type`, `attribute`, `lifecycle`,
75
+ `relation_type`) catalogs what's defined.
76
+ - `read_protrak_schema_element` (with `kind` and `name`) returns the full JSON of any one
77
+ element — for `kind='type'` you get the resolved schema with attributes, lifecycle, and
24
78
  relations hydrated.
25
- - Call `list_protrak_programs` for the program catalog; call `search_protrak_knowledge`
26
- with `scope='programs'` for relevance-ranked search by description.
79
+ - `list_protrak_programs` lists all programs; `read_protrak_program(program_name)` returns
80
+ source + metadata for one (but for refactor work, prefer `get_protrak_program_context`
81
+ with `program_name` — it returns source AND full context together).
82
+ - `search_protrak_knowledge(query, scope)` runs relevance-ranked search across patterns,
83
+ C# API docs, and workspace programs.
27
84
 
28
85
  ### Scaffolding schema elements (Types, Lifecycles, Attributes, RelationTypes):
29
86
  - For a brand-new entity, call `scaffold_protrak_entity` once with the type name, the
@@ -49,17 +106,55 @@ that implement Protrak interfaces (PreCreate, PostCreate, PreUpdate, PromoteActi
49
106
  - All schema names must be PascalCase. Aliases for `attribute_type`: `Number`→`Numeric`,
50
107
  `File`→`Attachment`.
51
108
 
52
- ### Key Protrak rules:
53
- - Services are **public properties**, never constructor parameters
54
- - Always null-check attributes: `instance.Attributes?["Name"]?.Value`
55
- - Use typed accessors: `GetTextAttributeValue`, `GetDateAttributeValue`, `GetPicklistAttributeValue`
56
- - Use `SetAttributeValue(name, AttributeType, value)` to write attributes
57
- - Prefer async interface variants (`RunAsync` not `Run`)
58
- - Return `ProgramResult { IsSuccess = false, Message = "..." }` to block operations in Pre-triggers
109
+ ### Key Protrak rules (each cross-references a `validate_protrak_program` check):
110
+ - Services are **public properties**, never constructor parameters — checked by
111
+ `service_as_public_property`.
112
+ - Implement the exact program-type interface — checked by `interface_compliance`.
113
+ - Prefer async interface variants (`RunAsync` not `Run`); never call `.Result` / `.Wait()`
114
+ checked by `no_dot_result`.
115
+ - Never write `async void` checked by `no_async_void`.
116
+ - `await` or `return` every `*Async()` call — checked by `missing_await`.
117
+ - Declare every service used as a public property — checked by `undeclared_service`.
118
+ - Declare a `namespace` — checked by `missing_namespace`.
119
+ - Always null-check attributes: `instance.Attributes?["Name"]?.Value` (style rule, not
120
+ validated by static analysis).
121
+ - Use typed accessors: `GetTextAttributeValue`, `GetDateAttributeValue`,
122
+ `GetPicklistAttributeValue` (style rule).
123
+ - Use `SetAttributeValue(name, AttributeType, value)` to write attributes (style rule).
124
+ - Return `ProgramResult { IsSuccess = false, Message = "..." }` to block operations in
125
+ Pre-triggers (style rule).
126
+
127
+ ## How to write notification templates
128
+
129
+ Notification templates define the email/push content sent by Protrak. They use **Razor syntax**
130
+ (`@Model.*`), not `{{}}` placeholders.
131
+
132
+ ### Writing a new notification template:
133
+ 1. Call `get_protrak_notification_context(type_name?)` — returns existing templates and the full
134
+ `@Model` variable reference for all target types.
135
+ 2. Write the Razor HTML body using `@Model.*` variables.
136
+ 3. Call `generate_protrak_notification_template(name='<Title> Body', target='PromoteNotification', email_html='<Razor HTML>')`.
137
+ 4. Call again for the subject: `generate_protrak_notification_template(name='<Title> Subject', target='PromoteNotification', email_html='<subject line>')`.
138
+
139
+ ```
140
+ get_protrak_notification_context(type_name='Invoice')
141
+ # ... agent writes the Razor HTML ...
142
+ generate_protrak_notification_template(name='Invoice Sent Body', target='PromoteNotification', email_html='<p>Hi @Model.AttributeValues["Manager"].UserValues[0].UserName,...</p>')
143
+ generate_protrak_notification_template(name='Invoice Sent Subject', target='PromoteNotification', email_html='Invoice @Model.InstanceName has been sent')
144
+ ```
145
+
146
+ Each call creates `NotificationTemplates/<name>.json` (metadata) and `NotificationTemplates/<name>.html`
147
+ (email body). `hashCode` is left empty — Protrak recomputes it on import.
148
+
149
+ Valid `target` values: `PromoteNotification`, `AccountNotification`, `WorkflowNotification`,
150
+ `WorkflowTaskNotification`.
59
151
 
60
152
  ## Project structure
61
153
  - `Types/` — entity type JSON definitions
62
154
  - `Attributes/` — attribute JSON definitions
63
155
  - `Lifecycles/` — lifecycle state machine definitions
64
- - `Programs/` — C# program source + JSON metadata
156
+ - `Programs/` — C# program source + JSON metadata (paired files; use
157
+ `generate_protrak_program` to keep them in sync)
65
158
  - `RelationTypes/` — relation definitions between types
159
+ - `NotificationTemplates/` — notification template files (paired `.json` + `.html`; use
160
+ `generate_protrak_notification_template` to keep them in sync)
@@ -0,0 +1,191 @@
1
+ # Notification Template Reference
2
+
3
+ ## Overview
4
+
5
+ Protrak notification templates define the content of notifications (email + push) triggered by
6
+ platform events. Templates use **Razor markup syntax** (`@Model.*`) and are stored as paired files:
7
+
8
+ - `NotificationTemplates/<title>.json` — metadata (title, target, push message, state)
9
+ - `NotificationTemplates/<title>.html` — HTML email body (Razor template)
10
+
11
+ Two templates are needed for a complete email notification: one for the **subject** (title ending
12
+ in `Subject`) and one for the **body** (title ending in `Body`). Push-only templates need only
13
+ the `.json` with a `messageText` value.
14
+
15
+ ## Target Types
16
+
17
+ | Target | Trigger event | Usage |
18
+ |---|---|---|
19
+ | `PromoteNotification` | Lifecycle promote action | Link in a lifecycle's "Send Notification" command |
20
+ | `AccountNotification` | User creation / password reset | Link in Tenant Settings → Notifications |
21
+ | `WorkflowNotification` | Workflow notification activity | Link in Workflow → Notification Activity |
22
+ | `WorkflowTaskNotification` | Workflow input/checklist task creation | Link in Workflow → Input/Checklist Task Activity |
23
+
24
+ ## @Model Variables — PromoteNotification
25
+
26
+ Available when `target` is `PromoteNotification`. These reflect the promoted instance.
27
+
28
+ ```
29
+ @Model.InstanceName — Display name of the instance
30
+ @Model.InstanceUrl — Deep link URL to the instance
31
+ @Model.InstanceId — GUID of the instance
32
+ @Model.InstanceTrackingId — Tracking / reference number
33
+ @Model.PreviousStateName — Lifecycle state before the promote
34
+ @Model.CurrentStateName — Lifecycle state after the promote
35
+ @Model.ModifiedDate — Date/time of the promote (UTC)
36
+ @Model.ModifiedBy — Name of the user who triggered it
37
+ @Model.CreatedBy — Name of the user who created the instance
38
+ @Model.Remarks — Remarks entered on the promote action
39
+ @Model.AttributeValues["<AttributeName>"] — Attribute accessor (see below)
40
+ @Model.TenantInfo.TenantName
41
+ @Model.TenantInfo.LogoUrl
42
+ @Model.TenantInfo.DateTimeFormat.DateFormat
43
+ @Model.TenantInfo.DateTimeFormat.TimeFormat
44
+ @Model.TenantInfo.DateTimeFormat.TimeZone
45
+ @Model.TenantInfo.Locale.Name
46
+ @Model.TenantInfo.Locale.DisplayName
47
+ @Model.ApplicationUrl
48
+ ```
49
+
50
+ ## @Model Variables — AccountNotification
51
+
52
+ Available when `target` is `AccountNotification`. These reflect the Protrak user account.
53
+
54
+ ```
55
+ @Model.UserName
56
+ @Model.UserFullName
57
+ @Model.UserEmail
58
+ @Model.UserPassword
59
+ @Model.CompanyName
60
+ @Model.CompanyEmail
61
+ @Model.CompanyDomain
62
+ @Model.CompanyLogoUrl
63
+ @Model.UserRoles[index]
64
+ @Model.UserAttributes["<AttributeName>"]
65
+ @Model.MFAVerificationCode
66
+ @Model.MFAVerificationCodeValidity
67
+ ```
68
+
69
+ ## @Model Variables — WorkflowNotification
70
+
71
+ Available when `target` is `WorkflowNotification`. These reflect the instance attached to the workflow.
72
+
73
+ ```
74
+ @Model.InstanceName
75
+ @Model.InstanceUrl
76
+ @Model.InstanceId
77
+ @Model.InstanceTrackingId
78
+ @Model.ApplicationUrl
79
+ @Model.StateName
80
+ @Model.ModifiedDate
81
+ @Model.ModifiedBy
82
+ @Model.CreatedBy
83
+ @Model.AttributeValues["<AttributeName>"]
84
+ ```
85
+
86
+ ## @Model Variables — WorkflowTaskNotification
87
+
88
+ Available when `target` is `WorkflowTaskNotification`. These reflect the workflow task.
89
+
90
+ ```
91
+ @Model.InstanceName
92
+ @Model.InstanceUrl
93
+ @Model.InstanceId
94
+ @Model.InstanceTrackingId
95
+ @Model.ApplicationUrl
96
+ @Model.StateName
97
+ @Model.ModifiedDate
98
+ @Model.ModifiedBy
99
+ @Model.CreatedBy
100
+ @Model.AttributeValues["<AttributeName>"]
101
+ @Model.WorkflowTask.Id
102
+ @Model.WorkflowTask.Name
103
+ @Model.WorkflowTask.Description
104
+ @Model.WorkflowTask.Comments
105
+ @Model.WorkflowTask.Type
106
+ @Model.WorkflowTask.WorkflowInstanceId
107
+ @Model.WorkflowTask.PausedWorkflowInstActivityId
108
+ @Model.WorkflowTask.TypeInstId
109
+ @Model.WorkflowTask.Status
110
+ @Model.WorkflowTask.AssignedUserId
111
+ @Model.WorkflowTask.AssignedRoleId
112
+ @Model.WorkflowTask.Created
113
+ @Model.WorkflowTask.Due
114
+ @Model.WorkflowTask.Completed
115
+ @Model.WorkflowTask.CompletedBy
116
+ @Model.WorkflowTask.ChecklistItems[index].Id
117
+ @Model.WorkflowTask.ChecklistItems[index].Name
118
+ @Model.WorkflowTask.ChecklistItems[index].IsMandatory
119
+ @Model.WorkflowTask.ChecklistItems[index].IsCompleted
120
+ ```
121
+
122
+ ## Attribute Value Accessors
123
+
124
+ The `@Model.AttributeValues["<AttributeName>"]` accessor returns an object with type-specific
125
+ properties. Access the correct property based on the attribute type:
126
+
127
+ ```
128
+ .TextValue — Text attributes
129
+ .NumericValue — Numeric attributes
130
+ .DateValue — Date attributes (stored as UTC DateTime)
131
+ .ArrayValue[0] — Picklist (single) — string value
132
+ .UserValues[0].UserName — User attributes (first user)
133
+ .UserValues[0].UserEmail
134
+ .UserValues[0].UserId
135
+ .ReferenceValue[0].Name — Reference attributes (first linked instance)
136
+ .ReferenceValue[0].Id
137
+ ```
138
+
139
+ ## Razor Syntax Examples
140
+
141
+ ### Simple substitution
142
+ ```html
143
+ <p>Hi @Model.AttributeValues["Employee"].UserValues[0].UserName,</p>
144
+ <p>Your <a href='@Model.InstanceUrl'>@Model.InstanceName</a> has been approved.</p>
145
+ ```
146
+
147
+ ### Conditional content
148
+ ```html
149
+ @if (Model.AttributeValues["Country"].ArrayValue[0] == "India")
150
+ {
151
+ <span>Tax rate: @Model.AttributeValues["GSTRate"].NumericValue%</span>
152
+ }
153
+ else
154
+ {
155
+ <span>No tax applicable.</span>
156
+ }
157
+ ```
158
+
159
+ ### Null-safe access
160
+ ```html
161
+ @(Model.AttributeValues.ContainsKey("Title")
162
+ ? Model.AttributeValues["Title"]?.TextValue ?? "N/A"
163
+ : "N/A")
164
+ ```
165
+
166
+ ## File Format
167
+
168
+ ### `<title>.json`
169
+ ```json
170
+ {
171
+ "title": "Invoice Sent Body",
172
+ "target": "PromoteNotification",
173
+ "emailText": null,
174
+ "hashCode": "",
175
+ "messageText": null,
176
+ "templateState": "Published"
177
+ }
178
+ ```
179
+
180
+ - `emailText` is always `null` in file-based templates — the email body lives in the `.html` file.
181
+ - `hashCode` is left empty; Protrak recomputes it on import (same pattern as `programHashCode`).
182
+ - `messageText` is the plain-text push notification content (Razor syntax supported).
183
+ - `templateState`: `"Published"` or `"InProgress"`.
184
+
185
+ ### `<title>.html`
186
+ ```html
187
+ <p>Hi @Model.AttributeValues["Manager"].UserValues[0].UserName,</p>
188
+ <p>Invoice <strong>@Model.InstanceName</strong> has been sent to the client.</p>
189
+ <p><a href='@Model.InstanceUrl'>View invoice</a></p>
190
+ <p><i>This is a system generated mail, please do not reply.</i></p>
191
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prorigo/protrak-forge",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Protrak domain context for coding agents — MCP server for GitHub Copilot, Claude, and Cursor",
5
5
  "bin": {
6
6
  "protrak-forge": "./bin/protrak-forge.js"
@@ -1,140 +0,0 @@
1
-
2
- # Aggregate Related Instances Pattern
3
-
4
- ## When to Use
5
- Use this pattern whenever a parent instance needs a value computed from its related child instances. Common uses:
6
- - Sum/total an attribute across related records (consumed effort, total cost, hours logged)
7
- - Count related instances by state (open subtasks, overdue items)
8
- - Roll up a max/min/latest date from related records
9
- - Calculate completion percentages from related deliverables
10
-
11
- This pattern is the canonical replacement for the **N+1 fan-out anti-pattern** (calling `GetInstanceAsync` once per related id just to read attributes — see "Anti-Pattern" below).
12
-
13
- ## Program Types
14
- Applies to: `Scheduler`, `PostCreate`, `PromoteActionCommand`, `Common`
15
-
16
- ## Services Used
17
- - `IInstanceService` — query parents, fetch related instances, write the aggregate back
18
- - `IQueryBuilderService` — filter the parent set by state/attribute (optional)
19
-
20
- ## Key Idiom: `RelatedQueryBuilder.Select(...)`
21
-
22
- The single most important rule for this pattern:
23
-
24
- > **Always use `RelatedQueryBuilder.Select(...)` to fetch the attributes you need from related instances in the same call — do not call `GetInstanceAsync` per related id.**
25
-
26
- `GetRelatedInstancesAsync` returns `RelatedInstance` objects that already carry the attributes you `.Select()`-ed. Calling `GetInstanceAsync` afterwards is an N+1 round-trip per parent and is strictly worse.
27
-
28
- ## Code Example
29
-
30
- Scheduler that, for each Project in `Active` state, sums "consumed effort" across all related Allocations (weekdays between AllocationStartDate and AllocationEndDate, today if end is blank/future) and writes back to `ConsumedEffort` and `ConsumedEffortCalculatedOn` on the Project.
31
-
32
- ```csharp
33
- using Prorigo.Protrak.API.Contracts;
34
- using Prorigo.Protrak.API.Contracts.Builders;
35
- using Prorigo.Protrak.API.Contracts.Extensions;
36
- using System;
37
- using System.Linq;
38
- using System.Threading.Tasks;
39
-
40
- namespace MyProject.Customization
41
- {
42
- public class CalculateProjectConsumedEffort : ISchedulerProgramAsync
43
- {
44
- public IInstanceService InstanceService { get; set; }
45
- public IQueryBuilderService QueryBuilderService { get; set; }
46
-
47
- public async Task RunAsync()
48
- {
49
- var projects = await InstanceService.GetInstancesAsync(new InstanceQuery
50
- {
51
- TypeName = "Project",
52
- StateName = "Active",
53
- Take = int.MaxValue,
54
- });
55
- if (projects?.Data == null) return;
56
-
57
- var today = DateTime.UtcNow.Date;
58
-
59
- foreach (var project in projects.Data)
60
- {
61
- // ✅ Fetch only the attributes we need from related Allocations,
62
- // in a SINGLE call. Do NOT loop GetInstanceAsync per id.
63
- var allocationQuery = RelatedQueryBuilder.Create()
64
- .ForRelation("ProjectToAllocation", "Allocation", RelationDirection.From)
65
- .Select("AllocationStartDate", "AllocationEndDate")
66
- .TakeAll()
67
- .Build();
68
-
69
- var allocations = await InstanceService.GetRelatedInstancesAsync(
70
- project.Id, allocationQuery);
71
-
72
- double totalWeekdays = 0;
73
- foreach (var allocation in allocations.SafeItems())
74
- {
75
- var start = allocation.GetDateAttributeValue("AllocationStartDate");
76
- var end = allocation.GetDateAttributeValue("AllocationEndDate");
77
- if (start == null) continue;
78
-
79
- var effectiveEnd = (end == null || end.Value.Date > today)
80
- ? today
81
- : end.Value.Date;
82
-
83
- totalWeekdays += CountWeekdays(start.Value.Date, effectiveEnd);
84
- }
85
-
86
- var update = InstanceBuilder.ForUpdate(project.Id, "Project")
87
- .SetNumericAttributeValue("ConsumedEffort", totalWeekdays)
88
- .SetDateAttributeValue("ConsumedEffortCalculatedOn", today)
89
- .Build();
90
-
91
- await InstanceService.UpdateInstanceAsync(update, null);
92
- }
93
- }
94
-
95
- private static int CountWeekdays(DateTime fromInclusive, DateTime toInclusive)
96
- {
97
- if (toInclusive < fromInclusive) return 0;
98
- int count = 0;
99
- for (var d = fromInclusive; d <= toInclusive; d = d.AddDays(1))
100
- {
101
- if (d.DayOfWeek != DayOfWeek.Saturday && d.DayOfWeek != DayOfWeek.Sunday)
102
- count++;
103
- }
104
- return count;
105
- }
106
- }
107
- }
108
- ```
109
-
110
- ## Anti-Pattern (DO NOT DO THIS)
111
-
112
- ```csharp
113
- // ❌ Wrong: N+1 round-trips. One DB hit per related allocation.
114
- var allocationsRef = await InstanceService.GetRelatedInstancesAsync(
115
- project.Id, RelatedQueryBuilder.Create()
116
- .ForRelation("ProjectToAllocation", "Allocation", RelationDirection.From)
117
- .TakeAll().Build());
118
-
119
- foreach (var ref in allocationsRef.SafeItems())
120
- {
121
- var full = await InstanceService.GetInstanceAsync(
122
- ref.Id, new[] { "AllocationStartDate", "AllocationEndDate" }); // ← extra call per id
123
- var start = full.GetDateAttributeValue("AllocationStartDate");
124
- // ...
125
- }
126
- ```
127
-
128
- The fix is to add `.Select("AllocationStartDate", "AllocationEndDate")` to the builder and read the attributes off the `RelatedInstance` directly. `RelatedInstance` exposes the same `GetTextAttributeValue` / `GetDateAttributeValue` / `GetNumericAttributeValue` accessors as `Instance` — see [Instance Accessor Methods](./instance-accessor-methods.md#on-relatedinstance).
129
-
130
- ## Key Rules
131
-
132
- - Always pre-select the attributes you need with `.Select(name1, name2, ...)`. Omitting `.Select()` returns instances without their attribute values populated; adding `GetInstanceAsync` calls afterwards is the N+1 anti-pattern.
133
- - Use `.TakeAll()` (or `Skip`/`Take` for pagination) — the default page size truncates large parents.
134
- - Use `RelatedInstance` accessors directly (`GetDateAttributeValue`, etc.) — same syntax as `Instance`.
135
- - Iterate with `.SafeItems()` to avoid manual null checks.
136
- - Write the rolled-up value back with `InstanceBuilder.ForUpdate(...)` + `UpdateInstanceAsync`. Do not mutate the parent in-place inside a Scheduler — Scheduler programs receive no incoming Instance.
137
- - For schedulers, prefer `Take = int.MaxValue` on the parent query to ensure all parents are processed; for *very* large datasets, paginate explicitly.
138
-
139
- ## Related Patterns
140
- - [Query and Filter](./query-filter-pattern.md) — filtering the parent set