@hailer/mcp 1.1.17-beta.2 → 1.2.0

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.
Files changed (83) hide show
  1. package/.claude/CLAUDE.md +94 -135
  2. package/.claude/skills/create-and-publish-app/SKILL.md +127 -0
  3. package/.claude/skills/hailer-app-builder/SKILL.md +47 -0
  4. package/.claude/skills/publish-hailer-app/SKILL.md +63 -4
  5. package/.claude/skills/sdk-function-fields/SKILL.md +431 -287
  6. package/dist/bot/bot-manager.d.ts.map +1 -1
  7. package/dist/bot/bot-manager.js +2 -0
  8. package/dist/bot/bot-manager.js.map +1 -1
  9. package/dist/bot/bot.d.ts +2 -1
  10. package/dist/bot/bot.d.ts.map +1 -1
  11. package/dist/bot/bot.js +109 -41
  12. package/dist/bot/bot.js.map +1 -1
  13. package/dist/bot/services/message-classifier.d.ts.map +1 -1
  14. package/dist/bot/services/message-classifier.js +6 -0
  15. package/dist/bot/services/message-classifier.js.map +1 -1
  16. package/dist/bot/services/signal-router.d.ts.map +1 -1
  17. package/dist/bot/services/signal-router.js +1 -0
  18. package/dist/bot/services/signal-router.js.map +1 -1
  19. package/dist/bot/services/system-prompt.d.ts +4 -0
  20. package/dist/bot/services/system-prompt.d.ts.map +1 -1
  21. package/dist/bot/services/system-prompt.js +41 -12
  22. package/dist/bot/services/system-prompt.js.map +1 -1
  23. package/dist/bot/services/types.d.ts +7 -31
  24. package/dist/bot/services/types.d.ts.map +1 -1
  25. package/dist/bot/services/workspace-refresh.js.map +1 -1
  26. package/dist/bot/workspace-overview.d.ts.map +1 -1
  27. package/dist/bot/workspace-overview.js +4 -1
  28. package/dist/bot/workspace-overview.js.map +1 -1
  29. package/dist/bot-config/context.js.map +1 -1
  30. package/dist/bot-config/loader.d.ts.map +1 -1
  31. package/dist/bot-config/loader.js +1 -0
  32. package/dist/bot-config/loader.js.map +1 -1
  33. package/dist/bot-config/types.d.ts +2 -0
  34. package/dist/bot-config/types.d.ts.map +1 -1
  35. package/dist/mcp/UserContextCache.d.ts.map +1 -1
  36. package/dist/mcp/UserContextCache.js +8 -16
  37. package/dist/mcp/UserContextCache.js.map +1 -1
  38. package/dist/mcp/tool-registry.d.ts +3 -2
  39. package/dist/mcp/tool-registry.d.ts.map +1 -1
  40. package/dist/mcp/tool-registry.js +14 -9
  41. package/dist/mcp/tool-registry.js.map +1 -1
  42. package/dist/mcp/tools/activity.d.ts.map +1 -1
  43. package/dist/mcp/tools/activity.js +39 -94
  44. package/dist/mcp/tools/activity.js.map +1 -1
  45. package/dist/mcp/tools/app-scaffold.d.ts.map +1 -1
  46. package/dist/mcp/tools/app-scaffold.js +300 -575
  47. package/dist/mcp/tools/app-scaffold.js.map +1 -1
  48. package/dist/mcp/tools/date.d.ts +5 -0
  49. package/dist/mcp/tools/date.d.ts.map +1 -0
  50. package/dist/mcp/tools/date.js +23 -0
  51. package/dist/mcp/tools/date.js.map +1 -0
  52. package/dist/mcp/tools/discussion.d.ts.map +1 -1
  53. package/dist/mcp/tools/discussion.js +17 -9
  54. package/dist/mcp/tools/discussion.js.map +1 -1
  55. package/dist/mcp/tools/index.d.ts.map +1 -1
  56. package/dist/mcp/tools/index.js +2 -0
  57. package/dist/mcp/tools/index.js.map +1 -1
  58. package/dist/mcp/tools/insight.d.ts.map +1 -1
  59. package/dist/mcp/tools/insight.js +13 -19
  60. package/dist/mcp/tools/insight.js.map +1 -1
  61. package/dist/mcp/tools/workflow.d.ts +1 -0
  62. package/dist/mcp/tools/workflow.d.ts.map +1 -1
  63. package/dist/mcp/tools/workflow.js +293 -46
  64. package/dist/mcp/tools/workflow.js.map +1 -1
  65. package/dist/mcp/utils/data-transformers.d.ts +47 -10
  66. package/dist/mcp/utils/data-transformers.d.ts.map +1 -1
  67. package/dist/mcp/utils/data-transformers.js +12 -9
  68. package/dist/mcp/utils/data-transformers.js.map +1 -1
  69. package/dist/mcp/utils/types.d.ts +2 -0
  70. package/dist/mcp/utils/types.d.ts.map +1 -1
  71. package/dist/mcp/utils/types.js.map +1 -1
  72. package/dist/mcp/webhook-handler.d.ts.map +1 -1
  73. package/dist/mcp/webhook-handler.js +4 -1
  74. package/dist/mcp/webhook-handler.js.map +1 -1
  75. package/dist/mcp/workspace-cache.d.ts +8 -2
  76. package/dist/mcp/workspace-cache.d.ts.map +1 -1
  77. package/dist/mcp/workspace-cache.js +12 -8
  78. package/dist/mcp/workspace-cache.js.map +1 -1
  79. package/dist/plugins/vipunen/tools.d.ts +1 -0
  80. package/dist/plugins/vipunen/tools.d.ts.map +1 -1
  81. package/dist/plugins/vipunen/tools.js.map +1 -1
  82. package/package.json +1 -1
  83. package/scripts/postinstall.cjs +0 -9
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: sdk-function-fields
3
3
  description: Complete guide to creating calculated function fields in Hailer - workflow, variable types, patterns
4
- version: 1.2.0
4
+ version: 2.0.0
5
5
  triggers:
6
6
  - function field
7
7
  - calculated field
@@ -20,7 +20,7 @@ Complete guide for creating calculated fields that auto-update when dependencies
20
20
  ## Overview
21
21
 
22
22
  Function fields compute values from other fields. Examples:
23
- - Total = Quantity × Unit Price
23
+ - Total = Quantity x Unit Price
24
24
  - Days Until Due = Due Date - Today
25
25
  - Invoice Total = Sum of line item prices (filtered by phase)
26
26
 
@@ -35,15 +35,14 @@ npm run pull
35
35
 
36
36
  ### 2. Add Field Definition (fields.ts)
37
37
 
38
+ Add to the `fields` array in `workspace/[Workflow]_[id]/fields.ts`:
39
+
38
40
  ```typescript
39
- // workspace/[Workflow]_[id]/fields.ts
41
+ // NEW field — omit _id, server assigns it after push
40
42
  {
41
- label: "Total Cost",
42
- type: "numeric",
43
- key: "total_cost",
44
- function: "@function:totalCost",
43
+ data: [],
44
+ function: "@function:total_cost_XXX", // Must match exported function name
45
45
  functionEnabled: true,
46
- editable: false,
47
46
  functionVariables: {
48
47
  quantity: {
49
48
  type: "=",
@@ -53,46 +52,89 @@ npm run pull
53
52
  type: "=",
54
53
  data: [FieldIds.unit_price]
55
54
  }
56
- }
55
+ },
56
+ inviteToDiscussionOnChange: false,
57
+ key: "totalCost",
58
+ label: "Total Cost",
59
+ required: false,
60
+ type: "numeric"
57
61
  }
58
62
  ```
59
63
 
64
+ **Required properties for function fields:**
65
+ - `function`: `"@function:{functionName}"` — must match the exported function name exactly
66
+ - `functionEnabled`: `true`
67
+ - `functionVariables`: dependency wiring (see Variable Types below)
68
+ - `data`: `[]` — always empty array for function fields
69
+ - `type`: the output type (`"numeric"`, `"text"`, `"date"`, etc.)
70
+
71
+ **For new fields:** Omit `_id`. The server assigns it after push. Run `npm run pull` after push to get the server-assigned ID.
72
+
73
+ **For existing fields:** Keep the `_id` from enums.
74
+
60
75
  ### 3. Create Function File
61
76
 
62
- Create `workspace/[Workflow]_[id]/functions/totalCost.ts`:
77
+ **Naming convention:** `{functionName}_{last3HexOfFieldId}.ts`
78
+
79
+ - For new fields (no ID yet): use a temporary 3-char suffix, e.g., `total_cost_tmp.ts`
80
+ - After push + pull, the SDK renames to match the server-assigned ID
81
+
82
+ Create `workspace/[Workflow]_[id]/functions/total_cost_XXX.ts`:
63
83
 
64
84
  ```typescript
65
- export function totalCost(dep: {
66
- quantity: number | null;
67
- unitPrice: number | null;
68
- }): number {
85
+ /**
86
+ * Field function for: Total Cost
87
+ * Field name: Total Cost
88
+ * Field ID: [assigned after push]
89
+ */
90
+
91
+ interface Dependencies {
92
+ quantity: number;
93
+ unitPrice: number;
94
+ }
95
+
96
+ export function total_cost_XXX(dep: Dependencies): number {
69
97
  const qty = Number(dep.quantity) || 0;
70
98
  const price = Number(dep.unitPrice) || 0;
71
99
  return qty * price;
72
100
  }
73
101
  ```
74
102
 
75
- ### 4. Export Function
103
+ **File structure rules:**
104
+ 1. **JSDoc header** — field function name, label, and ID
105
+ 2. **`Dependencies` interface** — typed to match source field types (see Field Data Formats table)
106
+ 3. **Named export** — function name must match the `@function:` reference in fields.ts
107
+ 4. **Return type** — always explicit (`number`, `string`, etc.), never `any`
108
+
109
+ ### 4. Export from Index
76
110
 
77
111
  Edit `workspace/[Workflow]_[id]/functions/index.ts`:
78
112
 
79
113
  ```typescript
80
- export { totalCost } from './totalCost';
114
+ // Auto-generated index of field functions
115
+ // Import and re-export all field functions
116
+
117
+ export { total_cost_XXX } from "./total_cost_XXX";
81
118
  ```
82
119
 
83
- ### 5. Test
120
+ ### 5. Test with Vitest
84
121
 
85
122
  ```typescript
86
- // workspace/[Workflow]_[id]/main.test.ts
87
- import { totalCost } from './functions';
123
+ // workspace/[Workflow]_[id]/functions/total_cost_XXX.test.ts
124
+ import { total_cost_XXX } from "./total_cost_XXX";
88
125
 
89
- describe('totalCost', () => {
126
+ describe('total_cost_XXX', () => {
90
127
  it('multiplies quantity by unit price', () => {
91
- expect(totalCost({ quantity: 5, unitPrice: 10 })).toBe(50);
128
+ expect(total_cost_XXX({ quantity: 5, unitPrice: 10 })).toBe(50);
92
129
  });
93
130
 
94
- it('handles null values', () => {
95
- expect(totalCost({ quantity: null, unitPrice: 10 })).toBe(0);
131
+ it('handles null/undefined values', () => {
132
+ expect(total_cost_XXX({ quantity: null, unitPrice: 10 } as any)).toBe(0);
133
+ expect(total_cost_XXX({ quantity: 5, unitPrice: null } as any)).toBe(0);
134
+ });
135
+
136
+ it('handles non-numeric values', () => {
137
+ expect(total_cost_XXX({ quantity: "abc", unitPrice: 10 } as any)).toBe(0);
96
138
  });
97
139
  });
98
140
  ```
@@ -105,278 +147,325 @@ Run: `npm test`
105
147
  npm run fields-push:force
106
148
  ```
107
149
 
108
- Use `fields-push:force` (not `fields-push`). The non-force variant uses `fast-deep-equal` comparison which may skip `functionVariables` changes your dependency wiring won't update.
150
+ **Always use `:force`** the non-force variant uses `fast-deep-equal` comparison which may skip `functionVariables` changes, so your dependency wiring won't update.
151
+
152
+ ### 7. Pull to Get Server IDs
153
+
154
+ ```bash
155
+ npm run pull
156
+ ```
109
157
 
110
- ### 7. Verify Enum Imports After Pull
158
+ After pull:
159
+ - Field gets a server-assigned `_id`
160
+ - Function file may be renamed to match the new ID suffix
161
+ - **Check enum imports** — identical hex suffixes across workflows can resolve to the wrong enum
111
162
 
112
- After pull, check enum imports manually. Identical hex suffixes across workflows can resolve to the wrong enum (e.g., `Tasks_FieldIds` vs `Orders_FieldIds` both having a field ending in `_abc123`). Fix any wrong imports before proceeding.
163
+ **Important: Pull overwrites TypeScript declarations.** The server stores only the function body as plain JavaScript. On pull, the SDK regenerates the TypeScript wrapper (interface, return type, comments) from server metadata. Your type fixes get overwritten every time.
164
+
165
+ | Part of File | Persists After Pull? | Why |
166
+ |--------------|---------------------|-----|
167
+ | Function body code | Yes | Stored on server as-is |
168
+ | `Number()` wrapping | Yes | Part of function body |
169
+ | Named constants | Yes | Part of function body |
170
+ | Removed dependencies | Yes | Stored in server metadata |
171
+ | `interface Dependencies` types | No | SDK regenerates from schema |
172
+ | Return type annotation | No | SDK always uses `: any` |
173
+ | Comments/JSDoc | No | Not stored on server |
174
+
175
+ The SDK's type inference is imperfect — it may map `numeric` fields to `string` in the interface. This is cosmetic, not a runtime bug. **Always use defensive coding in the function body** (`Number()` wrapping, `|| 0` defaults) — never rely on TypeScript types for runtime safety. The function body is what runs on the server.
113
176
 
114
177
  ---
115
178
 
116
- ## Field Data Formats in Functions
179
+ ## Quality Rules
117
180
 
118
- What format does each field type return when used as a dependency variable?
181
+ These rules prevent the bugs found in real function fields. Follow them exactly.
119
182
 
120
- | Field Type | Returns | Example |
121
- |------------|---------|---------|
122
- | `text`, `textarea` | `string` | `"Hello world"` |
123
- | `numeric`, `numericunit` | `number` | `42.5` |
124
- | `date`, `datetime` | `number` (ms timestamp) | `1730937600000` |
125
- | `daterange`, `datetimerange` | `{ start: number, end: number }` | `{ start: 1730937600000, end: 1731024000000 }` |
126
- | `time` | `number` (ms timestamp, includes date!) | `1765863000000` |
127
- | `timerange` | `{ start: number, end: number }` | `{ start: 1765863000000, end: 1765915200000 }` (ms timestamps, includes date!) |
128
- | `textpredefinedoptions` | `string` | `"High"` |
129
- | `users`, `teams` | `string` (ID) | `"5f8a1b2c3d4e5f6a7b8c9d0e"` |
130
- | `activitylink` | `string` (activity ID) | `"692abc123def456"` |
131
- | `country` | `string` (ISO code) | `"FI"` |
132
- | `numeric` + `modifier.checkbox` | `number` | `1` (true) or `0` (false) |
183
+ ### Dependencies Interface Must Match Source Types
184
+
185
+ Map each `functionVariables` entry to its correct TypeScript type based on how it's wired:
186
+
187
+ | Variable type | Source | Dependencies type |
188
+ |---------------|--------|-------------------|
189
+ | `=` (same activity field) | numeric field | `number` |
190
+ | `=` (same activity field) | text field | `string` |
191
+ | `=` (same activity field) | date field | `number` (ms timestamp) |
192
+ | `<` (backlink to field) | any field | `Array<SourceType>` |
193
+ | `<` (backlink to `"meta"`) | metadata | `Array<ActivityMeta>` |
194
+ | `?` (static phase ID) | phase enum | `string` |
195
+ | `>` (forward link to field) | any field | `SourceType` |
196
+ | `>` (forward link to `"meta"`) | metadata | `ActivityMeta` |
197
+ | `=` with `data: ["meta"]` | own metadata | `ActivityMeta` |
198
+
199
+ **Wrong:** Typing a numeric source field as `string` (causes silent JS coercion)
200
+ **Right:** Always use the actual source field's type
201
+
202
+ ### No Unused Dependencies
133
203
 
134
- **Examples:**
135
-
136
- ```javascript
137
- // Date field (milliseconds timestamp)
138
- const dueDate = dep.dueDate; // 1730937600000
139
- const isOverdue = dueDate < Date.now();
140
-
141
- // Daterange field (object with start/end)
142
- const period = dep.eventPeriod; // { start: 1730937600000, end: 1731024000000 }
143
- const startDate = period ? period.start : null;
144
- const endDate = period ? period.end : null;
145
- const durationMs = (endDate && startDate) ? (endDate - startDate) : 0;
146
- const durationDays = Math.ceil(durationMs / 86400000);
147
-
148
- // Time field (ms timestamp - includes date!)
149
- const startTime = dep.startTime; // 1765863000000
150
- // To extract just the time, use Date methods:
151
- const date = new Date(startTime);
152
- const hours = date.getUTCHours();
153
- const minutes = date.getUTCMinutes();
154
-
155
- // Timerange field (object with start/end as ms timestamps - includes date!)
156
- const workHours = dep.workingHours; // { start: 1765863000000, end: 1765915200000 }
157
- const durationMs = workHours ? (workHours.end - workHours.start) : 0;
158
- const durationMinutes = durationMs / 60000;
159
-
160
- // IMPORTANT: When to convert vs keep raw:
161
- // - Display only (text output) → use Date methods to extract time
162
- // - Write to another date/time field → keep as Unix milliseconds (no conversion!)
163
-
164
- // Checkbox field (1 or 0)
165
- const isActive = dep.isActive; // 1 or 0
166
- if (isActive === 1) { /* checked */ }
204
+ Every entry in `functionVariables` must be used in the function body. If you declare a dependency you don't use, it causes unnecessary cross-workflow lookups on every recalculation.
205
+
206
+ **Wrong:**
207
+ ```typescript
208
+ interface Dependencies {
209
+ goals: Array<number>;
210
+ assists: Array<number>;
211
+ salary: Array<number>; // UNUSED wasted cross-workflow lookup
212
+ }
213
+ export function rating(dep: Dependencies): number {
214
+ // salary is never referenced
215
+ return sumArray(dep.goals) * 2 + sumArray(dep.assists);
216
+ }
167
217
  ```
168
218
 
219
+ **Right:** Remove `salary` from both `functionVariables` in fields.ts AND the Dependencies interface.
220
+
221
+ ### Return Type — Always Explicit, Never `any`
222
+
223
+ ```typescript
224
+ // Wrong
225
+ export function myFunc(dep: Dependencies): any { ... }
226
+
227
+ // Right
228
+ export function myFunc(dep: Dependencies): number { ... }
229
+ ```
230
+
231
+ ### Defensive Value Handling
232
+
233
+ ```typescript
234
+ // Scalars — default to zero/empty
235
+ const value = Number(dep.quantity) || 0;
236
+ const text = dep.name || "";
237
+
238
+ // Arrays (backlinks) — ALWAYS default to empty array
239
+ const items = dep.items || [];
240
+
241
+ // Metadata — null-check before property access
242
+ const phase = meta ? meta.phase : null;
243
+
244
+ // Array iteration — use for loop with index, null-check each element
245
+ for (let i = 0; i < items.length; i++) {
246
+ const val = Number(items[i]) || 0;
247
+ }
248
+ ```
249
+
250
+ ### Runtime Is Plain JavaScript
251
+
252
+ Files are `.ts` for local dev and testing, but **the runtime strips TypeScript and executes plain ES6 JavaScript** in isolated-vm. This means:
253
+ - `interface` declarations are stripped — they're for dev-time safety only
254
+ - No TypeScript-only syntax in the function body (no `as`, no generics, no `!` assertions)
255
+ - Optional chaining (`?.`) and nullish coalescing (`??`) work (ES2020)
256
+ - `const`/`let`/`for...of`/arrow functions/template literals all work
257
+
169
258
  ---
170
259
 
171
- ## Backlink Arrays (`<`)
260
+ ## Variable Types Reference
172
261
 
173
- Backlinks return **arrays** because multiple activities can link to this one.
262
+ ### `=` Same Activity Field
174
263
 
175
- ```javascript
176
- // functionVariables config
177
- {
264
+ Read a field value from the same activity.
265
+
266
+ ```typescript
267
+ // fields.ts
268
+ functionVariables: {
269
+ quantity: {
270
+ type: "=",
271
+ data: [FieldIds.quantity]
272
+ }
273
+ }
274
+
275
+ // function — returns scalar value
276
+ const qty = Number(dep.quantity) || 0;
277
+ ```
278
+
279
+ ### `<` — Backlink (Other Activities Linking TO This One)
280
+
281
+ Returns **arrays** — multiple activities can link to this one.
282
+
283
+ ```typescript
284
+ // fields.ts
285
+ functionVariables: {
178
286
  prices: {
179
287
  type: "<",
180
288
  data: [WorkflowIds.invoice_rows, InvoiceRows_FieldIds.total_price]
181
289
  }
182
290
  }
183
291
 
184
- // In function - ALWAYS default to empty array
292
+ // function ALWAYS default to empty array
185
293
  const prices = dep.prices || [];
186
294
  let total = 0;
187
295
  for (let i = 0; i < prices.length; i++) {
188
296
  total += Number(prices[i]) || 0;
189
297
  }
190
- return total;
191
298
  ```
192
299
 
193
- ---
194
-
195
- ## Activity Metadata (`"meta"`)
196
-
197
- The special `"meta"` value returns activity metadata. It works with **all variable types**:
300
+ ### `>` — Forward Link (This Activity Links TO Another)
198
301
 
199
- ### Current Activity Metadata (`=`)
302
+ Read a field value from a linked activity.
200
303
 
201
- ```javascript
202
- // Get metadata of THIS activity
203
- {
204
- myMeta: {
205
- type: "=",
206
- data: ["meta"]
304
+ ```typescript
305
+ // fields.ts
306
+ functionVariables: {
307
+ customerName: {
308
+ type: ">",
309
+ data: [FieldIds.customer_link, Customers_FieldIds.name]
207
310
  }
208
311
  }
209
312
 
210
- // In function
211
- const myPhase = dep.myMeta ? dep.myMeta.phase : null;
212
- const myCreated = dep.myMeta ? dep.myMeta.created : 0;
313
+ // function — returns scalar value (single linked activity)
314
+ const name = dep.customerName || "";
213
315
  ```
214
316
 
215
- ### Forward Link Metadata (`>`)
317
+ ### `?` Static Value (Phase ID for Comparison)
216
318
 
217
- ```javascript
218
- // Get metadata of LINKED activity (this → other)
219
- {
220
- customerMeta: {
221
- type: ">",
222
- data: [FieldIds.customer_link, "meta"]
319
+ Provides a known phase ID to compare against in logic.
320
+
321
+ ```typescript
322
+ // fields.ts
323
+ functionVariables: {
324
+ activePhase: {
325
+ type: "?",
326
+ data: [WorkflowIds.injuries, Injuries_PhaseIds.active]
223
327
  }
224
328
  }
225
329
 
226
- // In function
227
- const customerPhase = dep.customerMeta ? dep.customerMeta.phaseName : 'Unknown';
330
+ // function — returns phase ID string
331
+ const activePhase = dep.activePhase; // "627d14339381d6077ab90ce9"
332
+ if (meta.phase === activePhase) { ... }
333
+ ```
334
+
335
+ ---
336
+
337
+ ## Activity Metadata (`"meta"`)
338
+
339
+ The special `"meta"` value returns activity metadata. Works with all variable types.
340
+
341
+ ### Own Metadata (`=`)
342
+
343
+ ```typescript
344
+ functionVariables: {
345
+ myMeta: { type: "=", data: ["meta"] }
346
+ }
347
+ // dep.myMeta → { _id, name, phase, phaseName, created, updated, ... }
228
348
  ```
229
349
 
230
350
  ### Backlink Metadata (`<`)
231
351
 
232
- ```javascript
233
- // Get metadata of activities linking TO this (others → this)
234
- {
235
- rowsMeta: {
236
- type: "<",
237
- data: [WorkflowIds.invoice_rows, "meta"]
238
- }
352
+ ```typescript
353
+ functionVariables: {
354
+ injuryMeta: { type: "<", data: [WorkflowIds.injuries, "meta"] }
239
355
  }
356
+ // dep.injuryMeta → Array<ActivityMeta>
357
+ ```
240
358
 
241
- // In function - returns ARRAY
242
- const rows = dep.rowsMeta || [];
243
- for (let i = 0; i < rows.length; i++) {
244
- const phaseName = rows[i] ? rows[i].phaseName : 'Unknown';
359
+ ### Forward Link Metadata (`>`)
360
+
361
+ ```typescript
362
+ functionVariables: {
363
+ customerMeta: { type: ">", data: [FieldIds.customer_link, "meta"] }
245
364
  }
365
+ // dep.customerMeta → ActivityMeta
246
366
  ```
247
367
 
248
368
  ### Metadata Structure
249
369
 
250
- ```javascript
251
- {
252
- "_id": "694c9536acfa30f6df13201b", // Activity ID
253
- "name": "Invoice row name", // Activity name
254
- "process": "67dc1b7d3d2c9f6cf9a5468d", // Workflow ID
255
- "phase": "67dc1b7d3d2c9f6cf9a546c4", // Phase ID
256
- "processName": "Invoice Rows", // Workflow name
257
- "phaseName": "Active", // Phase name
258
- "created": 1766626614031, // Created timestamp (ms)
259
- "updated": 1766626614031, // Updated timestamp (ms)
260
- "completed": null, // Completed timestamp or null
261
- "sequence": 1649, // Activity sequence number
262
- "active": true // Is activity active
370
+ ```typescript
371
+ interface ActivityMeta {
372
+ _id: string; // Activity ID
373
+ name: string; // Activity name
374
+ process: string; // Workflow ID
375
+ phase: string; // Phase ID
376
+ processName: string; // Workflow name
377
+ phaseName: string; // Phase name
378
+ created: number; // Created timestamp (ms)
379
+ updated: number; // Updated timestamp (ms)
380
+ completed: number | null; // Completed timestamp or null
381
+ sequence: number; // Activity sequence number
382
+ active: boolean; // Is activity active
263
383
  }
264
384
  ```
265
385
 
386
+ **Note:** `"data"` and `"meta"` are interchangeable aliases. Prefer `"meta"` for clarity.
387
+
266
388
  ---
267
389
 
268
390
  ## Parallel Array Guarantee
269
391
 
270
- **CRITICAL INSIGHT:** When you have multiple `<` (backlink) variables from the **SAME source workflow**, they are guaranteed to be:
392
+ **CRITICAL:** When you have multiple `<` (backlink) variables from the **SAME source workflow**, they are guaranteed to be:
271
393
  - Same length
272
394
  - Same order (index 0 in all arrays = same activity)
273
395
 
274
- **This is a core Hailer feature that enables safe parallel iteration.**
396
+ This enables safe parallel iteration:
275
397
 
276
- ```javascript
277
- const prices = dep['Total price'] || []; // [41.76, 26.1, 153.47]
278
- const data = dep['Data'] || []; // [{...}, {...}, {...}]
398
+ ```typescript
399
+ const prices = dep.prices || []; // [41.76, 26.1, 153.47]
400
+ const rowMeta = dep.rowMeta || []; // [{...}, {...}, {...}]
279
401
 
280
- // Index 0 in both = same Invoice Row activity
402
+ // Index 0 in both = same source activity
281
403
  for (let i = 0; i < prices.length; i++) {
282
404
  const price = Number(prices[i]) || 0;
283
- const phaseName = data[i] ? data[i].phaseName : 'Unknown';
284
- // price and phaseName are from the SAME activity
405
+ const phaseName = rowMeta[i] ? rowMeta[i].phaseName : 'Unknown';
285
406
  }
286
407
  ```
287
408
 
288
- **Arrays from DIFFERENT workflows** are independent.
409
+ **Arrays from DIFFERENT source workflows are independent — never assume index alignment across workflows.**
289
410
 
290
411
  ---
291
412
 
292
- ## Static Variables (`?`)
293
-
294
- Static variables provide phase IDs for comparison in your function logic.
295
-
296
- ```javascript
297
- // Get specific phase IDs to compare against
298
- {
299
- donePhase: {
300
- type: "?",
301
- data: [WorkflowIds.sales_activities, PhaseIds.done]
302
- },
303
- archivePhase: {
304
- type: "?",
305
- data: [WorkflowIds.sales_activities, PhaseIds.archive]
306
- }
307
- }
308
-
309
- // In function - compare against metadata phase
310
- const donePhase = dep.donePhase; // "627d14339381d6077ab90ce9"
311
- const archivePhase = dep.archivePhase; // "639b49617a4413c20f15ac12"
413
+ ## Field Data Formats in Functions
312
414
 
313
- if (item.phase === donePhase || item.phase === archivePhase) {
314
- // Activity is in done or archive phase
315
- }
316
- ```
415
+ What format does each field type return when used as a dependency?
317
416
 
318
- **Note:** To get `process` and `phase` IDs from activities, use `"meta"` (not `?`). Static `?` is only for providing known phase IDs for comparison.
417
+ | Field Type | Returns | Example |
418
+ |------------|---------|---------|
419
+ | `text`, `textarea` | `string` | `"Hello world"` |
420
+ | `numeric`, `numericunit` | `number` | `42.5` |
421
+ | `date`, `datetime` | `number` (ms timestamp) | `1730937600000` |
422
+ | `daterange`, `datetimerange` | `{ start: number, end: number }` | `{ start: 1730937600000, end: 1731024000000 }` |
423
+ | `time` | `number` (ms timestamp, includes date!) | `1765863000000` |
424
+ | `timerange` | `{ start: number, end: number }` | `{ start: 1765863000000, end: 1765915200000 }` |
425
+ | `textpredefinedoptions` | `string` | `"High"` |
426
+ | `users`, `teams` | `string` (ID) | `"5f8a1b2c3d4e5f6a7b8c9d0e"` |
427
+ | `activitylink` | `string` (activity ID) | `"692abc123def456"` |
428
+ | `country` | `string` (ISO code) | `"FI"` |
429
+ | `numeric` + `modifier.checkbox` | `number` | `1` (true) or `0` (false) |
319
430
 
320
431
  ---
321
432
 
322
433
  ## Common Patterns
323
434
 
324
- ### Sum from Backlinks + Filter by Phase (Complete Example)
435
+ ### Sum Backlink Values
325
436
 
326
- **functionVariables config:**
327
- ```javascript
328
- {
329
- Data: {
330
- type: "<",
331
- data: [WorkflowIds.sales_activities, "meta"]
332
- },
333
- dl: {
334
- type: "<",
335
- data: [WorkflowIds.sales_activities, FieldIds.deadline]
336
- },
337
- done: {
338
- type: "?",
339
- data: [WorkflowIds.sales_activities, PhaseIds.done]
340
- },
341
- archive: {
342
- type: "?",
343
- data: [WorkflowIds.sales_activities, PhaseIds.archive]
344
- }
437
+ ```typescript
438
+ interface Dependencies {
439
+ goals: Array<number>;
345
440
  }
346
- ```
347
441
 
348
- **Function (find latest deadline from done/archived activities):**
349
- ```javascript
350
- function latestCompletedDeadline(dep) {
351
- const data = dep.Data || [];
352
- const deadlines = dep.dl || [];
353
- const donePhase = dep.done;
354
- const archivePhase = dep.archive;
355
-
356
- const arr = [];
357
- for (let i = 0; i < data.length; i++) {
358
- const item = data[i];
359
- if ((item.phase === donePhase || item.phase === archivePhase) && deadlines[i]) {
360
- arr.push(deadlines[i]);
361
- }
442
+ export function total_goals_XXX(dep: Dependencies): number {
443
+ const goals = dep.goals || [];
444
+ let sum = 0;
445
+ for (let i = 0; i < goals.length; i++) {
446
+ sum += Number(goals[i]) || 0;
362
447
  }
363
-
364
- if (arr.length === 0) return null;
365
- return Math.max(...arr);
448
+ return sum;
366
449
  }
367
450
  ```
368
451
 
369
- ### Sum Active Only
452
+ ### Sum Backlinks Filtered by Phase
453
+
454
+ ```typescript
455
+ interface Dependencies {
456
+ prices: Array<number>;
457
+ rowMeta: Array<ActivityMeta>;
458
+ activePhase: string;
459
+ }
370
460
 
371
- ```javascript
372
- function sumActiveOnly(dep) {
373
- const prices = dep['Total price'] || [];
374
- const data = dep['Data'] || [];
375
- const activePhase = dep['Active'];
461
+ export function sum_active_XXX(dep: Dependencies): number {
462
+ const prices = dep.prices || [];
463
+ const rowMeta = dep.rowMeta || [];
464
+ const activePhase = dep.activePhase;
376
465
 
377
466
  let total = 0;
378
467
  for (let i = 0; i < prices.length; i++) {
379
- if (data[i] && data[i].phase === activePhase) {
468
+ if (rowMeta[i] && rowMeta[i].phase === activePhase) {
380
469
  total += Number(prices[i]) || 0;
381
470
  }
382
471
  }
@@ -384,127 +473,182 @@ function sumActiveOnly(dep) {
384
473
  }
385
474
  ```
386
475
 
387
- ### Group by Phase
476
+ ### Composite Rating from Multiple Workflows
388
477
 
389
- ```javascript
390
- function groupByPhase(dep) {
391
- const prices = dep['Total price'] || [];
392
- const data = dep['Data'] || [];
478
+ ```typescript
479
+ interface Dependencies {
480
+ activePhase: string;
481
+ goals: Array<number>;
482
+ assists: Array<number>;
483
+ injuryMeta: Array<ActivityMeta>;
484
+ }
393
485
 
394
- const result = {};
395
- for (let i = 0; i < prices.length; i++) {
396
- const phaseName = data[i] ? data[i].phaseName : 'Unknown';
397
- const price = Number(prices[i]) || 0;
398
- result[phaseName] = (result[phaseName] || 0) + price;
486
+ export function player_rating_XXX(dep: Dependencies): number {
487
+ const GOAL_WEIGHT = 2;
488
+ const INJURY_PENALTY = 5;
489
+
490
+ const goals = dep.goals || [];
491
+ const assists = dep.assists || [];
492
+ const injuryMeta = dep.injuryMeta || [];
493
+ const activePhase = dep.activePhase || null;
494
+
495
+ let totalGoals = 0;
496
+ for (let i = 0; i < goals.length; i++) {
497
+ totalGoals += Number(goals[i]) || 0;
498
+ }
499
+
500
+ let totalAssists = 0;
501
+ for (let i = 0; i < assists.length; i++) {
502
+ totalAssists += Number(assists[i]) || 0;
399
503
  }
400
- return JSON.stringify(result);
504
+
505
+ let activeInjuryCount = 0;
506
+ for (let i = 0; i < injuryMeta.length; i++) {
507
+ const meta = injuryMeta[i];
508
+ if (meta && activePhase && meta.phase === activePhase) {
509
+ activeInjuryCount++;
510
+ }
511
+ }
512
+
513
+ return (totalGoals * GOAL_WEIGHT + totalAssists) - (activeInjuryCount * INJURY_PENALTY);
401
514
  }
402
515
  ```
403
516
 
404
- ### Exclude Multiple Phases
517
+ ### Per-90-Minutes Normalization
518
+
519
+ ```typescript
520
+ interface Dependencies {
521
+ goals: number;
522
+ minutesPlayed: number;
523
+ }
405
524
 
406
- ```javascript
407
- function sumExcludingCancelled(dep) {
408
- const prices = dep['Total price'] || [];
409
- const data = dep['Data'] || [];
410
- const cancelledPhase = dep['Cancelled'];
411
- const deletedPhase = dep['Deleted'];
525
+ export function goals_per_90_XXX(dep: Dependencies): number {
526
+ const goals = Number(dep.goals) || 0;
527
+ const minutes = Number(dep.minutesPlayed) || 0;
412
528
 
413
- let total = 0;
414
- for (let i = 0; i < prices.length; i++) {
415
- const phase = data[i] ? data[i].phase : null;
416
- if (phase === cancelledPhase || phase === deletedPhase) continue;
417
- total += Number(prices[i]) || 0;
418
- }
419
- return total;
529
+ if (minutes === 0) return 0;
530
+ return Math.round((goals / minutes) * 90 * 100) / 100;
531
+ }
532
+ ```
533
+
534
+ ### Same-Activity Field Sum
535
+
536
+ ```typescript
537
+ interface Dependencies {
538
+ yellowCards: number;
539
+ redCards: number;
540
+ }
541
+
542
+ export function total_cards_XXX(dep: Dependencies): number {
543
+ return (Number(dep.yellowCards) || 0) + (Number(dep.redCards) || 0);
420
544
  }
421
545
  ```
422
546
 
423
547
  ### Conditional Text
424
548
 
425
- ```javascript
426
- function status(dep) {
549
+ ```typescript
550
+ interface Dependencies {
551
+ dueDate: number;
552
+ }
553
+
554
+ export function status_XXX(dep: Dependencies): string {
427
555
  if (!dep.dueDate) return "No due date";
428
556
  if (dep.dueDate < Date.now()) return "Overdue";
429
557
  return "On track";
430
558
  }
431
559
  ```
432
560
 
433
- ### Forward Link Value
561
+ ### Exclude Multiple Phases
434
562
 
435
- ```javascript
436
- // Get value from linked activity
437
- functionVariables: {
438
- customerName: {
439
- type: ">",
440
- data: [FieldIds.customer_link, Customers_FieldIds.name]
563
+ ```typescript
564
+ interface Dependencies {
565
+ prices: Array<number>;
566
+ rowMeta: Array<ActivityMeta>;
567
+ cancelledPhase: string;
568
+ deletedPhase: string;
569
+ }
570
+
571
+ export function sum_excluding_cancelled_XXX(dep: Dependencies): number {
572
+ const prices = dep.prices || [];
573
+ const rowMeta = dep.rowMeta || [];
574
+
575
+ let total = 0;
576
+ for (let i = 0; i < prices.length; i++) {
577
+ const phase = rowMeta[i] ? rowMeta[i].phase : null;
578
+ if (phase === dep.cancelledPhase || phase === dep.deletedPhase) continue;
579
+ total += Number(prices[i]) || 0;
441
580
  }
581
+ return total;
442
582
  }
443
583
  ```
444
584
 
445
- ### Emoji Based on Own Phase
585
+ ### Emoji Name Function Based on Own Phase
446
586
 
447
- ```javascript
448
- // In name function: use activity's own phase, not parent workflow phase
449
- functionVariables: {
450
- myMeta: {
451
- type: "=",
452
- data: ["meta"] // Returns this activity's metadata
453
- },
454
- donePhase: {
455
- type: "?",
456
- data: [WorkflowIds.this_workflow, PhaseIds.done]
457
- }
587
+ ```typescript
588
+ interface Dependencies {
589
+ myMeta: ActivityMeta;
590
+ donePhase: string;
591
+ activityName: string;
458
592
  }
459
593
 
460
- // In name function
461
- function nameWithEmoji(dep) {
594
+ export function name_with_status_XXX(dep: Dependencies): string {
462
595
  const myPhase = dep.myMeta ? dep.myMeta.phase : null;
463
- const donePhase = dep.donePhase;
464
-
465
- const prefix = (myPhase === donePhase) ? "✅ " : "🔄 ";
466
- return prefix + dep.activityName;
596
+ const prefix = (myPhase === dep.donePhase) ? "done " : "";
597
+ return prefix + (dep.activityName || "Unnamed");
467
598
  }
468
599
  ```
469
600
 
470
- **Key:** Compare against the activity's OWN phase (from `"meta"` metadata), not the parent workflow's phase.
471
-
472
601
  ---
473
602
 
474
603
  ## Critical Rules
475
604
 
476
- 1. **ES6 JavaScript only** - No TypeScript syntax in function body
477
- 2. **Never return null/undefined** - Always return valid typed value
478
- 3. **Handle null inputs** - Use `|| 0` or `|| ""` defaults
479
- 4. **Same inputs = same outputs** - No randomness, deterministic
480
- 5. **Helpers inside function** - Define helper functions in function body
481
- 6. **Use enums** - Never hardcode field/workflow IDs
482
- 7. **updateMany single-workflow rule** - `v3.activity.updateMany` targets one workflow per call. For cascading updates across multiple workflows, create separate function fields per target workflow. Don't mix activities from different workflows in one payload
483
- 8. **Double-wrapped array for generic CRUD** - When outputting a payload for the monolith `POST /api/update-activities` handler, use `[[{_id, phaseId}, ...]]` (double-wrapped). The handler spreads parsed JSON as `updateMany(...args)`, so the first arg must be the array. Single-wrapped `[{_id, phaseId}]` causes "must be an array" validation error
484
- 9. **Empty string validation** - Hailer rejects empty string `''` with "value is not allowed to be empty". Always return `'[]'` or a valid non-empty string as default from function fields
485
- 10. **Metadata aliases** - `"data"` and `"meta"` are interchangeable aliases. `type: "="` with `data: ["meta"]` (or `data: ["data"]`) returns this activity's metadata object. `type: "<"` with `data: [workflowId, "meta"]` (or `data: [workflowId, "data"]`) returns array of metadata objects for linked activities. Prefer `"meta"` for clarity.
605
+ 1. **TypeScript files, JavaScript runtime** — Write `.ts` with interfaces for dev safety. Runtime strips TS and runs ES6 JS in isolated-vm. No TS-only syntax in function bodies.
606
+ 2. **Never return `null`/`undefined` from numeric fields** Always return `0` as default. Hailer rejects empty string `''` with "value is not allowed to be empty" — return `'[]'` or valid non-empty string for text fields.
607
+ 3. **Handle null inputs defensively** Use `Number(x) || 0` for numbers, `|| ""` for strings, `|| []` for arrays.
608
+ 4. **Deterministic** — Same inputs must produce same outputs. No `Math.random()`, no non-deterministic behavior.
609
+ 5. **No unused dependencies** Every `functionVariables` entry must be used in the function. Unused deps cause wasted cross-workflow lookups.
610
+ 6. **Correct types in Dependencies** Match the source field type exactly. A numeric source field is `number`, not `string`.
611
+ 7. **Return type is never `any`** Always declare the actual return type (`number`, `string`).
612
+ 8. **Use enums for IDs** Never hardcode workflow/field/phase IDs. Import from `workspace/enums.ts`.
613
+ 9. **`fields-push:force` only** Non-force push skips `functionVariables` changes.
614
+ 10. **Pull after push** Always `npm run pull` to get server-assigned IDs.
615
+ 11. **Magic numbers as constants** — Weights, multipliers, penalties should be named `const` at the top of the function.
616
+ 12. **`linkedfrom` fields don't work in isolated-vm** — Use `<` backlink dependencies instead.
486
617
 
487
618
  ---
488
619
 
489
620
  ## Common Mistakes
490
621
 
491
- | Wrong | Right |
492
- |-------|-------|
493
- | `var arr = dep.arr` | `const arr = dep.arr \|\| []` |
494
- | `arr[i] * 2` | `(Number(arr[i]) \|\| 0) * 2` |
495
- | `data[i].phase` | `data[i] ? data[i].phase : null` |
496
- | Assuming arrays match across workflows | Only same-workflow arrays are parallel |
497
- | Hardcoding phase IDs | Use `?` static variables with enums |
622
+ | Wrong | Right | Why |
623
+ |-------|-------|-----|
624
+ | `dep.arr` without default | `dep.arr \|\| []` | Backlinks can be undefined |
625
+ | `arr[i] * 2` | `(Number(arr[i]) \|\| 0) * 2` | Array values can be null |
626
+ | `meta.phase` | `meta ? meta.phase : null` | Metadata entries can be null |
627
+ | `minutesPlayed: string` | `minutesPlayed: number` | Source is numeric field |
628
+ | `): any` | `): number` | Explicit return type |
629
+ | Unused dep in interface | Remove from both interface AND functionVariables | Wasted lookups |
630
+ | `dep.salary` declared, never used | Don't declare it | Dead dependency |
631
+ | Hardcoded `"627d14..."` | Use `PhaseIds.active` enum | Brittle, non-portable |
632
+ | `npm run fields-push` | `npm run fields-push:force` | Non-force skips functionVariables |
633
+ | Assuming cross-workflow array alignment | Only same-workflow `<` arrays are parallel | Different workflows = independent arrays |
498
634
 
499
635
  ---
500
636
 
501
637
  ## Checklist
502
638
 
503
- Before creating a function with backlinks:
504
-
505
- - [ ] All `<` arrays have `|| []` default
506
- - [ ] Individual array values use `Number(x) || 0`
507
- - [ ] Data metadata access has null check
508
- - [ ] Phase comparisons use `?` static variables
509
- - [ ] Using enums from workspace for all IDs
510
- - [ ] Tested with Vitest before push
639
+ Before pushing a function field:
640
+
641
+ - [ ] `Dependencies` interface types match source field types exactly
642
+ - [ ] No unused dependencies (every functionVariables entry is used)
643
+ - [ ] Return type is explicit (`number`, `string`), never `any`
644
+ - [ ] All `<` arrays default with `|| []`
645
+ - [ ] All numeric values wrapped with `Number(x) || 0`
646
+ - [ ] All metadata access null-checked (`meta ? meta.phase : null`)
647
+ - [ ] Phase comparisons use `?` static variables with enums
648
+ - [ ] All IDs come from enums, never hardcoded
649
+ - [ ] Magic numbers extracted to named constants
650
+ - [ ] Function name matches `@function:` reference in fields.ts
651
+ - [ ] Exported from `functions/index.ts`
652
+ - [ ] Tested with Vitest (happy path + null/undefined inputs)
653
+ - [ ] Using `fields-push:force` (not `fields-push`)
654
+ - [ ] Will run `npm run pull` after push