@hailer/mcp 1.3.14 → 1.3.23

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 (57) hide show
  1. package/.claude/skills/create-and-publish-app/SKILL.md +44 -56
  2. package/.claude/skills/hailer-app-builder/SKILL.md +414 -970
  3. package/.claude/skills/hailer-app-primitives/SKILL.md +742 -0
  4. package/.claude/skills/hailer-apps-pictures/SKILL.md +191 -87
  5. package/.claude/skills/hailer-design-patterns/SKILL.md +317 -0
  6. package/.claude/skills/hailer-design-system/SKILL.md +202 -149
  7. package/.claude/skills/hailer-workflow-archetypes/SKILL.md +301 -0
  8. package/.claude/skills/insight-join-patterns/SKILL.md +313 -0
  9. package/.claude/skills/publish-hailer-app/SKILL.md +211 -0
  10. package/.claude/skills/sdk-activity-patterns/SKILL.md +257 -105
  11. package/.claude/skills/sdk-function-fields/SKILL.md +253 -492
  12. package/.claude/skills/sdk-insight-calculations/SKILL.md +364 -0
  13. package/.claude/skills/sdk-insight-queries/SKILL.md +192 -397
  14. package/.claude/skills/sdk-ws-config-skill/SKILL.md +265 -720
  15. package/.claude/skills/tool-response-verification/SKILL.md +143 -0
  16. package/CLAUDE.md +50 -19
  17. package/dist/bot/bot.d.ts.map +1 -1
  18. package/dist/bot/bot.js +26 -34
  19. package/dist/bot/bot.js.map +1 -1
  20. package/dist/bot/services/helper-prompt.d.ts +1 -1
  21. package/dist/bot/services/helper-prompt.d.ts.map +1 -1
  22. package/dist/bot/services/helper-prompt.js +62 -82
  23. package/dist/bot/services/helper-prompt.js.map +1 -1
  24. package/dist/bot/services/system-prompt.js +4 -4
  25. package/dist/config.d.ts +1 -0
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +6 -0
  28. package/dist/config.js.map +1 -1
  29. package/dist/mcp/hailer-rpc.d.ts +1 -0
  30. package/dist/mcp/hailer-rpc.d.ts.map +1 -1
  31. package/dist/mcp/hailer-rpc.js +4 -0
  32. package/dist/mcp/hailer-rpc.js.map +1 -1
  33. package/dist/mcp/tools/activity.d.ts.map +1 -1
  34. package/dist/mcp/tools/activity.js +8 -2
  35. package/dist/mcp/tools/activity.js.map +1 -1
  36. package/dist/mcp/tools/app-core.d.ts.map +1 -1
  37. package/dist/mcp/tools/app-core.js +6 -3
  38. package/dist/mcp/tools/app-core.js.map +1 -1
  39. package/dist/mcp/tools/app-marketplace.d.ts.map +1 -1
  40. package/dist/mcp/tools/app-marketplace.js +5 -1
  41. package/dist/mcp/tools/app-marketplace.js.map +1 -1
  42. package/dist/mcp/webhook-handler.d.ts.map +1 -1
  43. package/dist/mcp/webhook-handler.js +27 -0
  44. package/dist/mcp/webhook-handler.js.map +1 -1
  45. package/dist/public-chat/graduate.d.ts.map +1 -1
  46. package/dist/public-chat/graduate.js +153 -53
  47. package/dist/public-chat/graduate.js.map +1 -1
  48. package/dist/public-chat/rate-limit.js +1 -1
  49. package/dist/public-chat/rate-limit.js.map +1 -1
  50. package/dist/public-chat/studio-prewarm.js +2 -2
  51. package/dist/public-chat/studio-prewarm.js.map +1 -1
  52. package/dist/public-chat/system-prompt.d.ts.map +1 -1
  53. package/dist/public-chat/system-prompt.js +41 -25
  54. package/dist/public-chat/system-prompt.js.map +1 -1
  55. package/package.json +1 -1
  56. package/.claude/skills/hailer-project-protocol/SKILL.md +0 -398
  57. package/.opencode/package-lock.json +0 -117
@@ -1,16 +1,10 @@
1
1
  ---
2
2
  name: sdk-function-fields
3
- description: Complete guide to creating calculated function fields in Hailer - workflow, variable types, patterns
4
- version: 2.0.0
5
- triggers:
6
- - function field
7
- - calculated field
8
- - formula field
9
- - backlinks
10
- - phase filtering
11
- - Data metadata
3
+ description: Complete guide to creating calculated function fields in Hailer variable types (=, >, <, ?), field data formats, backlink arrays, activity metadata, editable fields, TypeScript gotchas, and push workflow. Load when creating, debugging, or updating any function field. Also use when you see code 191 "does not compile", null return values, or questions about cross-workflow calculations.
12
4
  ---
13
5
 
6
+ <!-- augmentation-guard: shrink-ok -->
7
+
14
8
  # Function Fields
15
9
 
16
10
  Complete guide for creating calculated fields that auto-update when dependencies change.
@@ -20,635 +14,402 @@ Complete guide for creating calculated fields that auto-update when dependencies
20
14
  ## Overview
21
15
 
22
16
  Function fields compute values from other fields. Examples:
23
- - Total = Quantity x Unit Price
17
+ - Total = Quantity × Unit Price
24
18
  - Days Until Due = Due Date - Today
25
19
  - Invoice Total = Sum of line item prices (filtered by phase)
26
20
 
27
21
  ---
28
22
 
29
- ## Step-by-Step Workflow
23
+ ## Canonical Fixture
30
24
 
31
- ### 1. Pull Latest
32
- ```bash
33
- npm run pull
25
+ Pinned to testspace, workflow `Date format test` (`_id: 6a16d64ed9d2a360ab10420d`), activity `test-active-probe` (`_id: 6a16d6f1d9d2a360ab1047de`). Gold copy — paste from this block.
26
+
27
+ ```typescript
28
+ // selfMeta_DATA — full metadata object via data: ["data"]
29
+ functionVariables: { selfMeta_DATA: { type: "=", data: ["data"] } }
30
+ // body: return JSON.stringify(dep.selfMeta_DATA)
31
+ // returned: {"_id":"6a16d6f1d9d2a360ab1047de","name":"test","process":"6a16d64ed9d2a360ab10420d","phase":"6a16d64ed9d2a360ab10420c","created":1779881713567,"updated":1779906718754,"completed":null,"processName":"Date format test","phaseName":"New Phase","sequence":1}
32
+
33
+ // selfMeta_META — wrong magic key silently returns null
34
+ functionVariables: { selfMeta_META: { type: "=", data: ["meta"] } }
35
+ // returned: "null"
36
+
37
+ // selfPhaseId — property accessor via data: ["data", "<key>"]
38
+ functionVariables: { selfPhaseId: { type: "=", data: ["data", "phase"] } }
39
+ // returned: "6a16d64ed9d2a360ab10420c"
34
40
  ```
35
41
 
36
- ### 2. Add Field Definition (fields.ts)
42
+ **Empirical observations (2026-05-27):**
43
+ - `data: ["data"]` returns the 10-key metadata object
44
+ - `data: ["meta"]` returns `null` (silent — wrong magic key)
45
+ - `data: ["data", "<key>"]` accesses a specific property of the metadata object
46
+ - `data: ["data", "active"]` returns `null` despite REST showing `active` — dropped on the resolver path
47
+ - `created`/`updated` timestamps are **13-digit milliseconds**
37
48
 
38
- Add to the `fields` array in `workspace/[Workflow]_[id]/fields.ts`:
49
+ ---
39
50
 
40
- ```typescript
41
- // NEW field — omit _id, server assigns it after push
42
- {
43
- data: [],
44
- function: "@function:total_cost_XXX", // Must match exported function name
45
- functionEnabled: true,
46
- functionVariables: {
47
- quantity: {
48
- type: "=",
49
- data: [FieldIds.quantity]
50
- },
51
- unitPrice: {
52
- type: "=",
53
- data: [FieldIds.unit_price]
54
- }
55
- },
56
- inviteToDiscussionOnChange: false,
57
- key: "totalCost",
58
- label: "Total Cost",
59
- required: false,
60
- type: "numeric"
61
- }
62
- ```
51
+ ## Directory Structure
63
52
 
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.)
53
+ ```
54
+ workspace/[Workflow]_[id]/
55
+ ├── fields.ts # Add function field definition here
56
+ ├── functions/
57
+ │ ├── index.ts # Export all functions
58
+ │ ├── total_cost_abc.ts # Individual function file filename encodes last 3 hex chars of field _id
59
+ │ └── risk_level_def.ts
60
+ └── main.test.ts # Test functions with Vitest
61
+ ```
70
62
 
71
- **For new fields:** Omit `_id`. The server assigns it after push. Run `npm run pull` after push to get the server-assigned ID.
63
+ **Filename convention:** `<slug>_<xyz>.ts` the last 3 hex chars come from the field's `_id`. Confirm by the field's `_id`, not the app column header.
72
64
 
73
- **For existing fields:** Keep the `_id` from enums.
65
+ ---
74
66
 
75
- ### 3. Create Function File
67
+ ## Step-by-Step Workflow
76
68
 
77
- **Naming convention:** `{functionName}_{last3HexOfFieldId}.ts`
69
+ ### 1. Add Field Definition (fields.ts)
78
70
 
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
71
+ ```typescript
72
+ {
73
+ label: "Total Cost",
74
+ type: "numeric",
75
+ key: "total_cost",
76
+ function: "@function:totalCost",
77
+ functionEnabled: true,
78
+ editable: false,
79
+ functionVariables: {
80
+ quantity: { type: "=", data: [FieldIds.quantity] },
81
+ unitPrice: { type: "=", data: [FieldIds.unit_price] }
82
+ }
83
+ }
84
+ ```
81
85
 
82
- Create `workspace/[Workflow]_[id]/functions/total_cost_XXX.ts`:
86
+ ### 2. Create Function File
83
87
 
84
88
  ```typescript
85
89
  /**
86
- * Field function for: Total Cost
87
- * Field name: Total Cost
88
- * Field ID: [assigned after push]
90
+ * total_cost Multiplies quantity by unit price to produce line-item total
89
91
  */
90
-
91
- interface Dependencies {
92
- quantity: number;
93
- unitPrice: number;
94
- }
95
-
96
- export function total_cost_XXX(dep: Dependencies): number {
92
+ export function totalCost(dep): any {
97
93
  const qty = Number(dep.quantity) || 0;
98
94
  const price = Number(dep.unitPrice) || 0;
99
95
  return qty * price;
100
96
  }
101
97
  ```
102
98
 
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
110
-
111
- Edit `workspace/[Workflow]_[id]/functions/index.ts`:
112
-
113
- ```typescript
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";
118
- ```
119
-
120
- ### 5. Test with Vitest
99
+ ### 3. Export Function
121
100
 
122
101
  ```typescript
123
- // workspace/[Workflow]_[id]/functions/total_cost_XXX.test.ts
124
- import { total_cost_XXX } from "./total_cost_XXX";
125
-
126
- describe('total_cost_XXX', () => {
127
- it('multiplies quantity by unit price', () => {
128
- expect(total_cost_XXX({ quantity: 5, unitPrice: 10 })).toBe(50);
129
- });
130
-
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);
138
- });
139
- });
102
+ // workspace/[Workflow]_[id]/functions/index.ts
103
+ export { totalCost } from './totalCost';
140
104
  ```
141
105
 
142
- Run: `npm test`
143
-
144
- ### 6. Push
106
+ ### 4. Push
145
107
 
146
108
  ```bash
147
109
  npm run fields-push:force
148
110
  ```
149
111
 
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.
112
+ **Two-batch field creation:** new function fields sometimes need two push cycles:
113
+ 1. Create the plain field (no function logic) → `fields-push:force` → `pull` (gets server ID)
114
+ 2. Add `functionEnabled`, `functionVariables`, and function code → `fields-push:force`
151
115
 
152
- ### 7. Pull to Get Server IDs
116
+ **Pull deletes unpushed function files.** `npm run pull` regenerates the entire `functions/` directory from server state — locally written `.ts` files not yet pushed are silently deleted. Push all function changes first; if a mid-work pull is unavoidable, `git stash -u` first.
153
117
 
154
- ```bash
155
- npm run pull
156
- ```
118
+ ---
157
119
 
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
120
+ ## Variable Types Reference
162
121
 
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.
122
+ | Type | Name | data Array | Returns |
123
+ |------|------|------------|---------|
124
+ | `=` | Current field | `[FieldId]` | Single value |
125
+ | `>` | Forward link | `[linkedId, fieldId, suffix?, key?]` (1–4 elements) | Single value |
126
+ | `<` | Backlink | `[WorkflowId, TargetFieldId]` | **Array** |
127
+ | `?` | Static | `[WorkflowId, PhaseId]` | Phase ID string |
164
128
 
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 |
129
+ **CRITICAL:** Forward links use `LinkFieldId` (the activitylink field ID). Backlinks use `WorkflowId`.
174
130
 
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.
131
+ `suffix` (third element): `"data"` accesses the linked activity's metadata pseudo-key. Use `"data"`, not `"meta"` `"meta"` silently returns null.
176
132
 
177
133
  ---
178
134
 
179
- ## Quality Rules
180
-
181
- These rules prevent the bugs found in real function fields. Follow them exactly.
182
-
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` |
135
+ ## Field Data Formats in Functions
198
136
 
199
- **Wrong:** Typing a numeric source field as `string` (causes silent JS coercion)
200
- **Right:** Always use the actual source field's type
137
+ | Field Type | Returns | Example |
138
+ |------------|---------|---------|
139
+ | `text`, `textarea` | `string` | `"Hello world"` |
140
+ | `numeric`, `numericunit` | `number` | `42.5` |
141
+ | `date`, `datetime` | `number` (ms timestamp) | `1730937600000` |
142
+ | `daterange`, `datetimerange` | `{ start: number, end: number }` | `{ start: 1730937600000, end: 1731024000000 }` |
143
+ | `time` | `number` (ms timestamp, includes date!) | `1765863000000` |
144
+ | `timerange` | `{ start: number, end: number }` (ms, includes date!) | — |
145
+ | `textpredefinedoptions` | `string` | `"High"` |
146
+ | `users` | `object {_id, firstname, lastname}` | Resolution happens server-side before isolated-vm runs |
147
+ | `teams` | `string` (ID) | Raw team ID string |
148
+ | `activitylink` | `object {_id, name}` | Use `dep.myLink._id === otherId` for comparison |
149
+ | `country` | `string` (ISO code) | `"FI"` |
150
+ | `numeric` + `modifier.checkbox` | `number` | `1` (true) or `0` (false) |
201
151
 
202
- ### No Unused Dependencies
152
+ ---
203
153
 
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.
154
+ ## Editable Fields (Freeze After First Calculation)
205
155
 
206
- **Wrong:**
207
156
  ```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);
157
+ {
158
+ _id: Fields.qty_per_parent,
159
+ editable: true, // Calculates initially; protected once user has set a value
160
+ functionEnabled: true,
161
+ function: "@function:calculate_ratio"
216
162
  }
217
163
  ```
218
164
 
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;
165
+ `editable: false` (default) Recalculates every time dependencies change.
166
+ `editable: true` — Stored values win; FF only runs when the field is empty.
243
167
 
244
- // Array iterationuse 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
- ```
168
+ **Write acceptance confirmed (Timo, platform owner, 2026-06-14):** all interfaces MCP `create_activity`/`update_activity`, REST `v3.activity.createMany`/`updateMany`, app-sdk, and CLI — accept writes to `editable: true` function fields.
249
169
 
250
- ### Runtime Is Plain JavaScript
170
+ **Writability test is `editable: true`, NOT `functionEnabled === false`.** A field with `functionEnabled: true` AND `editable: true` is both computed AND writable.
251
171
 
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
172
+ **Async backfill after fields-push:** when a new editable function field backfills from a same-activity source, the server-side recompute is asynchronous and can take several minutes. Don't conclude backfill failed from an immediate empty read.
257
173
 
258
174
  ---
259
175
 
260
- ## Variable Types Reference
261
-
262
- ### `=` — Same Activity Field
263
-
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)
176
+ ## Backlink Arrays (`<`)
280
177
 
281
- Returns **arrays** multiple activities can link to this one.
178
+ Backlinks return **arrays** because multiple activities can link to this one.
282
179
 
283
- ```typescript
284
- // fields.ts
285
- functionVariables: {
286
- prices: {
287
- type: "<",
288
- data: [WorkflowIds.invoice_rows, InvoiceRows_FieldIds.total_price]
289
- }
180
+ ```javascript
181
+ // functionVariables config
182
+ {
183
+ prices: { type: "<", data: [WorkflowIds.invoice_rows, InvoiceRows_FieldIds.total_price] }
290
184
  }
291
185
 
292
- // function ALWAYS default to empty array
186
+ // In function - ALWAYS default to empty array
293
187
  const prices = dep.prices || [];
294
188
  let total = 0;
295
189
  for (let i = 0; i < prices.length; i++) {
296
190
  total += Number(prices[i]) || 0;
297
191
  }
192
+ return total;
298
193
  ```
299
194
 
300
- ### `>` Forward Link (This Activity Links TO Another)
195
+ **Parallel Array Guarantee:** When you have multiple `<` variables from the **SAME source workflow**, they are guaranteed same length and same order. Index 0 in all arrays = same activity. This enables safe parallel iteration.
301
196
 
302
- Read a field value from a linked activity.
197
+ **`<` backlink aggregates recompute IN REAL TIME when a child is added, edited, or deleted** (confirmed by Timo, Hailer platform owner, 2026-06-22).
303
198
 
304
- ```typescript
305
- // fields.ts
306
- functionVariables: {
307
- customerName: {
308
- type: ">",
309
- data: [FieldIds.customer_link, Customers_FieldIds.name]
310
- }
311
- }
312
-
313
- // function — returns scalar value (single linked activity)
314
- const name = dep.customerName || "";
315
- ```
316
-
317
- ### `?` — Static Value (Phase ID for Comparison)
318
-
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]
327
- }
328
- }
329
-
330
- // function — returns phase ID string
331
- const activePhase = dep.activePhase; // "627d14339381d6077ab90ce9"
332
- if (meta.phase === activePhase) { ... }
333
- ```
199
+ **Don't chain one FF on another for `<` backlink VALUE aggregation.** Re-derive the backlink sum independently in each FF. Eval ordering is not guaranteed.
334
200
 
335
201
  ---
336
202
 
337
- ## Activity Metadata (`"meta"`)
203
+ ## Activity Metadata (`"data"`)
338
204
 
339
- The special `"meta"` value returns activity metadata. Works with all variable types.
205
+ **Critical:** use `"data"` as the magic key, NOT `"meta"` `"meta"` silently returns null.
340
206
 
341
- ### Own Metadata (`=`)
207
+ ### Current Activity Metadata (`=`)
342
208
 
343
- ```typescript
344
- functionVariables: {
345
- myMeta: { type: "=", data: ["meta"] }
209
+ ```javascript
210
+ {
211
+ myMeta: { type: "=", data: ["data"] }
346
212
  }
347
- // dep.myMeta { _id, name, phase, phaseName, created, updated, ... }
213
+ // Available meta fields (10 keys): _id, name, process, phase, created, updated,
214
+ // completed, processName, phaseName, sequence
348
215
  ```
349
216
 
350
- ### Backlink Metadata (`<`)
217
+ `sequence` is a server-maintained auto-incrementing activity counter — usable for serial names like `T-YYYY-NNNN`.
351
218
 
352
- ```typescript
353
- functionVariables: {
354
- injuryMeta: { type: "<", data: [WorkflowIds.injuries, "meta"] }
355
- }
356
- // dep.injuryMeta → Array<ActivityMeta>
357
- ```
219
+ **`active` is absent at runtime** despite appearing in REST show_activity_by_id — dropped on the resolver path. Compare `dep.selfMeta.phase` against known endpoint phase IDs instead.
358
220
 
359
221
  ### Forward Link Metadata (`>`)
360
222
 
361
- ```typescript
362
- functionVariables: {
363
- customerMeta: { type: ">", data: [FieldIds.customer_link, "meta"] }
223
+ ```javascript
224
+ {
225
+ customerMeta: { type: ">", data: [FieldIds.customer_link, "data"] }
364
226
  }
365
- // dep.customerMeta → ActivityMeta
227
+ // In function
228
+ const customerPhase = dep.customerMeta ? dep.customerMeta.phaseName : 'Unknown';
366
229
  ```
367
230
 
368
- ### Metadata Structure
231
+ ### Backlink Metadata (`<`)
369
232
 
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
233
+ ```javascript
234
+ {
235
+ rowsMeta: { type: "<", data: [WorkflowIds.invoice_rows, "data"] }
236
+ }
237
+ // In function - returns ARRAY
238
+ const rows = dep.rowsMeta || [];
239
+ for (let i = 0; i < rows.length; i++) {
240
+ const phaseName = rows[i] ? rows[i].phaseName : 'Unknown';
383
241
  }
384
242
  ```
385
243
 
386
- **Note:** `"data"` and `"meta"` are interchangeable aliases. Prefer `"meta"` for clarity.
387
-
388
244
  ---
389
245
 
390
- ## Parallel Array Guarantee
246
+ ## Static Variables (`?`)
391
247
 
392
- **CRITICAL:** When you have multiple `<` (backlink) variables from the **SAME source workflow**, they are guaranteed to be:
393
- - Same length
394
- - Same order (index 0 in all arrays = same activity)
248
+ Static variables provide phase IDs for comparison.
395
249
 
396
- This enables safe parallel iteration:
397
-
398
- ```typescript
399
- const prices = dep.prices || []; // [41.76, 26.1, 153.47]
400
- const rowMeta = dep.rowMeta || []; // [{...}, {...}, {...}]
401
-
402
- // Index 0 in both = same source activity
403
- for (let i = 0; i < prices.length; i++) {
404
- const price = Number(prices[i]) || 0;
405
- const phaseName = rowMeta[i] ? rowMeta[i].phaseName : 'Unknown';
250
+ ```javascript
251
+ {
252
+ donePhase: { type: "?", data: [WorkflowIds.sales_activities, PhaseIds.done] }
406
253
  }
254
+ // In function
255
+ if (item.phase === dep.donePhase) { /* Activity is done */ }
407
256
  ```
408
257
 
409
- **Arrays from DIFFERENT source workflows are independent — never assume index alignment across workflows.**
410
-
411
- ---
412
-
413
- ## Field Data Formats in Functions
414
-
415
- What format does each field type return when used as a dependency?
416
-
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) |
430
-
431
258
  ---
432
259
 
433
260
  ## Common Patterns
434
261
 
435
- ### Sum Backlink Values
262
+ ### Sum from Backlinks
436
263
 
437
- ```typescript
438
- interface Dependencies {
439
- goals: Array<number>;
440
- }
441
-
442
- export function total_goals_XXX(dep: Dependencies): number {
443
- const goals = dep.goals || [];
264
+ ```javascript
265
+ function total(dep) {
266
+ const prices = dep.prices || [];
444
267
  let sum = 0;
445
- for (let i = 0; i < goals.length; i++) {
446
- sum += Number(goals[i]) || 0;
268
+ for (let i = 0; i < prices.length; i++) {
269
+ sum += Number(prices[i]) || 0;
447
270
  }
448
271
  return sum;
449
272
  }
450
273
  ```
451
274
 
452
- ### Sum Backlinks Filtered by Phase
453
-
454
- ```typescript
455
- interface Dependencies {
456
- prices: Array<number>;
457
- rowMeta: Array<ActivityMeta>;
458
- activePhase: string;
459
- }
275
+ ### Filter by Phase
460
276
 
461
- export function sum_active_XXX(dep: Dependencies): number {
462
- const prices = dep.prices || [];
463
- const rowMeta = dep.rowMeta || [];
464
- const activePhase = dep.activePhase;
277
+ ```javascript
278
+ function latestCompletedDeadline(dep) {
279
+ const data = dep.Data || [];
280
+ const deadlines = dep.dl || [];
281
+ const donePhase = dep.done;
282
+ const archivePhase = dep.archive;
465
283
 
466
- let total = 0;
467
- for (let i = 0; i < prices.length; i++) {
468
- if (rowMeta[i] && rowMeta[i].phase === activePhase) {
469
- total += Number(prices[i]) || 0;
284
+ const arr = [];
285
+ for (let i = 0; i < data.length; i++) {
286
+ const item = data[i];
287
+ if ((item.phase === donePhase || item.phase === archivePhase) && deadlines[i]) {
288
+ arr.push(deadlines[i]);
470
289
  }
471
290
  }
472
- return total;
291
+ if (arr.length === 0) return null;
292
+ return Math.max(...arr);
473
293
  }
474
294
  ```
475
295
 
476
- ### Composite Rating from Multiple Workflows
477
-
478
- ```typescript
479
- interface Dependencies {
480
- activePhase: string;
481
- goals: Array<number>;
482
- assists: Array<number>;
483
- injuryMeta: Array<ActivityMeta>;
484
- }
485
-
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;
296
+ ### Auto-numbering with `sequence`
494
297
 
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;
503
- }
298
+ ```javascript
299
+ // nameFunctionVariables
300
+ { selfMeta: { type: "=", data: ["data"] } }
504
301
 
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);
302
+ // nameFunction body
303
+ function myNameFn(dep) {
304
+ if (!dep.selfMeta) return null;
305
+ var year = new Date(dep.selfMeta.created).getFullYear();
306
+ var seq = String(dep.selfMeta.sequence).padStart(4, '0');
307
+ return 'T-' + year + '-' + seq;
514
308
  }
515
309
  ```
516
310
 
517
- ### Per-90-Minutes Normalization
311
+ `sequence` is monotonic across the workflow's whole lifetime — does NOT reset each year. Deletions can leave gaps.
518
312
 
519
- ```typescript
520
- interface Dependencies {
521
- goals: number;
522
- minutesPlayed: number;
523
- }
313
+ ---
524
314
 
525
- export function goals_per_90_XXX(dep: Dependencies): number {
526
- const goals = Number(dep.goals) || 0;
527
- const minutes = Number(dep.minutesPlayed) || 0;
315
+ ## Critical Rules
528
316
 
529
- if (minutes === 0) return 0;
530
- return Math.round((goals / minutes) * 90 * 100) / 100;
531
- }
532
- ```
317
+ 1. **ES6 JavaScript only** no TypeScript syntax in function body
318
+ 2. **Return null when no value** — Hailer rejects empty string `""` with ValidationError. Return `null` instead of `''`. Exception for date/number FFs feeding filters: return `null` not `0` (a `0` fallback makes every empty date a valid timestamp at 1970-01-01).
319
+ 3. **Handle null inputs** — use `|| 0` or `|| ""` defaults
320
+ 4. **Same inputs = same outputs** — no randomness, deterministic
321
+ 5. **Helpers inside function** — define helper functions in function body
322
+ 6. **Use enums** — never hardcode field/workflow IDs
323
+ 7. **Empty string validation** — always return `null` for no-value cases
324
+ 8. **Data metadata shorthand** — `type: "="` with `data: ["data"]` returns metadata. Use `"data"` not `"meta"`.
325
+ 9. **`linkedFrom()` API does not work in isolated-vm** — use `type: "<"` backlink variables instead
326
+ 10. **Return type in .ts file** — always use `: any` as the return type annotation (SDK regex only matches single-word return types)
327
+ 11. **Server rejects ALL TypeScript-only constructs:**
328
+ - Parameter type annotations: `function foo(dep: Dependencies)` → code 191
329
+ - Union types in local vars: `let maxDate: number | null = null;` → code 191
330
+ - Typed array literals: `const dates: number[] = [];` → code 191
331
+ - `as Type` casts in body → code 191
332
+ - Safe: `// @ts-nocheck` at file top, untyped `let`/`const`, JSDoc comments
333
+
334
+ Safe pattern:
335
+ ```ts
336
+ // ✗ rejected — param type annotation
337
+ export function calcTotal(dep: Dependencies): any { ... }
338
+
339
+ // ✓ safe
340
+ export function calcTotal(dep): any { ... }
341
+
342
+ let maxDate = 0; // sentinel — timestamps always > 0
343
+ return maxDate || null;
344
+ ```
345
+
346
+ 12. **Same-value dep write does NOT trigger recalc** — Hailer skips re-evaluation when dependency write contains the same value as stored. Toggle the dep through a different value (e.g., `null`) and back to force re-evaluation.
347
+ 13. **Don't use `type` or `data` as functionVariables key names** — they collide with the variable schema's own properties. Use descriptive names.
533
348
 
534
- ### Same-Activity Field Sum
349
+ ---
535
350
 
536
- ```typescript
537
- interface Dependencies {
538
- yellowCards: number;
539
- redCards: number;
540
- }
351
+ ## Naming Collisions in functionVariables Keys
541
352
 
542
- export function total_cards_XXX(dep: Dependencies): number {
543
- return (Number(dep.yellowCards) || 0) + (Number(dep.redCards) || 0);
544
- }
545
- ```
353
+ Don't use `type` or `data` as a key name. They collide with the variable schema's properties — the function body receives `undefined` or malformed `dep.type`/`dep.data`.
546
354
 
547
- ### Conditional Text
355
+ ---
548
356
 
549
- ```typescript
550
- interface Dependencies {
551
- dueDate: number;
552
- }
357
+ ## Recompute Triggers
553
358
 
554
- export function status_XXX(dep: Dependencies): string {
555
- if (!dep.dueDate) return "No due date";
556
- if (dep.dueDate < Date.now()) return "Overdue";
557
- return "On track";
558
- }
559
- ```
359
+ **Field-definition change forces full recompute:** A change to `functionVariables` or function body triggers the backend to recompute across ALL existing activities. A plain push with no diff is deep-equal-skipped.
560
360
 
561
- ### Exclude Multiple Phases
361
+ **`>` (forward-link) FFs do NOT auto-compute on `v3.activity.createMany`** — they stay null on bulk-seeded data. Trigger a recompute via a field-definition edit.
562
362
 
563
- ```typescript
564
- interface Dependencies {
565
- prices: Array<number>;
566
- rowMeta: Array<ActivityMeta>;
567
- cancelledPhase: string;
568
- deletedPhase: string;
569
- }
363
+ **`<` backlinks recompute in real time** when a child is added, edited, or deleted.
570
364
 
571
- export function sum_excluding_cancelled_XXX(dep: Dependencies): number {
572
- const prices = dep.prices || [];
573
- const rowMeta = dep.rowMeta || [];
365
+ **Multi-hop FFs** (FF that routes through another FF as intermediate) do NOT recompute on bulk `createMany` — this is a separate confirmed gap.
574
366
 
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;
580
- }
581
- return total;
582
- }
583
- ```
367
+ ---
584
368
 
585
- ### Emoji Name Function Based on Own Phase
369
+ ## Bulk Function-Field Push: Use a NOT_INHERITANCE Exclusion Set
586
370
 
587
- ```typescript
588
- interface Dependencies {
589
- myMeta: ActivityMeta;
590
- donePhase: string;
591
- activityName: string;
592
- }
371
+ When writing a script that auto-discovers function fields and pushes them all with a shared shape, maintain an explicit exclusion set to avoid pushing fields with custom logic using the batch shape (would corrupt them silently).
593
372
 
594
- export function name_with_status_XXX(dep: Dependencies): string {
595
- const myPhase = dep.myMeta ? dep.myMeta.phase : null;
596
- const prefix = (myPhase === dep.donePhase) ? "done " : "";
597
- return prefix + (dep.activityName || "Unnamed");
373
+ ```typescript
374
+ const NOT_PUSH_TARGET = new Set([
375
+ "tilavaraus_phase_sync_fc8",
376
+ "yhteyshenkiln_yhteystiedot_96d",
377
+ ]);
378
+ for (const [key, fn] of discovered) {
379
+ if (NOT_PUSH_TARGET.has(key)) continue;
380
+ await pushFunctionField(key, fn);
598
381
  }
599
382
  ```
600
383
 
601
384
  ---
602
385
 
603
- ## Critical Rules
386
+ ## Null Safety Rule
604
387
 
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.
388
+ | Input type | Guard pattern |
389
+ |------------|---------------|
390
+ | Backlink arrays (`<`) | `dep.items \|\| []` |
391
+ | Metadata object | `dep.meta ? dep.meta.phase : null` |
392
+ | String/text field | `dep.name \|\| ''` |
393
+ | Number field | `Number(dep.amount) \|\| 0` |
394
+ | JSON string parse | `dep.json ? JSON.parse(dep.json) : []` do NOT use `dep.json && JSON.parse(dep.json)`; when `dep.json` is null, `&&` short-circuits to `false`, then `for (const item of false)` throws |
395
+ | Date range | `if (!range \|\| !range.start) return null;` |
396
+ | Nested property | `item && item._id` before accessing |
617
397
 
618
398
  ---
619
399
 
620
400
  ## Common Mistakes
621
401
 
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 |
634
-
635
- ---
636
-
637
- ## Checklist
638
-
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
402
+ | Wrong | Right |
403
+ |-------|-------|
404
+ | `return ''` when no value | `return null` |
405
+ | `var arr = dep.arr` | `const arr = dep.arr \|\| []` |
406
+ | `arr[i] * 2` | `(Number(arr[i]) \|\| 0) * 2` |
407
+ | `data[i].phase` | `data[i] ? data[i].phase : null` |
408
+ | Hardcoding phase IDs | Use `?` static variables with enums |
409
+ | TypeScript syntax in function body | Plain ES6 JS only |
410
+ | `let maxDate: number \| null = null;` | `let maxDate = 0;` (sentinel) |
411
+ | `const dates: number[] = [];` | `const dates = [];` |
412
+ | `export function calc(): string \| null` | `: any` SDK regex matches single-word return types only |
413
+ | `dep.myLink === 'someId'` | `dep.myLink._id === 'someId'` activitylink `=` returns object |
414
+ | Using `"meta"` magic key | Use `"data"` — `"meta"` silently returns null |
415
+ | `type: "="` with `"linkedfrom"` | Use `type: "<"` with `[workflowId, "data"]` for reverse links |