@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,55 +1,49 @@
1
1
  ---
2
2
  name: sdk-ws-config-skill
3
- description: Comprehensive workspace configuration - workflows, fields, phases
4
- version: 1.8.0
5
- triggers: Create workflow, add field, configure phase, field visibility, phase transitions, new project, init workspace, setup SDK, create workspace
3
+ description: Workspace configuration for Hailer — workflows, fields, phases, keys, push/pull sequencing, and common creation patterns. Load this skill before creating or editing any workflow structure (workflows, fields, phases, teams, groups). Also use when you see errors like "can't create activities", phase-transition errors, field push slow acks, or questions about field types and phase visibility.
6
4
  ---
7
5
 
8
- # New Project Setup
6
+ <!-- augmentation-guard: shrink-ok -->
9
7
 
10
- ```bash
11
- mkdir my-hailer-project && cd my-hailer-project
12
- npm init -y
13
- npm install @hailer/sdk typescript vitest --save-dev
14
- ```
8
+ # Workspace Configuration
15
9
 
16
- Create `.env`:
17
- ```
18
- HAILER_API_KEY=your_api_key
19
- HAILER_WORKSPACE_ID=your_workspace_id
20
- ```
10
+ ## Push/Pull Sequencing
21
11
 
22
- Create `config.json`:
23
- ```json
24
- { "workspaceId": "your_workspace_id", "apiUrl": "https://api.hailer.com" }
25
- ```
12
+ ### Decision Tree — Read First
26
13
 
27
- ```bash
28
- npm run pull # Download workspace config into workspace/
29
- git init
30
- echo ".env" >> .gitignore
31
- echo "node_modules" >> .gitignore
32
- ```
14
+ **Rule 1: Use granular per-layer commands, not `npm run push`**
33
15
 
34
- **Project structure after pull:**
35
- ```
36
- workspace/
37
- ├── workflows.ts, enums.ts (AUTO-GENERATED), teams.ts, groups.ts
38
- ├── hailer.d.ts ← AUTO-GENERATED (never edit)
39
- └── [Workflow]_[id]/
40
- ├── main.ts, fields.ts, phases.ts
41
- └── functions/*.ts
42
- ```
16
+ | What you want to do | Command |
17
+ |---------------------|---------|
18
+ | Create a new workflow | `npm run workflows-sync:force` |
19
+ | Update workflow properties (name, nameFunction) | `npm run workflows-push` |
20
+ | Create or update fields | `npm run fields-push` |
21
+ | Create or update phases | `npm run phases-push:force` |
22
+ | Push everything at once | `npm run push:force` (only when all items already exist server-side) |
43
23
 
44
- **`npm run pull` vs `npm run generate`:**
45
- - `npm run pull` — Downloads workspace config into `workspace/` (editable TypeScript files: fields, phases, workflows, enums, etc.)
46
- - `npm run generate` — Generates `hailer-types.ts` (a single file with workflow/phase/field enums for use in app code). These are separate commands serving different purposes.
24
+ **Rule 2: Pull only after creating new items (workflows, phases, fields) that need server-assigned IDs. Skip pull after update-only pushes — pull overwrites local changes.**
47
25
 
48
- **After setup:** Always set a default team on each workflow (see CRITICAL rule below).
26
+ | Push situation | Pull after? | Why |
27
+ |----------------|:-----------:|-----|
28
+ | `workflows-sync:force` (new workflow) | YES | Server assigns `_id` + folder |
29
+ | `phases-push:force` — creating new phases | YES | Server assigns phase hex IDs |
30
+ | `fields-push` — creating new fields | YES | Server assigns field hex IDs |
31
+ | `phases-push:force` — only updating existing phases | SKIP | No new IDs; pull overwrites local edits |
32
+ | `fields-push` — only updating existing fields | SKIP | No new IDs |
33
+ | `workflows-push` (nameFunction, permissions) | SKIP | Pull strips nameFunction |
49
34
 
50
- ---
35
+ **Rule 3: Two-pass push for new entity + references to it.** Pass 1 = push the new entity only. Pull to get server-assigned ID into `enums.ts`. Pass 2 = add references using the freshly-pulled enum symbol.
51
36
 
52
- # Workspace Configuration
37
+ **Rule 3b: New-workflow creation is a two-pass dance — the pull between passes is a wipe trap.**
38
+
39
+ The sequence:
40
+ 1. `workflows-sync:force` creates only the workflow shell (no fields, no real phases). It reads new entries from `workflows.ts`.
41
+ 2. You MUST `npm run pull` to fetch the server-assigned workflow `_id` and rename the folder. **That pull overwrites local with server state** — it empties the renamed dir (server shell has no fields/phases yet) AND deletes any un-pushed field additions staged on other existing workflows.
42
+ 3. After pull: re-stage field/phase defs into the renamed dir → `phases-push:force` → `fields-push`.
43
+
44
+ **Pull-wipe prevention guard:** before running `npm run pull` after `workflows-sync`, verify the CLI printed "✨ Created: N" (N ≥ 1). If N = 0, the create was a no-op — do NOT pull. Never stage un-pushed field-definition edits on existing workflows into the same batch as a new-workflow create-then-pull. New-workflow creation must be agent-owned end-to-end (the orchestrator cannot split edit → push → pull → re-stage across agents; the renamed dir path isn't known until after pull).
45
+
46
+ ---
53
47
 
54
48
  ## File Structure
55
49
 
@@ -57,7 +51,6 @@ workspace/
57
51
  workspace/
58
52
  ├── workflows.ts ← Workflow registry
59
53
  ├── enums.ts ← AUTO-GENERATED (never edit)
60
- ├── hailer.d.ts ← AUTO-GENERATED (never edit)
61
54
  ├── teams.ts, groups.ts ← Access control
62
55
  ├── insights.ts ← SQL-like reports
63
56
  └── [Workflow]_[id]/
@@ -76,282 +69,125 @@ workspace/
76
69
 
77
70
  ```typescript
78
71
  export const workflows: WorkflowEntry[] = [
79
- // NEW workflow - omit _id and folder
80
- {
81
- name: "Tasks",
82
- enableUnlinkedMode: false
83
- },
84
- // EXISTING workflow - has _id and folder
85
- {
86
- _id: "682ac815fba468d857d498f7",
87
- name: "Tasks",
88
- enableUnlinkedMode: false,
89
- folder: "tasks_682ac815fba468d857d498f7"
90
- }
72
+ // NEW - omit _id and folder
73
+ { name: "Tasks", enableUnlinkedMode: false },
74
+ // EXISTING - has _id and folder
75
+ { _id: "682ac815fba468d857d498f7", name: "Tasks", enableUnlinkedMode: false, folder: "tasks_682ac815fba468d857d498f7" }
91
76
  ];
92
77
  ```
93
78
 
94
- | Property | Description |
95
- |----------|-------------|
96
- | `name` | Display name |
97
- | `enableUnlinkedMode` | `true` = standalone items, `false` = must link to other activities |
98
- | `_id` | Server-generated (omit for new) |
99
- | `folder` | Auto-generated on pull (omit for new) |
100
-
101
- <critical-rules>
102
- ## CRITICAL: Workflows Require a Default Team
79
+ `WorkflowEntry` schema is `{_id, name, folder, enableUnlinkedMode}` only. Do not add `key` or `description` — the server rejects them at that level. `key` is set separately via `auto_set_keys` or push path.
103
80
 
104
- **This is the #1 cause of "can't create activities" errors.**
105
-
106
- When you create a new workflow, it has NO default team. Without a default team:
107
- - `mcp__hailer__create_activity` fails
108
- - Apps using `hailer.activity.create()` fail
109
- - You get confusing permission/validation errors
110
-
111
- **Solution:** Always set a default team immediately after creating a workflow:
112
-
113
- 1. Create the workflow
114
- 2. In Hailer UI: Workflow Settings → Default Team → Select any team
115
- 3. Or via SDK: Add `defaultTeam: TeamIds.your_team` to workflow config
116
-
117
- **For development/testing:** Any team works. Just pick one so activity creation functions properly.
118
- </critical-rules>
119
-
120
- ---
81
+ **CRITICAL: Workflows Require a Default Team.** Without a default team, `create_activity` fails with confusing errors. After creating a workflow: Hailer UI → Workflow Settings → Default Team.
121
82
 
122
83
  ### Workflow Creation Flow
123
84
 
124
85
  **Order matters: Workflow → Phases → Fields → Phase Links → Workflow Settings**
125
86
 
126
- All items get server-generated IDs. You must push each step and pull to get the IDs before referring to them.
127
-
128
87
  ```
129
88
  1. npm run pull
130
- 2. Edit workflows.ts (add new entry without _id/folder)
131
- 3. Return ["npm run workflows-sync:force"]
132
- 4. npm run pull (generates folder + enums with workflow ID)
89
+ 2. Edit workflows.ts (add entry without _id/folder)
90
+ 3. npm run workflows-sync:force
91
+ 4. Verify "✨ Created: N" in output if N=0, do NOT pull
92
+ 5. npm run pull ← generates folder + enums with workflow ID
133
93
 
134
- 5. Edit phases.ts (add phases without _id, empty fields array)
135
- 6. Return ["npm run phases-push:force"]
136
- 7. npm run pull (generates phase IDs in enums)
94
+ 6. Edit phases.ts (add phases without _id, empty fields array)
95
+ Use plain name strings only — enums don't exist yet for new workflows.
96
+ 7. npm run phases-push:force
97
+ 8. npm run pull ← generates phase IDs in enums
137
98
 
138
- 8. Edit fields.ts (add fields without _id)
139
- 9. Return ["npm run fields-push:force"]
140
- 10. npm run pull (generates field IDs in enums)
99
+ 9. Edit fields.ts (add fields without _id)
100
+ 10. npm run fields-push
101
+ 11. npm run pull generates field IDs in enums
141
102
 
142
- 11. Edit phases.ts:
143
- - Add field IDs to phase.fields arrays
144
- - Add possibleNextPhase arrays (phase transition links)
145
- 12. Return ["npm run phases-push:force"]
103
+ 12. Edit phases.ts: add field IDs to phase.fields arrays + possibleNextPhase
104
+ 13. npm run phases-push:force
105
+ (NO pull updates only, no new IDs)
146
106
 
147
- 13. Edit main.ts: Set primaryDateField and primaryNumberField
148
- 14. Return ["npm run push:force"]
107
+ 14. (If needed) Edit main.ts: add nameFunction + nameFunctionVariables
108
+ 15. npm run workflows-push
109
+ (NO pull — nameFunction is local, pull would overwrite it)
149
110
  ```
150
111
 
151
112
  ### Workflow Permissions (main.ts)
152
113
 
153
- Control who can access the workflow and what they can do.
154
-
155
114
  ```typescript
156
- // In main.ts
157
115
  export const workflowConfig = {
158
116
  _id: WorkflowIds.tasks_abc,
159
117
  name: "Tasks",
160
-
161
- // WHO CAN ACCESS THIS WORKFLOW
162
118
  members: [
163
- {
164
- id: HailerMembers.timo_ahonen_94d, // User ID
165
- permissions: ["admin"] // Full control
166
- },
167
- {
168
- id: WorkspaceTeams.sales_team_fc0, // Team ID
169
- permissions: ["any"] // Standard access
170
- }
119
+ { id: HailerMembers.timo_ahonen_94d, permissions: ["admin"] },
120
+ { id: WorkspaceTeams.sales_team_fc0, permissions: ["any"] }
171
121
  ],
172
-
173
- // DEFAULT TEAM for new activities
174
- preselectedTeam: {
175
- account: "workspace_account_id",
176
- team: WorkspaceTeams.default_team_abc
177
- },
122
+ preselectedTeam: { account: "workspace_account_id", team: WorkspaceTeams.default_team_abc },
178
123
  enablePreselectedTeam: true,
179
-
180
- // GUEST ACCESS
181
- allowGuests: false, // Allow external guests
182
- enableGuestEditing: false, // Can guests edit?
183
-
184
- // ... other config
185
124
  };
186
125
  ```
187
126
 
188
- **Permission levels:**
189
- | Level | Meaning |
190
- |-------|---------|
191
- | `"admin"` | Full control - can edit workflow settings, delete activities |
192
- | `"any"` | Standard access - can view, create, edit based on phase settings |
127
+ `members[]` (and any `HailerMemberId`-typed field) requires `HailerMembers.*` — the prefixed enum that has both Users + Teams sections. `WorkspaceTeams.*` is plain hex and fails TS2322.
193
128
 
194
- **Member types:**
195
- - Users: `HailerMembers.user_name_id`
196
- - Teams: `WorkspaceTeams.team_name_id`
129
+ ---
197
130
 
198
- ### Phase Permissions (phases.ts)
131
+ ## Workflow Settings (workflowConfig toggles)
199
132
 
200
- Control visibility and behavior per phase.
133
+ `primaryDateField` / `primaryNumericField` are **phase-level** — set them in `phases.ts`, not here.
201
134
 
202
- ```typescript
203
- // In phases.ts
204
- export const phases = [
205
- {
206
- _id: PhaseIds.in_progress_def,
207
- name: "In Progress",
208
-
209
- // FIELD VISIBILITY - only these fields visible in this phase
210
- fields: [
211
- FieldIds.title_abc,
212
- FieldIds.assignee_def,
213
- FieldIds.due_date_ghi
214
- ],
215
-
216
- // AUTO-FOLLOWERS - notified when activity enters this phase
217
- followers: [
218
- WorkspaceTeams.managers_team_abc, // Team
219
- "user_id_123" // Or specific user
220
- ],
221
-
222
- // PHASE TRANSITIONS - where activities can move TO from here
223
- possibleNextPhase: [
224
- PhaseIds.review_ghi,
225
- PhaseIds.done_jkl
226
- ],
227
-
228
- // TRANSITION SETTINGS (optional)
229
- possibleNextPhaseSettings: {
230
- [PhaseIds.done_jkl]: {
231
- requireComment: true // Must add comment when moving to Done
232
- }
233
- },
234
-
235
- // WEBHOOKS - trigger external systems
236
- webhooksEnabled: true,
237
- webhookUrl: "https://hooks.example.com/phase-entered"
238
- }
239
- ];
240
- ```
135
+ | Toggle | Type | Notes |
136
+ |--------|------|-------|
137
+ | `name` | `string` | Change in `main.ts` AND `workflows.ts` before pushing. |
138
+ | `key` | `string` | Human-readable ID (e.g. `"workOrders"`). See Keys section. |
139
+ | `defaultView` | `"table"\|"kanban"\|"wall"\|"calendar"` | View mode on first open. |
140
+ | `enableUnlinkedMode` | `boolean` | `true` = standalone dataset list; `false` = requires linking (kanban). Naming is inverted from intuition. |
141
+ | `nameEditable` | `boolean` | `false` when `nameFunction` drives the title. |
142
+ | `fieldsOrder` | `string[]` | Workflow-level field column order. |
143
+ | `phasesOrder` | `string[]` | Phase display order. |
144
+ | `allowGuests` | `boolean` | Grants guest/external users view access via shared link. |
145
+ | `enableGuestEditing` | `boolean` | Lets guests edit. Requires `allowGuests: true`. |
146
+ | `enableAttachments` | `boolean` | File attachments panel on activities. |
147
+ | `enableMapLocation` | `boolean` | Map/location picker on activities. |
148
+ | `enableMessenger` | `boolean` | Private messenger thread per activity. |
149
+ | `inviteActivityCreator` | `boolean` | Automatically adds the creator as a discussion follower. Use this instead of building creator-invite logic in an app. |
150
+ | `enableUniqueName` | `boolean` | Server rejects create/update if the activity name already exists in the workflow. |
151
+ | `discussionPermissions` | `DiscussionPermission[]` | Controls what members can do in the discussion. |
241
152
 
242
- **Key phase permissions:**
243
- | Setting | Purpose |
244
- |---------|---------|
245
- | `fields[]` | Which fields are visible/editable in this phase |
246
- | `followers[]` | Users/teams auto-notified for activities in this phase |
247
- | `possibleNextPhase[]` | Allowed phase transitions (empty = cannot move out) |
248
- | `isInitial` | New activities start here |
249
- | `isEndpoint` | Activities here are "complete" |
153
+ **Activity tagging is not configurable via `workflows-push`.** `normalizeWorkflow` strips `enableActivityTagging` from the payload. Configure activity tagging via `process.set_info` / the Hailer UI. Same class as workflow `translations`.
250
154
 
251
- ### Teams & Groups (workspace root)
252
-
253
- Teams and groups are defined at workspace root level (not per-workflow).
254
-
255
- **Location:** `workspace/teams.ts` and `workspace/groups.ts`
155
+ ### Name Function Pattern (main.ts)
256
156
 
257
157
  ```typescript
258
- // workspace/teams.ts
259
- import { WorkspaceMembers, WorkspaceTeams } from "./enums";
260
-
261
- export const teams: HailerTeamUpdatePayload[] = [
262
- {
263
- _id: WorkspaceTeams.sales_team_abc,
264
- name: "Sales Team",
265
- description: "Sales department members",
266
- members: [
267
- WorkspaceMembers.john_doe_123,
268
- WorkspaceMembers.jane_smith_456
269
- ],
270
- public: false, // Only members can see team activities
271
- defaultView: {
272
- type: "app", // "app" | "workflow" | "phase"
273
- value: "app_id_here" // ID of default view
274
- }
275
- }
276
- ];
277
- ```
278
-
279
- **Team properties:**
280
- | Property | Purpose |
281
- |----------|---------|
282
- | `_id` | Team ID (from enums after first push) |
283
- | `name` | Display name |
284
- | `description` | Team description |
285
- | `members[]` | Array of user IDs (WorkspaceMembers enum) |
286
- | `public` | If true, visible to all workspace users |
287
- | `defaultView` | What opens when team is selected |
288
-
289
- **Groups** (less common) are collections of teams:
290
- ```typescript
291
- // workspace/groups.ts
292
- export const groups: HailerGroupUpdatePayload[] = [
293
- {
294
- _id: GroupIds.all_managers_abc,
295
- name: "All Managers",
296
- teams: [TeamIds.sales_managers, TeamIds.tech_managers],
297
- users: [WorkspaceMembers.ceo_123] // Direct user members
298
- }
299
- ];
300
- ```
301
-
302
- **Push commands:**
303
- ```bash
304
- npm run teams-push:force # Push team changes
305
- npm run groups-push:force # Push group changes
158
+ nameEditable: false,
159
+ nameFunction: "const meta = dep.activityMeta; if (!meta) return 'PREFIX-' + Date.now(); const created = Number(meta.created) || Date.now(); const year = new Date(created).getUTCFullYear(); const seq = Number(meta.sequence) || 0; let seqStr = ''; if (seq > 0) { seqStr = String(seq); while (seqStr.length < 4) seqStr = '0' + seqStr; } else { const id = meta._id || '0000'; seqStr = id.slice(-4); } return 'PREFIX-' + year + '-' + seqStr;",
160
+ nameFunctionEnabled: true,
161
+ nameFunctionVariables: {
162
+ activityMeta: { data: ["data"], type: "=" } // magic key is "data" (NOT "meta" — "meta" silently returns null). data is plain STRINGS, not objects. type: "=" is required.
163
+ },
306
164
  ```
307
165
 
308
- **Where teams are used:**
309
- - Workflow `members[]` - who can access workflow
310
- - Workflow `preselectedTeam` - default team for new activities
311
- - Phase `followers[]` - auto-notify teams
312
- - App permissions - grant team access to apps
166
+ `nameFunction` push does NOT retroactively recompute existing activity names — existing activities keep their stored titles until next individual save.
313
167
 
314
168
  ---
315
169
 
316
- <critical-rules>
317
- ### CRITICAL: Phase Links (possibleNextPhase)
318
-
319
- **Problem:** Without phase links, users cannot move activities between phases.
320
-
321
- Every phase needs `possibleNextPhase` array defining allowed transitions:
170
+ ## Teams & Groups
322
171
 
323
172
  ```typescript
324
- {
325
- _id: Tasks_PhaseIds.lead_abc,
326
- name: "Lead",
327
- isInitial: true,
328
- possibleNextPhase: [
329
- Tasks_PhaseIds.qualified_def,
330
- Tasks_PhaseIds.lost_ghi
331
- ]
332
- }
333
- ```
334
-
335
- **Without this:** Activities are stuck in their initial phase forever.
336
- </critical-rules>
337
-
338
- <critical-rules>
339
- ### CRITICAL: Primary Date and Number Fields
340
-
341
- **Set in main.ts** to enable calendar view and number aggregation:
173
+ // workspace/teams.ts — push: teams-push:force
174
+ { _id: WorkspaceTeams.sales_abc, name: "Sales", members: [WorkspaceMembers.john_123], public: false }
342
175
 
343
- ```typescript
344
- // workspace/[Workflow]_[id]/main.ts
345
- export const workflowConfig = {
346
- // ... other config
347
- primaryDateField: Tasks_FieldIds.due_date_abc, // For calendar view
348
- primaryNumberField: Tasks_FieldIds.amount_def, // For aggregations
349
- };
176
+ // workspace/groups.ts — push: groups-push:force
177
+ { _id: GroupIds.managers_abc, name: "Managers", teams: [TeamIds.sales], users: [WorkspaceMembers.ceo_123] }
350
178
  ```
351
179
 
352
- **Without primaryDateField:** Activities won't appear on the calendar.
353
- **Without primaryNumberField:** Number aggregations won't work in views.
354
- </critical-rules>
180
+ **Teams single-pass creation:**
181
+ 1. Add object **without `_id`** to `workspace/teams.ts`
182
+ 2. `npm run teams-push` — push itself creates the team
183
+ 3. `npm run pull` — writes server-assigned `_id` into `enums.ts`
184
+
185
+ **Groups — two-pass creation (group `teams` can't be set in the same push that creates the group):**
186
+ 1. Add group without `_id` and with empty `teams: []` to `workspace/groups.ts`
187
+ 2. `npm run groups-push` — creates the group
188
+ 3. `npm run pull` — gets the generated `_id`
189
+ 4. Add desired `teams` array using enum symbols now in `enums.ts`
190
+ 5. `npm run groups-push` again — sets the team membership
355
191
 
356
192
  ---
357
193
 
@@ -360,153 +196,107 @@ export const workflowConfig = {
360
196
  ### Basic Field Structure
361
197
 
362
198
  ```typescript
363
- // In fields.ts
364
- import { Tasks_FieldIds, WorkflowIds } from "../enums";
365
-
366
199
  export const fields: HailerFieldGeneric[] = [
367
200
  // NEW field - omit _id
368
- {
369
- label: "Due Date",
370
- type: "date",
371
- required: false
372
- },
201
+ { label: "Due Date", type: "date", required: false },
373
202
  // EXISTING field - use enum for _id
374
- {
375
- _id: Tasks_FieldIds.due_date_abc,
376
- label: "Due Date",
377
- type: "date",
378
- required: false
379
- }
203
+ { _id: Tasks_FieldIds.due_date_abc, label: "Due Date", type: "date", required: false }
380
204
  ];
381
205
  ```
382
206
 
383
- ### Common Field Properties
207
+ **CRITICAL: Never invent an `_id` for a new item.**
384
208
 
385
- | Property | Type | Description |
386
- |----------|------|-------------|
387
- | `_id` | string | Field ID (omit for new, use enum for existing) |
388
- | `label` | string | Display name shown in UI |
389
- | `type` | string | Field type (see below) |
390
- | `key` | string | URL-safe identifier (see Key Naming below) |
391
- | `description` | string | Help text shown below field in forms |
392
- | `required` | boolean | Is field required |
393
- | `defaultTo` | boolean | Use default value |
394
- | `defaultValue` | string | Default value |
395
- | `inviteToDiscussionOnChange` | boolean | Notify discussion on change |
396
-
397
- ### Field Key Naming
398
-
399
- The `key` property is an optional URL-safe identifier for the field. If omitted, Hailer auto-generates one from the label.
400
-
401
- **Conventions:**
402
- - Use `snake_case` (lowercase with underscores)
403
- - Keep it short but descriptive
404
- - Use consistent prefixes for related fields
405
- - Avoid reserved words
406
-
407
- **Examples:**
408
- ```typescript
409
- // Good keys
410
- { label: "Customer Name", key: "customer_name" }
411
- { label: "Invoice #", key: "invoice_number" }
412
- { label: "Due Date", key: "due_date" }
413
- { label: "Total (€)", key: "total_eur" }
414
-
415
- // Prefixed for grouping
416
- { label: "Billing Address", key: "billing_address" }
417
- { label: "Billing City", key: "billing_city" }
418
- { label: "Shipping Address", key: "shipping_address" }
419
- { label: "Shipping City", key: "shipping_city" }
420
- ```
209
+ Two valid shapes for `_id`:
210
+ 1. **New item — the `_id` property is absent.** Not `null`, not `""`, not `"new"`. The key doesn't appear at all.
211
+ 2. **Existing item — `_id` references an enum symbol that already exists in `enums.ts`**.
421
212
 
422
- **When to specify `key`:**
423
- - Always for fields used in integrations (APIs, Zapier, webhooks)
424
- - Always for fields referenced in insights or functions
425
- - Optional for internal-only fields (auto-generated is fine)
213
+ Known failure modes:
214
+ - `_id: "new_field_1"` server rejects or silently miswrites
215
+ - `_id: "000000000000000000000001"` fake hex, same failure
216
+ - `_id: Tasks_FieldIds.not_yet_in_enums` — TypeScript catches this only with `npx tsc --noEmit`
217
+ - `_id: Tasks_FieldIds.some_existing_field` copied from neighbor — silently overwrites that field
426
218
 
427
- ### Non-Obvious Field Type Notes
219
+ ### fields-push slow ack — not a hang
428
220
 
429
- **Checkbox** not a distinct type. Use `numeric` + `modifier.checkbox: true` (stores `1`/`0`).
221
+ `npm run fields-push` can appear to hang for minutes. This is **slow server-side backfill**, not a deadlock. Fields in `json_snapshot`'s dependency graph trigger synchronous snapshot recomputation across every existing activity.
430
222
 
431
- **File upload** — use `text` + `modifier.file: true`.
223
+ **Fire-and-forget recipe:**
432
224
 
433
225
  ```typescript
434
- // Checkbox
435
- { label: "Is Active", type: "numeric", modifier: { checkbox: true, file: false } }
436
-
437
- // File upload
438
- { label: "Attachments", type: "text", modifier: { checkbox: false, file: true } }
439
- ```
440
-
441
- **`linkedfrom`** (backlink) — read-only, auto-populated. Supports `modifier.quickAdd` to create related activities inline:
442
-
443
- ```typescript
444
- {
445
- type: "linkedfrom",
446
- label: "Orders",
447
- data: [WorkflowIds.orders_def],
448
- modifier: {
449
- quickAdd: {
450
- fieldIds: [], // Fields shown in quick-add form
451
- targetFieldId: Orders_FieldIds.customer_link
452
- }
453
- }
226
+ const TIMEOUT_MS = 30_000;
227
+ for (const field of fieldsToPush) {
228
+ await Promise.race([
229
+ client.updateField(workflowId, field._id, field),
230
+ new Promise((_, rej) => setTimeout(() => rej(new Error('ack pending')), TIMEOUT_MS))
231
+ ]).catch(err => {
232
+ if (err.message === 'ack pending') {
233
+ console.log(`Field ${field.label}: ack pending server is still processing`);
234
+ } else { throw err; }
235
+ });
454
236
  }
237
+ // After all calls fired, verify: npm run pull → count fields in enums.ts
455
238
  ```
456
239
 
457
- **`activitylink`** `data` must be a plain string array of workflow IDs (not objects):
458
- ```typescript
459
- data: [WorkflowIds.customers_abc] // ✅
460
- data: [{ workflowId: "697b9b54..." }] // ❌ causes API error
461
- ```
462
-
463
- **`textpredefinedoptions`** — `data` is a string array (not `options`, not objects):
464
- ```typescript
465
- { type: "textpredefinedoptions", data: ["High", "Medium", "Low"] } // ✅
466
- { type: "dropdown", options: [...] } // ❌
467
- ```
240
+ ### Common Field Properties
468
241
 
469
- **No multi-select types exist.** Use multiple fields if needed.
242
+ | Property | Description |
243
+ |----------|-------------|
244
+ | `_id` | Omit for new, use enum for existing |
245
+ | `label` | Display name |
246
+ | `type` | Field type (see below) |
247
+ | `key` | URL-safe identifier (snake_case). Specify for integration/insight fields. |
248
+ | `description` | Help text shown below field in forms |
249
+ | `required` | Mark essential fields required. Don't mark everything required — fields not yet known at creation time add friction. |
250
+ | `defaultValue` | Default value |
251
+
252
+ ### Field Types
253
+
254
+ | Type | Use For | Config Notes |
255
+ |------|---------|-------------|
256
+ | `text` | Short text | |
257
+ | `textarea` | Long text | |
258
+ | `textunit` / `numericunit` | Text/number with unit | Set `unit` property. Prefer `numericunit` over `numeric` for most numeric fields — adding a unit (€, kg, h) makes the field self-documenting. |
259
+ | `numeric` | Numbers | NOT `number`! Unit-less values only. |
260
+ | `date` / `datetime` | Single date/datetime | |
261
+ | `daterange` / `datetimerange` | Date spans | |
262
+ | `time` / `timerange` | Time only | |
263
+ | `textpredefinedoptions` | Dropdown | `data: ["High", "Medium", "Low"]` (string array, NOT `options`, NOT `[{label,value}]`) |
264
+ | `users` | Single-user picker | |
265
+ | `teams` | Single-team picker | Token is `teams` (not `team`) |
266
+ | `activitylink` | Link to activity | `data: [WorkflowIds.target]` (plain string array!) |
267
+ | `linkedfrom` | Backlink (read-only) | Auto-populated |
268
+ | `country` | Country picker | ISO code |
269
+ | `subheader` | Section divider | UI only, use `collapsedByDefault: true` for grouping |
270
+ | `numeric` + `modifier.checkbox` | Checkbox | |
271
+ | `text` + `modifier.file` | File upload | |
272
+
273
+ **No multi-select field types exist in Hailer — period.**
470
274
 
471
- **`time` / `timerange`** — stored as full Unix timestamp (ms) but UI shows only the time portion. Extract with `new Date(val).getUTCHours()`.
275
+ ```typescript
276
+ // Dropdown
277
+ { type: "textpredefinedoptions", label: "Priority", data: ["High", "Medium", "Low"] }
472
278
 
473
- ---
279
+ // Activity link
280
+ { type: "activitylink", label: "Customer", data: [WorkflowIds.customers, WorkflowIds.projects] }
281
+ // data MUST be plain string array — data: [{workflowId: "..."}] causes API error
474
282
 
475
- #### Calculated Function Fields
283
+ // Checkbox / File
284
+ { label: "Active", type: "numeric", modifier: { checkbox: true, file: false } }
285
+ { label: "Attachments", type: "text", modifier: { checkbox: false, file: true } }
476
286
 
477
- ```typescript
478
- {
479
- _id: Tasks_FieldIds.total_abc,
480
- label: "Total",
481
- type: "numericunit", // Output type
482
- unit: "€",
483
- functionEnabled: true,
484
- function: "@function:total_abc", // Function reference
485
- functionVariables: { // Input dependencies
486
- quantity: {
487
- type: "=",
488
- data: [Tasks_FieldIds.quantity_def]
489
- },
490
- price: {
491
- type: "=",
492
- data: [Tasks_FieldIds.unit_price_ghi]
493
- }
494
- }
495
- }
287
+ // numericunit — unit is a TOP-LEVEL property, NOT inside data[]
288
+ { label: "Amount", type: "numericunit", unit: "€", data: [] }
289
+ // WRONG: { type: "numericunit", data: [{ unit: "€" }] } — API rejects unit inside data array
496
290
  ```
497
291
 
498
- **See `sdk-function-fields` skill for complete function field guide.**
292
+ ### Field Limitations
499
293
 
500
- ---
294
+ - Cannot change field type after creation — delete and create new; new `_id` cascades into enums, phases, function field variables.
295
+ - `linkedfrom` adding on a NEW workflow requires 2-pass push: push source workflow first → pull → push `linkedfrom` reference.
501
296
 
502
- #### Field Type Limitations
297
+ ### Field Visibility
503
298
 
504
- | Limitation | Details |
505
- |------------|---------|
506
- | **Cannot change type** | Once created, field type cannot be changed via API. Must delete and recreate (loses data) or change in Hailer UI. |
507
- | **linkedfrom is read-only** | Cannot set values - auto-populated from links |
508
- | **richtext size** | Large HTML can slow down activity loading |
509
- | **attachment storage** | Files stored in Hailer, count toward workspace storage |
299
+ When `fields-push` creates a new field, the server appends it to every phase's `fields[]` array automatically — field is visible everywhere immediately. Only push `phases.ts` if you want non-default placement (reordering, removing from specific phases).
510
300
 
511
301
  ---
512
302
 
@@ -515,339 +305,131 @@ data: [{ workflowId: "697b9b54..." }] // ❌ causes API error
515
305
  ### Basic Phase Structure
516
306
 
517
307
  ```typescript
518
- // In phases.ts
519
- import { Tasks_FieldIds, Tasks_PhaseIds } from "../enums";
520
-
521
308
  export const phases: HailerPhaseUpdatePayload[] = [
522
309
  // NEW phase - omit _id
523
- {
524
- name: "Todo",
525
- color: "#57636E",
526
- fields: [],
527
- possibleNextPhase: []
528
- },
529
- // EXISTING phase - use enum for _id
310
+ { name: "Todo", color: "#57636E", fields: [], possibleNextPhase: [] },
311
+ // EXISTING phase - use enum
530
312
  {
531
313
  _id: Tasks_PhaseIds.todo_abc,
532
314
  name: "Todo",
533
315
  color: "#57636E",
534
- fields: [
535
- Tasks_FieldIds.title_def,
536
- Tasks_FieldIds.assignee_ghi
537
- ],
538
- possibleNextPhase: [
539
- Tasks_PhaseIds.in_progress_jkl,
540
- Tasks_PhaseIds.done_mno
541
- ]
316
+ fields: [Tasks_FieldIds.title_def, Tasks_FieldIds.assignee_ghi],
317
+ possibleNextPhase: [Tasks_PhaseIds.in_progress_jkl, Tasks_PhaseIds.done_mno]
542
318
  }
543
319
  ];
544
320
  ```
545
321
 
546
- ### Phase Properties Reference
322
+ ### Phase Properties
547
323
 
548
324
  | Property | Type | Description |
549
325
  |----------|------|-------------|
550
326
  | `_id` | string | Phase ID (omit for new) |
551
327
  | `name` | string | Display name |
552
- | `color` | string | Hex color (e.g., "#57636E") |
553
- | `description` | string | Phase description |
554
-
555
- ### Phases Can Represent Different Things
328
+ | `key` | string | URL-safe identifier (snake_case) |
329
+ | `color` | string | Hex color — set on every phase |
330
+ | `fields[]` | FieldId[] | Fields visible in this phase |
331
+ | `isInitial` | boolean | New activities start here |
332
+ | `isEndpoint` | boolean | Activities here are "complete" |
333
+ | `possibleNextPhase[]` | PhaseId[] | Allowed transitions — without this, activities are stuck forever |
334
+ | `followers[]` | string[] | Users/teams auto-notified |
556
335
 
557
- **Not all phases are lifecycle stages.** Check semantics before assuming flow:
336
+ **CRITICAL: Phase Links (possibleNextPhase) without them, activities are stuck forever.**
558
337
 
559
- - **Lifecycle phases:** Draft Review Active Archived
560
- - **Category phases:** Products workflow might use phases as categories (Doors, Windows, etc.)
561
- - **Status phases:** Support tickets with Open, Waiting, Resolved
338
+ **CRITICAL: Primary Date and Number Fields are phase properties, not workflow properties.**
562
339
 
563
- Always ask the user about phase semantics when creating workflows.
340
+ | Property | Accepted values | Use a custom field ID? |
341
+ |---|---|---|
342
+ | `primaryDateField` | `"created"` \| `"updated"` \| `"completedOn"` \| `undefined` | No — TS rejects custom field IDs |
343
+ | `primaryNumericField` | `string` (field ID) \| `undefined` | Yes — pass the field ID enum |
564
344
 
565
- #### Field Visibility (IMPORTANT)
345
+ Setting them via `process.set_info` (workflow-level) fails with error code 191.
566
346
 
567
- **Fields are NOT automatically visible in all phases.**
347
+ **Dropping a phase is destructive all activities in that phase are cascade-deleted.** Verify count before removing (`list_activities` with `countOnly:true`). Move activities to another phase first. No undo.
568
348
 
569
- Each phase has a `fields` array that controls which fields are visible when an activity is in that phase. A field can be:
570
- - Visible in ALL phases (add to every phase's `fields` array)
571
- - Visible in SOME phases (add only to specific phases)
572
- - Hidden in certain phases (omit from those phases)
349
+ **`possibleNextPhase` enforces phase walks — intermediate phases cannot be skipped.** If a phase only allows certain next phases, jumping to a non-adjacent phase fails silently with `[object Object]` error. Walk the chain in hops.
573
350
 
574
- ```typescript
575
- // Phase 1: Only basic fields visible
576
- {
577
- _id: Tasks_PhaseIds.draft_abc,
578
- name: "Draft",
579
- fields: [
580
- Tasks_FieldIds.title_def,
581
- Tasks_FieldIds.description_ghi
582
- // assignee and due_date NOT visible in draft
583
- ]
584
- }
585
-
586
- // Phase 2: More fields become visible
587
- {
588
- _id: Tasks_PhaseIds.active_jkl,
589
- name: "Active",
590
- fields: [
591
- Tasks_FieldIds.title_def,
592
- Tasks_FieldIds.description_ghi,
593
- Tasks_FieldIds.assignee_mno, // NOW visible
594
- Tasks_FieldIds.due_date_pqr // NOW visible
595
- ]
596
- }
597
- ```
351
+ **Disabling a phase webhook:** Set `webhooksEnabled: false` and keep `webhookUrl` as any non-empty string. Setting `webhookUrl: ""` causes error 191. `webhookAdded`/`webhookUpdated` flags are immutable from SDK side — flip the webhook toggle in Hailer UI instead.
598
352
 
599
- **When adding a new field:**
600
- 1. Push the field first (`fields-push:force`)
601
- 2. Pull to get the field ID in enums
602
- 3. Decide which phases should show this field
603
- 4. Add the field ID to those phases' `fields` arrays
604
- 5. Push phases (`phases-push:force`)
353
+ ### Phase Configuration Defaults
605
354
 
606
- #### Phase Flow Control
607
- ```typescript
608
- {
609
- isInitial: true, // Starting phase (activities created here)
610
- isEndpoint: true, // Final phase (workflow complete)
611
- possibleNextPhase: [ // Allowed transitions
612
- Tasks_PhaseIds.review_abc,
613
- Tasks_PhaseIds.done_def
614
- ],
615
- possibleNextPhaseSettings: {
616
- // Phase transition settings (optional)
617
- }
618
- }
619
- ```
620
-
621
- #### Calendar Integration
622
- ```typescript
623
- {
624
- primaryDateField: Tasks_FieldIds.due_date_abc // Show on calendar
625
- // OR
626
- primaryDateField: "updated" // Use last updated timestamp
627
- }
628
- ```
629
-
630
- #### Auto-Follow
631
- ```typescript
632
- {
633
- followers: [
634
- "user_id_abc", // User ID
635
- "team_id_def" // Team ID
636
- ]
637
- // These users/teams auto-follow activities in this phase
638
- }
639
- ```
640
-
641
- #### Webhooks
642
- ```typescript
643
- {
644
- webhooksEnabled: true,
645
- webhookUrl: "https://hooks.zapier.com/..."
646
- // Triggers when activity enters this phase
647
- }
648
- ```
649
-
650
- #### Announcements
651
- ```typescript
652
- {
653
- announcementFields: [
654
- Tasks_FieldIds.title_abc,
655
- Tasks_FieldIds.due_date_def
656
- ],
657
- announcementFieldsOrder: [
658
- Tasks_FieldIds.title_abc,
659
- Tasks_FieldIds.due_date_def
660
- ],
661
- announcementRecipients: [
662
- "team_id_abc"
663
- ]
664
- // Send notification when activity enters phase
665
- }
666
- ```
355
+ 1. **Set a `color` on every phase.** Uncoloured phases are indistinguishable on the board.
356
+ 2. **Set primaryDate/Numeric only when meaningful.** For reference/Register-archetype datasets, leaving both unset is correct.
667
357
 
668
358
  ---
669
359
 
670
- ## Push Commands
671
-
672
- | Command | What It Does |
673
- |---------|--------------|
674
- | `npm run workflows-sync:force` | Create/update/delete workflows |
675
- | `npm run fields-push:force` | Create/update/delete fields |
676
- | `npm run phases-push:force` | Create/update/delete phases |
677
- | `npm run push:force` | Push all changes |
678
-
679
- **:force variants** may delete resources not in local files.
680
- **Non-force variants** only update, never delete.
360
+ ## Human-Readable Keys (workflow / phase / field)
681
361
 
682
- ### Push vs Sync — What Each Command Does
362
+ **When keys matter:**
363
+ - Hailer apps (`@hailer/app-sdk`) — read fields by key via scaffolded `field-resolver.ts`
364
+ - MCP tools — accept keys anywhere they accept IDs
365
+ - Direct REST integrations — human-readable URLs/bodies
366
+ - Marketplace-distributable apps — must use keys (installed workflow IDs differ per tenant)
683
367
 
684
- | Command | Creates | Updates | Deletes |
685
- |---------|---------|---------|---------|
686
- | `workflows push` | No | Yes | No |
687
- | `workflows sync` | Yes | No | Yes |
688
- | `fields push` | Yes | Yes | Yes (with confirmation) |
689
- | `templates push` | No | Yes | No |
690
- | `templates sync` | Yes | No | Yes |
368
+ **When keys don't matter:** Insights (use enums), Function fields (enum substitution at compile time).
691
369
 
692
- **Key rule:** Use `sync` to create or delete; use `push` to update existing config. For fields, `push` handles all three operations.
370
+ ### Where keys work production-verified
693
371
 
694
- ### CRITICAL: Push/Pull Order
372
+ | Surface | Keys work? |
373
+ |---|---|
374
+ | MCP tools (`list_activities`, `create_activity`, etc.) | Yes |
375
+ | Direct REST — `v3.activity.list/createMany/updateMany` — `phaseId` and field refs | Yes |
376
+ | Direct REST — `processId` (workflow) | No — hex only |
377
+ | Direct REST — `v3.activity.list` response with `returnFlat: true` | Fields keyed by key name (if field has `key` set) |
378
+ | Insight SQL | No |
379
+ | Activity IDs | Never — always 24-char hex |
695
380
 
696
- **Push FIRST, then pull.** Running pull before push overwrites local changes.
381
+ ### Easiest setup `auto_set_keys`
697
382
 
698
383
  ```
699
- # CORRECT order when adding new fields locally:
700
- 1. Edit fields.ts (add new field)
701
- 2. npm run fields-push:force
702
- 3. npm run pull ← Now safe, gets generated IDs
703
-
704
- # WRONG order (loses your work):
705
- 1. Edit fields.ts
706
- 2. npm run pull ← Overwrites your changes!
384
+ auto_set_keys({ workflowId: 'hex-id' })
707
385
  ```
708
386
 
709
- **Why?** Pull fetches server state and overwrites local files. Your unpushed edits are lost.
387
+ - Derives camelCase keys from names/labels across the workflow, phases, and fields
388
+ - Skips entities that already have a key
389
+ - **Phase key writes fail silently in `auto_set_keys`** (known MCP bug). Set phase keys via push path instead: add `key` to each phase in `phases.ts` and run `npm run phases-push`.
390
+ - The push path is recommended for all three key types (workflow, field, phase) — they round-trip cleanly.
710
391
 
711
392
  ---
712
393
 
713
- ## Common Patterns
394
+ ## Push Commands Reference
714
395
 
715
- ### Organizing Large Workflows with Subheaders
396
+ ### Deleted Workflow Cleanup
716
397
 
717
- For workflows with 15+ fields, use `subheader` fields to group related fields into collapsible sections.
398
+ After deleting a workflow in Hailer and running `npm run pull`:
399
+ - The workflow's directory is auto-removed
400
+ - Enum references in other `workspace/*.ts` files are NOT cleaned up
718
401
 
719
- ```typescript
720
- // In fields.ts - organize fields into logical groups
721
- export const fields: HailerFieldGeneric[] = [
722
- // === BASIC INFO (always visible) ===
723
- { label: "Customer Name", type: "text", required: true },
724
- { label: "Contact Email", type: "text" },
725
-
726
- // === FINANCIAL SECTION (collapsible) ===
727
- {
728
- type: "subheader",
729
- label: "Financial Details",
730
- collapsedByDefault: true // Starts collapsed
731
- },
732
- { label: "Unit Price", type: "numericunit", unit: "€" },
733
- { label: "Quantity", type: "numeric" },
734
- { label: "Discount %", type: "numeric" },
735
- { label: "Total", type: "numericunit", unit: "€", functionEnabled: true, ... },
736
-
737
- // === DELIVERY SECTION (collapsible) ===
738
- {
739
- type: "subheader",
740
- label: "Delivery Information",
741
- collapsedByDefault: true
742
- },
743
- { label: "Delivery Address", type: "textarea" },
744
- { label: "Delivery Date", type: "date" },
745
- { label: "Delivery Notes", type: "textarea" },
746
-
747
- // === INTERNAL/HELPER FIELDS (hidden by default) ===
748
- {
749
- type: "subheader",
750
- label: "Internal Fields",
751
- collapsedByDefault: true
752
- },
753
- { label: "Integration ID", type: "text" },
754
- { label: "Last Sync", type: "datetime" }
755
- ];
402
+ After delete + pull:
403
+ ```bash
404
+ grep -r "deleted_workflow_id" workspace/
756
405
  ```
406
+ Fix stale references before next push.
757
407
 
758
- **When to use subheaders:**
759
- - Workflows with 15+ fields
760
- - Logical groupings exist (financial, delivery, contact, internal)
761
- - Some fields are rarely edited after initial entry
762
- - Helper/integration fields that clutter the UI
763
-
764
- **Subheader properties:**
765
- | Property | Description |
766
- |----------|-------------|
767
- | `type: "subheader"` | Required - marks as section divider |
768
- | `label` | Section title shown in UI |
769
- | `collapsedByDefault` | `true` = starts collapsed, `false` = starts expanded |
770
-
771
- **Common section patterns:**
772
- - "Basic Information" - core fields (often expanded)
773
- - "Financial Details" - prices, totals, discounts
774
- - "Delivery/Shipping" - addresses, dates
775
- - "Contact Information" - emails, phones
776
- - "Internal/System" - IDs, sync fields, helper fields (collapsed)
777
-
778
- **Note:** Subheaders have no data - they're UI-only for organization.
779
-
780
- ---
781
-
782
- ### Add Field to Existing Workflow
408
+ ### Renaming a Workflow
783
409
 
784
410
  ```
785
- 1. npm run pull
786
- 2. Edit workspace/[Workflow]_[id]/fields.ts
787
- - Add new field (omit _id)
788
- 3. Return ["npm run fields-push:force"]
789
- 4. npm run pull (to get generated field ID in enums)
790
- 5. Edit phases.ts - add new field ID to phase.fields arrays using enum
791
- 6. Return ["npm run phases-push:force"]
411
+ 1. Change name in workflows.ts AND main.ts
412
+ 2. npm run workflows-push
413
+ 3. npm run pull
792
414
  ```
793
415
 
794
- ### Progressive Field Visibility
795
-
796
- Common pattern: reveal more fields as activity progresses through phases.
797
-
798
- ```typescript
799
- // Draft: minimal fields
800
- { name: "Draft", fields: [title, description] }
801
-
802
- // Review: add reviewer
803
- { name: "Review", fields: [title, description, reviewer] }
416
+ After rename: re-grep `enums.ts` for actual generated member names — they often shift to Finnish-label-derived stems.
804
417
 
805
- // Active: full visibility
806
- { name: "Active", fields: [title, description, reviewer, assignee, due_date, priority] }
807
-
808
- // Done: keep all visible for reference
809
- { name: "Done", fields: [title, description, reviewer, assignee, due_date, priority] }
810
- ```
811
-
812
- ### Linear Phase Flow
418
+ ---
813
419
 
814
- ```typescript
815
- {
816
- _id: Tasks_PhaseIds.todo_abc,
817
- name: "Todo",
818
- isInitial: true,
819
- possibleNextPhase: [Tasks_PhaseIds.in_progress_def]
820
- },
821
- {
822
- _id: Tasks_PhaseIds.in_progress_def,
823
- name: "In Progress",
824
- possibleNextPhase: [Tasks_PhaseIds.done_ghi]
825
- },
826
- {
827
- _id: Tasks_PhaseIds.done_ghi,
828
- name: "Done",
829
- isEndpoint: true,
830
- possibleNextPhase: []
831
- }
832
- ```
420
+ ## Common Patterns
833
421
 
834
- ### Kanban-Style (Any to Any)
422
+ ### Add Field to Existing Workflow
423
+ 1. Edit `fields.ts` (omit `_id` for new) → `npm run fields-push` → `pull` (need new field IDs)
424
+ 2. Add field ID to relevant `phase.fields` arrays → `phases-push:force` (no pull needed)
835
425
 
836
- ```typescript
837
- const allPhases = [
838
- Tasks_PhaseIds.backlog_abc,
839
- Tasks_PhaseIds.todo_def,
840
- Tasks_PhaseIds.doing_ghi,
841
- Tasks_PhaseIds.done_jkl
842
- ];
426
+ ### Phase Flow Patterns
427
+ - **Linear:** Each phase points to next only. End phase: `isEndpoint: true, possibleNextPhase: []`
428
+ - **Kanban:** Each phase's `possibleNextPhase` includes all other phases
429
+ - **Progressive visibility:** Early phases show fewer fields, later phases show more
843
430
 
844
- // Each phase can move to any other
845
- {
846
- _id: Tasks_PhaseIds.todo_def,
847
- name: "Todo",
848
- possibleNextPhase: allPhases.filter(p => p !== Tasks_PhaseIds.todo_def)
849
- }
850
- ```
431
+ ### Subheaders
432
+ For 15+ fields, use `{ type: "subheader", label: "Section", collapsedByDefault: true }` to group.
851
433
 
852
434
  ---
853
435
 
@@ -856,53 +438,16 @@ const allPhases = [
856
438
  | Wrong | Right |
857
439
  |-------|-------|
858
440
  | Creating fields before phases | Order: Workflow → Phases → Fields |
859
- | Adding `_id` to new items | Omit `_id` for new (server generates) |
860
- | Referencing IDs before pull | Push Pull Then use new IDs from enums |
861
- | Hardcoding IDs | Use enums from `enums.ts` |
862
- | Editing `enums.ts` | Never edit - auto-generated |
863
- | Running push commands directly | Return to orchestrator (hooks) |
864
- | Pulling after local edits | Push first, then pull |
865
- | Assuming fields auto-visible | Fields only visible if in `phase.fields` array |
866
- | Adding field to all phases | Ask which phases need this field visible |
867
- | Forgetting to add field to phases | New fields need to be in `phase.fields` |
868
- | Changing field type via API | Field types CANNOT be changed - see below |
869
- | Including `key` on existing phases | `key` only for NEW phases - push fails on existing |
870
- | Forgetting `possibleNextPhase` | Activities stuck without phase transition links |
871
- | Missing `primaryDateField` | Activities won't show on calendar |
872
- | Two fields with same name/label | One will be removed during push — deduplicate labels! |
873
-
874
- ---
875
-
876
- ## CRITICAL: Field Types Cannot Be Changed via API
877
-
878
- **Problem:** Once a field is created, you cannot change its `type` via the SDK push commands.
879
-
880
- **Example:** Cannot change `textarea` → `activitylink` via `npm run fields-push`
881
-
882
- **Workarounds:**
883
- 1. Create a **new field** with the correct type (deprecate old one)
884
- 2. Change field type manually in **Hailer UI** (Settings → Workflow → Edit field)
885
- 3. Delete and recreate the field (loses existing data)
886
-
887
- **What CAN be changed via API:**
888
- - `label`, `description`, `required`
889
- - `data` (options for predefinedoptions, linked workflows for activitylink)
890
- - Field order in workflow
891
-
892
- ---
893
-
894
- ## Checklist
895
-
896
- Before pushing workflow changes:
897
-
898
- - [ ] Used enums for all existing IDs
899
- - [ ] Omitted `_id` for new items
900
- - [ ] **Asked user which phases should show new fields**
901
- - [ ] New fields added ONLY to relevant `phase.fields` arrays
902
- - [ ] Phase transitions make sense (`possibleNextPhase`)
903
- - [ ] Required fields have `required: true`
904
- - [ ] Link fields have correct workflow IDs in `data`
905
- - [ ] Using `:force` variant for commands that need it
906
-
907
- When adding a new field, ALWAYS ask:
908
- > "Which phases should this field be visible in? All phases or specific ones?"
441
+ | Adding `_id` to new items | Omit server generates |
442
+ | `npm run pull` before push | Push first, then pull only if new IDs needed |
443
+ | Hardcoding IDs / editing `enums.ts` | Use enums, never edit |
444
+ | Manually wiring new fields into phases | New fields auto-wire into ALL phases on creation; only push `phase.fields[]` for non-default placement |
445
+ | `workflows-push` for new workflow | `workflows-sync:force` |
446
+ | `type: "dropdown"`, `options: [...]` | `type: "textpredefinedoptions"`, `data: [...]` |
447
+ | Forgetting `possibleNextPhase` | Activities stuck forever |
448
+ | `{ type: "numericunit", data: [{ unit: "€" }] }` | `{ type: "numericunit", unit: "€", data: [] }` — unit is top-level |
449
+ | `enableUnlinkedMode: true` = requires linking | Inverted: `true` = standalone items (dataset), `false` = requires linking |
450
+ | `primaryDateField` in `main.ts` | Set on phases in `phases.ts` |
451
+ | `primaryNumberField` (typo) | `primaryNumericField` |
452
+ | Pulling without reading "✨ Created: N" first | If N=0, pull wipes local edits without creating anything |
453
+ | Batching un-pushed field additions with a new-workflow create | Un-pushed fields get wiped by the create-pull; push existing-workflow changes first |