@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,19 +1,34 @@
1
1
  ---
2
2
  name: sdk-activity-patterns
3
- description: Activity CRUD patterns - field value formats, dates, users, links, dropdowns
4
- version: 1.5.0
5
- triggers: Create activity, update activity, field values, date format, activitylink value
3
+ description: Correct field value formats for creating and updating Hailer activities via MCP tools (mcp__hailer__create_activity, mcp__hailer__update_activity). Load whenever creating activities, updating activity fields, bulk-creating activities, or debugging field write failures. Covers dates (Unix timestamps), dropdowns (plain strings), user fields, activitylink fields, daterange/timerange objects, clearing fields, bulk teamId placement, phase-gated field writes, and all-or-nothing batch behavior.
6
4
  ---
7
5
 
6
+ <!-- augmentation-guard: shrink-ok -->
7
+
8
8
  # Activity CRUD Patterns
9
9
 
10
+ ## MCP Tools
11
+
12
+ | Tool | Purpose |
13
+ |------|---------|
14
+ | `mcp__hailer__create_activity` | Create new activity |
15
+ | `mcp__hailer__update_activity` | Update existing activity |
16
+
17
+ ---
18
+
10
19
  ## Create Activity
11
20
 
21
+ **`name` must be a non-empty string when provided.** Hailer rejects `name: ""` with `name: Activity name cannot be empty`.
22
+
23
+ **`create_activity` REQUIRES `name` even when `nameFunctionEnabled: true`.** The MCP runtime enforces this — pass a meaningful placeholder (e.g. `'Luonnos'`). The `nameFunction` will overwrite it immediately.
24
+
25
+ **`nameFunction` overrides any `name` you pass on create.** Place the human-readable identity in the field that `nameFunction` reads.
26
+
12
27
  ```javascript
13
28
  mcp__hailer__create_activity({
14
29
  workflowId: "682ac815fba468d857d498f7",
15
30
  phaseId: "682ac815fba468d857d49904",
16
- name: "New Task", // Optional
31
+ name: "New Task", // non-empty string; omit entirely not allowed in single mode
17
32
  fields: {
18
33
  "fieldId1": "value1",
19
34
  "fieldId2": "value2"
@@ -31,78 +46,132 @@ mcp__hailer__update_activity({
31
46
  })
32
47
  ```
33
48
 
49
+ **Update error reporting is lossy.** `update_activity` rejections surface as `[object Object]` — the underlying server error is swallowed. When an MCP write errors opaquely, re-run the same operation via REST, or confirm via read-back (`show_activity_by_id`). `create_activity` errors ARE rich (full `{code, msg, details}` envelope).
50
+
51
+ **@hailer/mcp 1.3.x behavior change — unknown field keys now ERROR.** Passing an unknown field key in `create_activity` or `update_activity` now returns an error. On 1.2.1 the key was silently ignored.
52
+
34
53
  ---
35
54
 
36
- ## Critical Field Value Rules
55
+ ## Field Value Formats
37
56
 
38
- **All single-value fields use STRING, not array.** No multi-select types exist in Hailer.
57
+ **No multi-select types exist in Hailer.**
39
58
 
40
- ```javascript
41
- // ✅ Correct
42
- fields: { priority: "High", assignee: "userId", customer: "actId" }
59
+ ### Text Fields
43
60
 
44
- // Wrong arrays rejected
45
- fields: { priority: ["High"], assignee: ["userId"], customer: ["actId"] }
46
- ```
61
+ | Type | Format | Example |
62
+ |------|--------|---------|
63
+ | `text` | String | `"Hello world"` |
64
+ | `textarea` | String | `"Multi\nline\ntext"` |
65
+ | `textunit` | String | `"100"` |
47
66
 
48
- **Dates are Unix timestamps in milliseconds** (not ISO strings):
49
- ```javascript
50
- fields: { due_date: new Date('2024-11-07').getTime() } // 1730937600000
51
- ```
67
+ ### Number Fields
68
+
69
+ | Type | Format | Example |
70
+ |------|--------|---------|
71
+ | `numeric` | Number | `42` or `42.5` |
72
+ | `numericunit` | Number | `99.99` (plain number — unit is display-only) |
73
+
74
+ ### Date/Time Fields
75
+
76
+ | Type | Format | Example |
77
+ |------|--------|---------|
78
+ | `date` | Unix timestamp (ms) OR ISO `YYYY-MM-DD` string via MCP (auto-converted) | `1730937600000` or `"2024-11-07"` |
79
+ | `datetime` | Unix timestamp (ms) OR ISO `YYYY-MM-DD` string via MCP | `1730937600000` |
80
+ | `daterange` | Object | `{ start: 1730937600000, end: 1731024000000 }` |
81
+ | `datetimerange` | Object | `{ start: 1730937600000, end: 1731024000000 }` |
82
+ | `time` | Unix ms since epoch (includes date) — `HH:MM` string is rejected, NOT coerced | `46800000` |
83
+ | `timerange` | Object | `{ start: 1765863000000, end: 1765915200000 }` |
52
84
 
53
- **Time fields** also use full millisecond timestamps including date:
54
85
  ```javascript
86
+ // JavaScript Date to Hailer timestamp
87
+ const timestamp = new Date('2024-11-07').getTime(); // 1730937600000
88
+
89
+ // Time field: also Unix timestamp (includes date)
55
90
  const today = new Date();
56
91
  today.setUTCHours(9, 30, 0, 0);
57
- fields: { start_time: today.getTime() }
92
+ const timeValue = today.getTime();
58
93
  ```
59
94
 
60
- **daterange / datetimerange** use object with `start` + `end`:
61
- ```javascript
62
- fields: { period: { start: 1730937600000, end: 1731024000000 } }
63
- ```
95
+ ### Selection Fields
64
96
 
65
- **activitylink** is plain string ID READ format (object with `_id`, `name`) differs from WRITE format:
66
- ```javascript
67
- // Write: plain string ID
68
- fields: { customer: "692abc123def456" }
97
+ | Type | Format | Example |
98
+ |------|--------|---------|
99
+ | `textpredefinedoptions` | **STRING** (exact option string from field's `data[]`) | `"High"` |
69
100
 
70
- // Wrong: object (this is what you get when reading)
71
- fields: { customer: { _id: "692abc123def456", name: "Acme" } }
72
- ```
101
+ **CRITICAL #1:** Value is STRING, not array!
102
+ **CRITICAL #2:** The string is the **exact option string** from the field's `data[]` — there is no separate generated key.
103
+ **CRITICAL #3 — No server-side membership check.** A non-option value or wrong-case value is stored verbatim without error, renders blank, and silently breaks downstream filters.
73
104
 
74
- **textpredefinedoptions** is the exact option string, not an array:
75
105
  ```javascript
76
- fields: { status: "In Progress" }
77
- ```
106
+ // Correct
107
+ fields: { industry: "Retail" }
78
108
 
79
- **text fields must always be strings**, even for numeric-looking values:
80
- ```javascript
81
- fields: { code: "300" } // not 300
109
+ // Wrong array rejected with code 191
110
+ fields: { industry: ["Retail"] }
111
+
112
+ // ⚠️ Silently stores invalid value — renders blank
113
+ fields: { industry: "retail" } // wrong case
82
114
  ```
83
115
 
84
- ---
116
+ ### User & Team Fields
117
+
118
+ | Type | Format | Example |
119
+ |------|--------|---------|
120
+ | `users` | **STRING** (user ID) | `"5f8a1b2c3d4e5f6a7b8c9d0e"` |
121
+ | `teams` | **STRING** (team ID) | `"teamId123"` |
122
+
123
+ **CRITICAL:** Value is STRING, not array! No multi-select types exist.
124
+
125
+ ### Link Fields
85
126
 
86
- ## Modified Fields (Checkbox & File)
127
+ | Type | Format | Example |
128
+ |------|--------|---------|
129
+ | `activitylink` | **STRING** (activity ID) | `"692abc123def456"` |
130
+
131
+ **CRITICAL:** Canonical write format is a plain STRING ID, not array or object!
132
+
133
+ **CREATE vs UPDATE asymmetry:**
134
+ - On **CREATE**: object format is REJECTED with code 191. Use plain string only.
135
+ - On **UPDATE** (single mode): `{_id, name}` or `{value: "<24hex>"}` objects are silently coerced to string ID.
136
+ - Arrays are NOT coerced in either mode.
137
+
138
+ **READ vs WRITE formats differ:**
139
+ - **READ via app-sdk / `returnFlat: true`**: `{ _id: string, name: string }` object
140
+ - **READ via MCP `show_activity_by_id`**: `{ type: "activitylink", value: { _id, name } }` — unwrap `.value`
141
+ - **WRITE**: plain string ID only
142
+
143
+ **ActivityLink Multi-Value Inconsistency:** The same ActivityLink field on different activities can return different shapes (`{_id, name}` object vs array). Use a defensive helper:
144
+
145
+ ```js
146
+ function matchesActivityLink(value, targetId) {
147
+ if (Array.isArray(value)) return value.some(x => x?._id === targetId);
148
+ return value?._id === targetId;
149
+ }
150
+ ```
87
151
 
88
- These use base types with a `modifier` in the field config:
152
+ ### Modified Fields (Checkbox & File)
89
153
 
90
- | Config | Write Format |
91
- |--------|-------------|
92
- | `numeric` + `modifier.checkbox: true` | `1` (true) or `0` (false) |
93
- | `text` + `modifier.file: true` | File reference (UI handles upload) |
154
+ | Config | Value Format | Example |
155
+ |--------|--------------|---------|
156
+ | `numeric` + `modifier.checkbox: true` | Number | `1` (true) or `0` (false) |
157
+ | `text` + `modifier.file: true` | Object | File reference (UI handles) |
94
158
 
95
159
  ---
96
160
 
97
- ## Clearing Field Values
161
+ ## Complete Create Example
98
162
 
99
163
  ```javascript
100
- mcp__hailer__update_activity({
101
- activityId: "692abc123def456",
164
+ mcp__hailer__create_activity({
165
+ workflowId: "682ac815fba468d857d498f7",
166
+ phaseId: "682ac815fba468d857d49904",
167
+ name: "Fix login bug",
102
168
  fields: {
103
- "assignee_abc": "", // Clear user
104
- "due_date_def": null, // Clear date
105
- "notes_ghi": "" // Clear text
169
+ "description_abc": "Users cannot log in after password reset",
170
+ "priority_def": "High", // Dropdown — exact string from data[]
171
+ "assignee_jkl": "5f8a1b2c3d4e5f6a7b8c9d0e", // User — string ID
172
+ "project_mno": "692abc123def456", // Activity link — string ID
173
+ "due_date_pqr": 1730937600000, // Date — ms timestamp
174
+ "estimated_hours_stu": 4 // Number
106
175
  }
107
176
  })
108
177
  ```
@@ -111,7 +180,7 @@ mcp__hailer__update_activity({
111
180
 
112
181
  ## Bulk Creation
113
182
 
114
- Use the `activities[]` array for bulk creation. **Each activity must include its own `teamId` and `phaseId`.**
183
+ Use the `activities[]` array for bulk creation. **In MCP `create_activity` bulk mode, each activity must include its own `teamId` and `phaseId`** in the `activities[]` array.
115
184
 
116
185
  ```javascript
117
186
  mcp__hailer__create_activity({
@@ -139,87 +208,168 @@ mcp__hailer__create_activity({
139
208
  |-----------|-------------|-----------|
140
209
  | `name` | Top-level | Inside each `activities[]` item |
141
210
  | `phaseId` | Top-level | Inside each `activities[]` item |
142
- | `teamId` | Top-level | **Inside each `activities[]` item** |
211
+ | `teamId` | Top-level | Inside each `activities[]` item |
143
212
  | `fields` | Top-level | Inside each `activities[]` item |
144
213
  | `workflowId` | Top-level | Top-level (shared) |
145
214
 
146
- **CRITICAL:** In bulk mode, `teamId` at the top level is **IGNORED**. Each activity in the array must have its own `teamId`. Omitting it causes "Missing team(s)" (code 127) unless the workflow has a default team.
215
+ **CRITICAL (MCP bulk mode):** `teamId` at the top level is **IGNORED**. MCP backfills the workflow default team when `teamId` is absent code 127 "Missing team(s)" only fires if no default team exists at all. Supply `teamId` explicitly per activity to avoid relying on fallback.
147
216
 
148
- ### Team ID Fallback Chain
217
+ ### Team ID Fallback Chain (MCP)
149
218
 
150
- The MCP tool auto-fills `teamId` if omitted:
151
- 1. Per-activity `teamId` (if provided) — **always provide this**
219
+ 1. Per-activity `teamId` (if provided) — always provide this
152
220
  2. Workflow's default team (`workflow.team`)
153
- 3. First workspace team (last resort, may be wrong team)
221
+ 3. First workspace team (last resort, may be wrong)
222
+
223
+ ---
154
224
 
155
- **Best practice:** Always pass `teamId` explicitly. Get IDs from `workspace/teams.ts`.
225
+ ## Getting IDs
156
226
 
157
- ### Common Error: "Missing team(s)" (Code 127)
227
+ - **Workflow ID** from `describe_workflows` (real MongoDB ObjectId)
228
+ - **Phase ID** — from `describe_workflows` with `workflowId` + `include:['phases']`
229
+ - **Field IDs** — from `describe_workflows` with `workflowId` + `include:['schema']`
230
+ - **User IDs** — from `search_workspace_users`
231
+ - **Activity IDs** — from `list_activities` or previous create results
158
232
 
159
- **Fix options:**
160
- 1. Pass `teamId` explicitly in every create call (best)
161
- 2. Set default team on the workflow in Hailer UI
162
- 3. Ask orchestrator for available team IDs before creating
233
+ **`workflowId` and `phaseId` need real ObjectIds** (24-char hex strings), not enum key names.
234
+
235
+ **Field keys vs field IDs vs enum keys:**
236
+ - Field key: `firstName` the `key` property set on the field in Hailer
237
+ - Field ID: `68cbfec59b3869137fe2af84` — MongoDB ObjectId
238
+ - Enum key: `etunimi_f84` — TypeScript constant (NOT accepted by API)
239
+
240
+ Both field keys and field IDs work in `create_activity`/`update_activity`. Enum key names don't.
163
241
 
164
242
  ---
165
243
 
166
- ## Enum Key vs ObjectId Gotcha
244
+ ## Clearing Field Values
167
245
 
168
- MCP tools require actual MongoDB ObjectIds (24-char hex strings), not enum key names.
246
+ To clear a field, use `null`. **Do not use `""` for text or textarea fields** — empty string is rejected with code 191.
169
247
 
170
248
  ```javascript
171
- // ❌ WRONG - enum KEY NAME, not the ID
172
- mcp__hailer__create_activity({
173
- workflowId: "Asiakkaat", // enum key
174
- fields: { "projekti_f84": "value" } // enum key
249
+ mcp__hailer__update_activity({
250
+ activityId: "692abc123def456",
251
+ fields: {
252
+ "assignee_abc": null, // Clear user
253
+ "due_date_def": null, // Clear date
254
+ "notes_ghi": null // Clear text — NOT ""
255
+ }
175
256
  })
257
+ ```
176
258
 
177
- // ✅ CORRECT - real ObjectIds
178
- mcp__hailer__create_activity({
179
- workflowId: "682ac815fba468d857d498f7",
180
- fields: { "68cbfec59b3869137fe2af84": "value" }
181
- })
259
+ ---
260
+
261
+ ## Phase-Gated Field Writes on Create
262
+
263
+ `create_activity` gates field writes to the fields listed on the activity's **current phase** at creation time. If a field is defined on the workflow but not included in the initial phase's `fields[]` list, passing it in `create_activity` returns code 191 "not allowed" — even though the field exists and the value is valid.
264
+
265
+ **Fix — split create from update:**
266
+
267
+ ```javascript
268
+ // Step 1: create with ONLY fields present on the initial phase
269
+ const activity = await mcp__hailer__create_activity({
270
+ workflowId: "...",
271
+ phaseId: initialPhaseId,
272
+ name: "Task name",
273
+ fields: { "field_on_initial_phase": value }
274
+ });
275
+
276
+ // Step 2: update the rest immediately after
277
+ await mcp__hailer__update_activity({
278
+ activityId: activity.created_ids[0],
279
+ fields: { "due_date_field": dueDateTimestamp }
280
+ });
182
281
  ```
183
282
 
184
- **TypeScript code vs MCP calls:**
185
- - **TypeScript:** Use enums for type safety → `WorkflowIds.Asiakkaat` resolves to the ObjectId
186
- - **MCP tools:** Pass the actual ObjectId string directly
283
+ ---
284
+
285
+ ## createMany / updateMany Cross-Cutting Patterns
286
+
287
+ ### All-or-Nothing Batch Behavior
288
+
289
+ `v3.activity.createMany` and `v3.activity.updateMany` are **all-or-nothing per batch**. A single rejected row fails the entire batch.
290
+
291
+ Mitigations:
292
+ 1. Isolate phase/status mixing into separate batches
293
+ 2. Pre-validate phase transitions against `possibleNextPhase` — terminal phases (`possibleNextPhase: []`) block reverse moves
294
+ 3. Bisect on failure to find the offender
295
+
296
+ ```javascript
297
+ async function batchWithRetry(workflowId, rows, options) {
298
+ if (rows.length === 0) return;
299
+ try {
300
+ await v3.activity.createMany(workflowId, rows, options);
301
+ } catch (err) {
302
+ if (rows.length === 1) throw err;
303
+ const mid = Math.floor(rows.length / 2);
304
+ await batchWithRetry(workflowId, rows.slice(0, mid), options);
305
+ await batchWithRetry(workflowId, rows.slice(mid), options);
306
+ }
307
+ }
308
+ ```
187
309
 
188
- **Where to get real IDs:**
189
- - `workspace/enums.ts` → enum values ARE the real ObjectIds
190
- - `list_workflows` workflow `_id` field
191
- - `get_workflow_schema` field `_id` property
310
+ ### Batch Size
311
+
312
+ - createMany: 50 per batch
313
+ - updateMany: 100 per batch
314
+
315
+ ### Empty String Rejected Even When `required: false` (Code 191)
316
+
317
+ Passing `""` for a text or textarea field always triggers code 191, regardless of `required`. Omit keys with no value instead.
318
+
319
+ ```javascript
320
+ function cleanFields(raw) {
321
+ return Object.fromEntries(
322
+ Object.entries(raw).filter(([, v]) => v !== null && v !== undefined && v !== "")
323
+ );
324
+ }
325
+ ```
326
+
327
+ ### `updateMany` row shape
328
+
329
+ Row shape is `{_id, fields?, phaseId?}` only — no `processId`. `processId` in the per-row object causes error 191.
192
330
 
193
331
  ---
194
332
 
195
- ## Phase Transitions
333
+ ## Deleting Activities — Use `activities.remove`
196
334
 
197
335
  ```javascript
198
- // Move only
199
- mcp__hailer__update_activity({ activityId: "...", phaseId: "..." })
336
+ // Hard delete
337
+ await client.request('activities.remove', [activityId]);
200
338
 
201
- // Move + update fields
202
- mcp__hailer__update_activity({
203
- activityId: "692abc123def456",
204
- phaseId: "682ac815fba468d857d49906",
205
- fields: { "completed_date_abc": Date.now() }
206
- })
339
+ // Bulk delete
340
+ await client.request('activities.remove', [[id1, id2, id3]]);
341
+ ```
342
+
343
+ `v3.activity.delete` / `v3.activity.deleteMany` / `v3.activity.removeMany` → **code 7 "Method not found"**.
344
+
345
+ **Soft-archive alternative:** Move to a terminal phase via `updateMany`.
346
+
347
+ ---
348
+
349
+ ## Create Response Shape (MCP vs REST)
350
+
351
+ MCP `create_activity` returns `{ created_ids: [...] }` in BOTH single and bulk mode.
352
+
353
+ ```javascript
354
+ // ✅ Correct success check
355
+ const result = await mcp__hailer__create_activity({ workflowId, activities: [...] });
356
+ const ids = result.created_ids ?? [];
357
+
358
+ // ❌ Wrong — single _id check misses bulk responses
359
+ if (!result._id) throw new Error('create failed');
207
360
  ```
208
361
 
209
362
  ---
210
363
 
211
- ## Checklist
364
+ ## Filtering Reads: `text_search` Does NOT Match ActivityLink Fields
365
+
366
+ `list_activities` `filters` with `operator: "text_search"` matches **text fields only**. On an `activitylink` field it returns 0 rows. Use `equals` + the linked activity's `_id` instead.
367
+
368
+ ---
212
369
 
213
- Before creating/updating activities:
370
+ ## Editable Function Fields
214
371
 
215
- - [ ] Have workflow ID (from enum or API)
216
- - [ ] Have phase ID (from enum or API)
217
- - [ ] Have field IDs (from enum or API)
218
- - [ ] Have **team ID** (from teams.ts or API) — don't rely on fallbacks
219
- - [ ] All values are **STRINGS** for select/user/link fields (no multi-select types exist)
220
- - [ ] Dates are **Unix timestamps in milliseconds**
221
- - [ ] Times are **Unix timestamps in milliseconds** (like dates)
222
- - [ ] **Bulk mode:** teamId, phaseId, fields are **inside each activity object**, not top-level
372
+ A function field with `editable: true` is a valid write target — pass a value the same way as any other field. A stored value wins over the computed default; the function only back-fills while the field is empty.
223
373
 
224
374
  ---
225
375
 
@@ -227,12 +377,14 @@ Before creating/updating activities:
227
377
 
228
378
  | Wrong | Right |
229
379
  |-------|-------|
230
- | `{ priority: ["High"] }` | `{ priority: "High" }` (string) |
231
- | `{ assignee: ["userId"] }` | `{ assignee: "userId" }` (string) |
232
- | `{ customer: ["actId"] }` | `{ customer: "actId" }` (string) |
233
- | `{ date: "2024-11-07" }` | `{ date: 1730937600000 }` (timestamp ms) |
234
- | `{ time: "09:00" }` | `{ time: 1765863000000 }` (ms timestamp) |
380
+ | `{ priority: ["High"] }` | `{ priority: "High" }` string for single select |
381
+ | `{ assignee: ["userId"] }` | `{ assignee: "userId" }` string for single user |
382
+ | `{ customer: ["actId"] }` | `{ customer: "actId" }` string for activitylink |
383
+ | `{ date: "2024-11-07" }` via raw REST/app-sdk | `{ date: 1730937600000 }` ms timestamp |
384
+ | `{ time: "09:00" }` | `{ time: 46800000 }` `HH:MM` string is rejected |
235
385
  | `{ daterange: 1730937600000 }` | `{ daterange: { start: ..., end: ... } }` |
236
- | `{ textField: 300 }` | `{ textField: "300" }` (must be string) |
237
- | Passing enum key names as IDs | Use real 24-char hex ObjectIds |
386
+ | `{ textField: 300 }` | `{ textField: "300" }` — text fields must be STRINGS |
387
+ | Comparing activitylink by value | Compare `value._id` READ format is `{_id, name}` object |
238
388
  | Bulk create with top-level teamId | Put teamId INSIDE each activity object |
389
+ | `name: ""` | Non-empty string or omit; empty string rejected |
390
+ | `data: [{value: "A", label: "A"}]` in field creation | `data: ["A", "B"]` — plain string array only |