@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.
- package/.claude/skills/create-and-publish-app/SKILL.md +44 -56
- package/.claude/skills/hailer-app-builder/SKILL.md +414 -970
- package/.claude/skills/hailer-app-primitives/SKILL.md +742 -0
- package/.claude/skills/hailer-apps-pictures/SKILL.md +191 -87
- package/.claude/skills/hailer-design-patterns/SKILL.md +317 -0
- package/.claude/skills/hailer-design-system/SKILL.md +202 -149
- package/.claude/skills/hailer-workflow-archetypes/SKILL.md +301 -0
- package/.claude/skills/insight-join-patterns/SKILL.md +313 -0
- package/.claude/skills/publish-hailer-app/SKILL.md +211 -0
- package/.claude/skills/sdk-activity-patterns/SKILL.md +257 -105
- package/.claude/skills/sdk-function-fields/SKILL.md +253 -492
- package/.claude/skills/sdk-insight-calculations/SKILL.md +364 -0
- package/.claude/skills/sdk-insight-queries/SKILL.md +192 -397
- package/.claude/skills/sdk-ws-config-skill/SKILL.md +265 -720
- package/.claude/skills/tool-response-verification/SKILL.md +143 -0
- package/CLAUDE.md +50 -19
- package/dist/bot/bot.d.ts.map +1 -1
- package/dist/bot/bot.js +26 -34
- package/dist/bot/bot.js.map +1 -1
- package/dist/bot/services/helper-prompt.d.ts +1 -1
- package/dist/bot/services/helper-prompt.d.ts.map +1 -1
- package/dist/bot/services/helper-prompt.js +62 -82
- package/dist/bot/services/helper-prompt.js.map +1 -1
- package/dist/bot/services/system-prompt.js +4 -4
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/mcp/hailer-rpc.d.ts +1 -0
- package/dist/mcp/hailer-rpc.d.ts.map +1 -1
- package/dist/mcp/hailer-rpc.js +4 -0
- package/dist/mcp/hailer-rpc.js.map +1 -1
- package/dist/mcp/tools/activity.d.ts.map +1 -1
- package/dist/mcp/tools/activity.js +8 -2
- package/dist/mcp/tools/activity.js.map +1 -1
- package/dist/mcp/tools/app-core.d.ts.map +1 -1
- package/dist/mcp/tools/app-core.js +6 -3
- package/dist/mcp/tools/app-core.js.map +1 -1
- package/dist/mcp/tools/app-marketplace.d.ts.map +1 -1
- package/dist/mcp/tools/app-marketplace.js +5 -1
- package/dist/mcp/tools/app-marketplace.js.map +1 -1
- package/dist/mcp/webhook-handler.d.ts.map +1 -1
- package/dist/mcp/webhook-handler.js +27 -0
- package/dist/mcp/webhook-handler.js.map +1 -1
- package/dist/public-chat/graduate.d.ts.map +1 -1
- package/dist/public-chat/graduate.js +153 -53
- package/dist/public-chat/graduate.js.map +1 -1
- package/dist/public-chat/rate-limit.js +1 -1
- package/dist/public-chat/rate-limit.js.map +1 -1
- package/dist/public-chat/studio-prewarm.js +2 -2
- package/dist/public-chat/studio-prewarm.js.map +1 -1
- package/dist/public-chat/system-prompt.d.ts.map +1 -1
- package/dist/public-chat/system-prompt.js +41 -25
- package/dist/public-chat/system-prompt.js.map +1 -1
- package/package.json +1 -1
- package/.claude/skills/hailer-project-protocol/SKILL.md +0 -398
- 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
|
|
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
|
|
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
|
-
##
|
|
23
|
+
## Canonical Fixture
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
+
---
|
|
39
50
|
|
|
40
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
65
|
+
---
|
|
74
66
|
|
|
75
|
-
|
|
67
|
+
## Step-by-Step Workflow
|
|
76
68
|
|
|
77
|
-
|
|
69
|
+
### 1. Add Field Definition (fields.ts)
|
|
78
70
|
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
86
|
+
### 2. Create Function File
|
|
83
87
|
|
|
84
88
|
```typescript
|
|
85
89
|
/**
|
|
86
|
-
*
|
|
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
|
-
|
|
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/
|
|
124
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
### 6. Push
|
|
106
|
+
### 4. Push
|
|
145
107
|
|
|
146
108
|
```bash
|
|
147
109
|
npm run fields-push:force
|
|
148
110
|
```
|
|
149
111
|
|
|
150
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
npm run pull
|
|
156
|
-
```
|
|
118
|
+
---
|
|
157
119
|
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
152
|
+
---
|
|
203
153
|
|
|
204
|
-
|
|
154
|
+
## Editable Fields (Freeze After First Calculation)
|
|
205
155
|
|
|
206
|
-
**Wrong:**
|
|
207
156
|
```typescript
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
178
|
+
Backlinks return **arrays** because multiple activities can link to this one.
|
|
282
179
|
|
|
283
|
-
```
|
|
284
|
-
//
|
|
285
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (`"
|
|
203
|
+
## Activity Metadata (`"data"`)
|
|
338
204
|
|
|
339
|
-
|
|
205
|
+
**Critical:** use `"data"` as the magic key, NOT `"meta"` — `"meta"` silently returns null.
|
|
340
206
|
|
|
341
|
-
###
|
|
207
|
+
### Current Activity Metadata (`=`)
|
|
342
208
|
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
myMeta: { type: "=", data: ["
|
|
209
|
+
```javascript
|
|
210
|
+
{
|
|
211
|
+
myMeta: { type: "=", data: ["data"] }
|
|
346
212
|
}
|
|
347
|
-
//
|
|
213
|
+
// Available meta fields (10 keys): _id, name, process, phase, created, updated,
|
|
214
|
+
// completed, processName, phaseName, sequence
|
|
348
215
|
```
|
|
349
216
|
|
|
350
|
-
|
|
217
|
+
`sequence` is a server-maintained auto-incrementing activity counter — usable for serial names like `T-YYYY-NNNN`.
|
|
351
218
|
|
|
352
|
-
|
|
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
|
-
```
|
|
362
|
-
|
|
363
|
-
customerMeta: { type: ">", data: [FieldIds.customer_link, "
|
|
223
|
+
```javascript
|
|
224
|
+
{
|
|
225
|
+
customerMeta: { type: ">", data: [FieldIds.customer_link, "data"] }
|
|
364
226
|
}
|
|
365
|
-
//
|
|
227
|
+
// In function
|
|
228
|
+
const customerPhase = dep.customerMeta ? dep.customerMeta.phaseName : 'Unknown';
|
|
366
229
|
```
|
|
367
230
|
|
|
368
|
-
### Metadata
|
|
231
|
+
### Backlink Metadata (`<`)
|
|
369
232
|
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
phaseName:
|
|
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
|
-
##
|
|
246
|
+
## Static Variables (`?`)
|
|
391
247
|
|
|
392
|
-
|
|
393
|
-
- Same length
|
|
394
|
-
- Same order (index 0 in all arrays = same activity)
|
|
248
|
+
Static variables provide phase IDs for comparison.
|
|
395
249
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
|
262
|
+
### Sum from Backlinks
|
|
436
263
|
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
|
|
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 <
|
|
446
|
-
sum += Number(
|
|
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
|
-
###
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
const
|
|
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
|
-
|
|
467
|
-
for (let i = 0; i <
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
291
|
+
if (arr.length === 0) return null;
|
|
292
|
+
return Math.max(...arr);
|
|
473
293
|
}
|
|
474
294
|
```
|
|
475
295
|
|
|
476
|
-
###
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
311
|
+
`sequence` is monotonic across the workflow's whole lifetime — does NOT reset each year. Deletions can leave gaps.
|
|
518
312
|
|
|
519
|
-
|
|
520
|
-
interface Dependencies {
|
|
521
|
-
goals: number;
|
|
522
|
-
minutesPlayed: number;
|
|
523
|
-
}
|
|
313
|
+
---
|
|
524
314
|
|
|
525
|
-
|
|
526
|
-
const goals = Number(dep.goals) || 0;
|
|
527
|
-
const minutes = Number(dep.minutesPlayed) || 0;
|
|
315
|
+
## Critical Rules
|
|
528
316
|
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
349
|
+
---
|
|
535
350
|
|
|
536
|
-
|
|
537
|
-
interface Dependencies {
|
|
538
|
-
yellowCards: number;
|
|
539
|
-
redCards: number;
|
|
540
|
-
}
|
|
351
|
+
## Naming Collisions in functionVariables Keys
|
|
541
352
|
|
|
542
|
-
|
|
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
|
-
|
|
355
|
+
---
|
|
548
356
|
|
|
549
|
-
|
|
550
|
-
interface Dependencies {
|
|
551
|
-
dueDate: number;
|
|
552
|
-
}
|
|
357
|
+
## Recompute Triggers
|
|
553
358
|
|
|
554
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
369
|
+
## Bulk Function-Field Push: Use a NOT_INHERITANCE Exclusion Set
|
|
586
370
|
|
|
587
|
-
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
##
|
|
386
|
+
## Null Safety Rule
|
|
604
387
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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 |
|
|
623
|
-
|
|
624
|
-
| `
|
|
625
|
-
| `arr
|
|
626
|
-
| `
|
|
627
|
-
| `
|
|
628
|
-
|
|
|
629
|
-
|
|
|
630
|
-
| `
|
|
631
|
-
|
|
|
632
|
-
| `
|
|
633
|
-
|
|
|
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 |
|