@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
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hailer-workflow-archetypes
|
|
3
|
+
description: Pick the right Hailer workspace structural skeleton before building — 9 canonical archetypes (Register, Pipeline, Header+LineItems, Booking-Line, Event-Hub, Request-Fulfilment, Approval, Many-to-Many, Recurring Template). Load this skill at design time, before delegating any workflow or field creation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hailer Workflow Archetypes — Catalogue
|
|
7
|
+
|
|
8
|
+
<!-- Synthesised from 1383 real workflows across 39 sdk-projects workspaces (2026-06-16).
|
|
9
|
+
This is the WHICH-SHAPE catalogue. HOW to implement each shape lives in the cross-linked skills. -->
|
|
10
|
+
|
|
11
|
+
**Purpose:** Pick the canonical structural skeleton that fits the design problem, then load the owning mechanic skill for implementation detail. This skill is skeletons — field declarations, function-field bodies, and push/pull commands live in the cross-linked skills listed in the table and in the Cross-links section.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Archetype menu
|
|
16
|
+
|
|
17
|
+
| Archetype | When to use (one line) | Owning mechanic skill |
|
|
18
|
+
|-----------|------------------------|----------------------|
|
|
19
|
+
| **Register / Ledger** | Master data, lookup catalogs, reference tables, immutable logs | `sdk-ws-config-skill` |
|
|
20
|
+
| **Pipeline** | Single workflow tracking one record through named lifecycle stages | `hailer-design-patterns` |
|
|
21
|
+
| **Header + Line-Items** | Document-level header (order/invoice/BOM) that owns N child rows and needs rollup totals | `sdk-function-fields` (pivot/backlink aggregation) |
|
|
22
|
+
| **Resource-Reservation / Booking-Line** | Booking line sitting between an event/job header and a bookable resource catalog | `sdk-function-fields` (linked-data cross-workflow reads) |
|
|
23
|
+
| **Event-Aggregate Hub** | Central coordinator that fans out to multiple registers and aggregates from 3–8 distinct child workflows | `sdk-function-fields` (linked-data cross-workflow reads) |
|
|
24
|
+
| **Request ↔ Fulfilment** | Two linked workflows with genuinely different lifecycles and owners | `sdk-function-fields` (phase-cascade JSON payload) |
|
|
25
|
+
| **Approval Flow** | Single workflow with a binary approve/reject decision gate as its defining feature | `hailer-design-patterns` |
|
|
26
|
+
| **Many-to-Many Junction** | Pure join table connecting two registers in a M:N relationship — no multi-value field exists in Hailer | `sdk-ws-config-skill` |
|
|
27
|
+
| **Recurring Template** | Template/master record that gets cloned into time-bound per-occurrence instances | `hailer-design-patterns` |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
### Register / Ledger
|
|
32
|
+
|
|
33
|
+
**When to use:** Master data, reference tables, lookup catalogs, or immutable event logs that other workflows point at. Records live permanently in one place; phases (if any) are status buckets (Active/Archive), type categories, or year/version slots — never a forward process. Reach for this for customers, products, price lists, locations, employees, taxonomies, and append-only logs.
|
|
34
|
+
|
|
35
|
+
**Field skeleton:**
|
|
36
|
+
- Name / Title — `string` — primary identifier; often the nameColumnText
|
|
37
|
+
- Code / SKU / Reference — `string` — external/business key
|
|
38
|
+
- Category / Type — `textpredefinedoptions` — single-value dropdown, never an array
|
|
39
|
+
- Unit Price — `numericunit` — for pricing catalogs
|
|
40
|
+
- Parent Reference — `activitylink` — optional single link up a taxonomy/hierarchy (one field = one link)
|
|
41
|
+
- Child Count / Rollup — `function` — optional `<` backlink count/sum of children that link in; synchronous, returns array to reduce
|
|
42
|
+
|
|
43
|
+
**Phase skeleton:** All — or Active / Archive
|
|
44
|
+
|
|
45
|
+
**Linking mechanic:** Other workflows link TO a register via their own single-value ActivityLink field. The register itself usually links out 0–1 times (e.g. a parent in a self-referential taxonomy chain). Optional function fields on the register use a `<` backlink variable (`data: [childWorkflowId, fieldId]`) to count or summarize children that link in — returns a same-length array per declared variable, reduced synchronously in the body.
|
|
46
|
+
|
|
47
|
+
**Owning skill:** `sdk-ws-config-skill`
|
|
48
|
+
|
|
49
|
+
**Real exemplars:**
|
|
50
|
+
- onelamor / Items (master SKU registry) — `69f9923c72a68c80863bb3c9`
|
|
51
|
+
- #hailer / Products, Services and Licenses — `67dc1b7d3d2c9f6cf9a54657`
|
|
52
|
+
- project-logistics / product_master_data — `69905ff98415a9621a718982`
|
|
53
|
+
- 3pl-pro / VH - Nimikkeistö — `63df86a3eddeece67291f28c`
|
|
54
|
+
|
|
55
|
+
**Frequency:** Most common archetype fleet-wide. Present in every batch; dominant shape in data-heavy workspaces. Roughly a third to two-thirds of any workspace.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### Pipeline
|
|
60
|
+
|
|
61
|
+
**When to use:** A single workflow whose primary value is tracking one record through named lifecycle stages. Use for sales funnels, task/Kanban boards, production/order execution, ticket queues, project lifecycles, incident/complaint trackers. The phase IS the state; there is no mandatory parent header and no aggregating children (links out, if any, are context references to registers).
|
|
62
|
+
|
|
63
|
+
**Field skeleton:**
|
|
64
|
+
- Title / Subject — `string`
|
|
65
|
+
- Customer / Subject Link — `activitylink` — single context link to a register
|
|
66
|
+
- Owner / Assignee — `users` — single user field; multiple owners = multiple user fields, never an array
|
|
67
|
+
- Amount / Value — `numericunit` — optional deal/order value
|
|
68
|
+
- Due Date — `date` — Unix timestamp on write
|
|
69
|
+
- Context Value (pulled) — `function` — optional `>` forward-link pulling a value from the single linked register activity
|
|
70
|
+
|
|
71
|
+
**Phase skeleton:** New / Backlog → In Progress → Review / Waiting → Done / Won → Cancelled / Lost (terminal) → Archive (terminal)
|
|
72
|
+
|
|
73
|
+
**Linking mechanic:** Phases drive the record forward via `possibleNextPhase` transitions (linear or branching, with terminal endpoint phases for cancel/archive). Cross-workflow data is read-only context: a single-value ActivityLink to a register plus optional `>` forward-link function variables (`data: [linkFieldId, targetFieldId]`) that resolve one linked activity's value. No `<` aggregation — that would make it a header.
|
|
74
|
+
|
|
75
|
+
**Owning skill:** `hailer-design-patterns`
|
|
76
|
+
|
|
77
|
+
**Real exemplars:**
|
|
78
|
+
- onelamor / 001. Sales Funnel — `558ef59c7b417df45de6325c`
|
|
79
|
+
- #hailer / Development Tasks — `5c1c9772b0631420d10e0b6a`
|
|
80
|
+
- project-logistics / transport_orders — `69906454686166113617b382`
|
|
81
|
+
|
|
82
|
+
**Frequency:** In every batch; second/third most common. The default shape for any process that moves.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### Header + Line-Items
|
|
87
|
+
|
|
88
|
+
**When to use:** A document-level header (order, invoice, BOM, quote) that owns N child row activities and needs totals/counts rolled up from them. Use whenever you have an order with order lines, an invoice with invoice rows, a BOM with components — anything where the parent must sum or count what the children hold.
|
|
89
|
+
|
|
90
|
+
**Field skeleton:**
|
|
91
|
+
- Header: Customer / Supplier — `activitylink` — single link to a CRM/supplier register
|
|
92
|
+
- Header: Total (excl. VAT) — `function` — `<` backlink SUM over child row amounts
|
|
93
|
+
- Header: Row Count — `function` — `<` backlink COUNT of children
|
|
94
|
+
- Row: Parent Header — `activitylink` — the child's single link back to the header (this is the join)
|
|
95
|
+
- Row: Product / Item — `activitylink` — single link to a product register
|
|
96
|
+
- Row: Quantity — `numericunit`
|
|
97
|
+
- Row: Line Total — `function` — `>` forward-link unit price × qty, computed on the row
|
|
98
|
+
|
|
99
|
+
**Phase skeleton:**
|
|
100
|
+
- Header: Draft → Confirmed → Sent / In Progress → Completed / Invoiced → Cancelled (terminal)
|
|
101
|
+
- Row: All (single flat phase)
|
|
102
|
+
|
|
103
|
+
**Linking mechanic:** Two workflows. The child row carries ONE single-value ActivityLink field pointing back to the header (`data: [headerWorkflowId]`). The header aggregates via `<` backlink function variables (`data: [rowWorkflowId, rowFieldId]`) — each variable returns a parallel, same-length, same-order array of that field across all linked children; the body sums/counts/zips them synchronously. Multiple parallel `<` variables from the same child workflow are index-aligned (pivot pattern). Never model rows as an array on one field — use a separate row workflow.
|
|
104
|
+
|
|
105
|
+
**Owning skill:** `sdk-function-fields` (for the `<` backlink pivot aggregation pattern — declare `functionVariables` with `type: '<'`, `data: [rowWorkflowId, rowFieldId]`, reduce the returned array synchronously in the body)
|
|
106
|
+
|
|
107
|
+
**Real exemplars:**
|
|
108
|
+
- #hailer / Invoicing + Invoice Rows — `67dc1b7d3d2c9f6cf9a5468b / 67dc1b7d3d2c9f6cf9a5468d`
|
|
109
|
+
- onelamor / Purchase orders + Purchase order rows — `68a857b5aa341124b5670239`
|
|
110
|
+
- claude-config-testspace / Orders + OrderLines — `69e5dea5f8cd96dba3411522 / 69e5deacf8cd96dba34115ad`
|
|
111
|
+
|
|
112
|
+
**Frequency:** In every batch; co-leads with Pipeline as second most common. The backbone of every ERP/finance/order workspace.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### Resource-Reservation / Booking-Line
|
|
117
|
+
|
|
118
|
+
**When to use:** A booking line that sits between an event/job header and a bookable resource catalog — venue room, staff member, equipment, catering product, time slot. Use in event-management, venue-rental, scheduling, and field-service workspaces where each reservation must reference both WHAT is booked (resource) and WHAT it belongs to (event), and the parent rolls reservations up.
|
|
119
|
+
|
|
120
|
+
**Field skeleton:**
|
|
121
|
+
- Event / Header Link — `activitylink` — single link to the event/job hub
|
|
122
|
+
- Reserved Resource — `activitylink` — single link to the resource register (room/person/equipment/product)
|
|
123
|
+
- Time / Period — `timerange` — or datetimerange object; the slot being held
|
|
124
|
+
- Line Total — `function` — `>` forward-link pulls unit price from the resource, computed on the line
|
|
125
|
+
- Resource Calendar JSON — `function` — optional `<` backlink on the resource register itself, aggregating its own bookings for conflict detection
|
|
126
|
+
|
|
127
|
+
**Phase skeleton:** Reserved / Preliminary → Confirmed → Cancelled (terminal) → Archive (terminal)
|
|
128
|
+
|
|
129
|
+
**Linking mechanic:** Two single-value ActivityLink fields on the booking line: one back to the event header, one out to the resource register. The header aggregates the booking lines via `<` backlinks (same as Header + Line-Items). Separately, the resource register often carries its own `<` backlink function that serializes all its reservations into a calendar/availability JSON for overlap checking. A line that needs two possible parents uses an ActivityLink field whose `data[]` allows two target workflows — still one value at a time, never an array.
|
|
130
|
+
|
|
131
|
+
**Owning skill:** `sdk-function-fields` (for linked-data cross-workflow reads — both `<` backlink on the resource and `>` forward-link on the booking line)
|
|
132
|
+
|
|
133
|
+
**Real exemplars:**
|
|
134
|
+
- skm / Henkilöresurssivaraukset — `67bc2c9535d8f29758a2fbdd`
|
|
135
|
+
- sunborn-linna / Astiat myyntitilausrivit — `62678581897c430b07400954`
|
|
136
|
+
- veikkaus-arena / Tilavaraukset — `682ac817fba468d857d4991a`
|
|
137
|
+
|
|
138
|
+
**Frequency:** Pervasive across every venue/event workspace. Structurally a Header + Line-Items child with a second resource link.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### Event-Aggregate Hub
|
|
143
|
+
|
|
144
|
+
**When to use:** The central coordinator record of a workspace — a master event/production/project that fans OUT to several registers (customer, venue, personnel, promoter) AND aggregates IN from 3–8 distinct child booking/line workflows. Use it as the single hub everything else hangs off in event-production, venue, and large project workspaces.
|
|
145
|
+
|
|
146
|
+
**Field skeleton:**
|
|
147
|
+
- Event Name — `string`
|
|
148
|
+
- Customer — `activitylink` — one of several single context links out
|
|
149
|
+
- Contact Person — `activitylink`
|
|
150
|
+
- Venue / Site — `activitylink`
|
|
151
|
+
- Period — `datetimerange`
|
|
152
|
+
- Reservations Summary (per child type) — `function` — one `<` backlink function PER child workflow (e.g. space, staff, catering, equipment); never one mega-field
|
|
153
|
+
- Grand Total — `function` — `<` sum across child line totals
|
|
154
|
+
|
|
155
|
+
**Phase skeleton:** Preliminary / Tentative → Confirmed → Published / In Production → Invoiced → Held / Done → Cancelled (terminal) → Archive (terminal)
|
|
156
|
+
|
|
157
|
+
**Linking mechanic:** Multiple single-value ActivityLink fields out to registers (one field per dimension — three teams = three team fields, never an array). Multiple `<` backlink function variables IN, each scoped to a DIFFERENT child workflow (`data: [childWorkflowId, fieldId]`); each produces its own array, summarized into its own summary field. Phase-cascade function fields can emit JSON to move children when the hub is cancelled/archived. All synchronous; no field holds more than one value.
|
|
158
|
+
|
|
159
|
+
**Owning skill:** `sdk-function-fields` (for multi-child `<` backlink aggregation and phase-cascade JSON payload pattern)
|
|
160
|
+
|
|
161
|
+
**Real exemplars:**
|
|
162
|
+
- TKT / Tapahtumavaraukset (event-hub) — `685e3794d97fdb32fb2e1715`
|
|
163
|
+
- nokia-arena / Tuotantovaraukset — `6033b5fc0f6e7900071daa43`
|
|
164
|
+
- #hailer / Change & Configuration Management (cross-linked tracker) — `691356ecdbaed8d0028763ce`
|
|
165
|
+
|
|
166
|
+
**Frequency:** Recurs as the spine of event/production workspaces. Usually 1–3 per workspace but structurally load-bearing — everything else links to it.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### Request ↔ Fulfilment
|
|
171
|
+
|
|
172
|
+
**When to use:** Two linked workflows: an intake/request side (often a public form or external entry) and a separate fulfilment/execution side that carries the work to completion. Use when the request and the doing-of-it have genuinely different lifecycles and owners (recruitment application vs hiring, service request vs work order, RFQ vs delivery, tender vs contract).
|
|
173
|
+
|
|
174
|
+
**Field skeleton:**
|
|
175
|
+
- Request: Subject — `string`
|
|
176
|
+
- Request: Requester — `activitylink` — single link to customer/person register
|
|
177
|
+
- Request: Fulfilment Link — `activitylink` — single link to the fulfilment record once spawned
|
|
178
|
+
- Fulfilment: Source Request — `activitylink` — single link back to the originating request
|
|
179
|
+
- Fulfilment: Assignee — `users`
|
|
180
|
+
- Cascade Payload — `function` — optional JSON output consumed by an automation to move/update the partner record
|
|
181
|
+
|
|
182
|
+
**Phase skeleton:**
|
|
183
|
+
- Request: New → In Review → Approved (spawns) / Rejected
|
|
184
|
+
- Fulfilment: Open → In Progress → Done / Archive
|
|
185
|
+
|
|
186
|
+
**Linking mechanic:** Each side carries ONE single-value ActivityLink to the other. Phase changes propagate via a phase-cascade function field whose JSON output is read by an external automation (monolith webhook, cron, microservice) that calls `v3.activity.updateMany` on the linked record — function fields cannot move activities themselves (no async/network in isolated-vm). Read-only context flows via `>` forward-link; the actual cascade is automation-driven.
|
|
187
|
+
|
|
188
|
+
**Owning skill:** `sdk-function-fields` (for the phase-cascade JSON payload pattern — emit a structured payload from the function body; automate the cross-workflow update externally)
|
|
189
|
+
|
|
190
|
+
**Real exemplars:**
|
|
191
|
+
- gofitness / Huoltopyyntölomake → Huolto — `69dcc1d8d36be8b37cc9e771 / 69a83c19bf7a99ffd0bd8e2c`
|
|
192
|
+
- pohjaset / Pohjaset - Työhakemuslomake → Avoimet työpaikat / Työsopimukset — `67a9b8fe1feff92854dfac87 / 67a9b8fe1feff92854dfac89`
|
|
193
|
+
- skm / Työtarjoukset → Ryhmävaraukset — `68109d4e05b8d02c7ebd0288 / 68109d4e05b8d02c7ebd0277`
|
|
194
|
+
|
|
195
|
+
**Frequency:** Moderately common. Often blends with Pipeline at the edges — call it Request ↔ Fulfilment only when there are genuinely two linked workflows with a cascade.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### Approval Flow
|
|
200
|
+
|
|
201
|
+
**When to use:** A single workflow whose defining feature is a binary decision gate — Approved vs Rejected as terminal endpoints — usually with an intake phase before and optional fulfilment tail after. Use for leave/vacation requests, expense/reimbursement claims, purchase requisitions, benefit payouts, simple sign-offs. Reach for this over a generic Pipeline when the outcome is a yes/no decision, not a multi-stage progression.
|
|
202
|
+
|
|
203
|
+
**Field skeleton:**
|
|
204
|
+
- Request Title — `string`
|
|
205
|
+
- Requester / Employee — `activitylink` — single link to employee/customer register
|
|
206
|
+
- Amount / Days Requested — `numericunit`
|
|
207
|
+
- Approver — `users` — single approver field; second approver = a second field, not an array
|
|
208
|
+
- Decision Note — `textarea`
|
|
209
|
+
- Computed Eligibility — `function` — optional `>` forward-link pulling balance/entitlement from the employee register
|
|
210
|
+
|
|
211
|
+
**Phase skeleton:** Request / Pending → Approved (terminal) / Rejected / Denied (terminal) → (optional) Fulfilled → Archive
|
|
212
|
+
|
|
213
|
+
**Linking mechanic:** Phases encode the decision: an intake phase with two `possibleNextPhase` targets (approved, rejected), both terminal. Context fields are single-value ActivityLinks plus optional `>` forward-link functions to read the requester's entitlement. No `<` aggregation and no child workflow — that's what separates it from Header + Line-Items and Request ↔ Fulfilment.
|
|
214
|
+
|
|
215
|
+
**Owning skill:** `hailer-design-patterns`
|
|
216
|
+
|
|
217
|
+
**Real exemplars:**
|
|
218
|
+
- onelamor / Vacation Requests — `68909d924bd6f64c1e922816`
|
|
219
|
+
- #hailer / Distributed Well-Being Pool — `69e9e8919bfb1d30714f0dfd`
|
|
220
|
+
- nokia-arena / Hankinta (purchase request) — `60349f050f6e790007219617`
|
|
221
|
+
|
|
222
|
+
**Frequency:** Real and recurring but narrow — frequently collapses into Pipeline. Distinct only when the binary approve/reject gate is the whole point.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### Many-to-Many Junction
|
|
227
|
+
|
|
228
|
+
**When to use:** A pure join table connecting two (or three) registers in a many-to-many relationship — partner↔skill, supplier↔product, project↔supplier, event↔attendee, customer↔license. Use whenever an entity can relate to many of another and vice-versa, since Hailer has no multi-value link. Each junction record represents ONE pairing, with optional payload (level, price override, role) on the join. This is the canonical Hailer answer to "I need multiple X linked to Y".
|
|
229
|
+
|
|
230
|
+
**Field skeleton:**
|
|
231
|
+
- Parent A — `activitylink` — single link to register A
|
|
232
|
+
- Parent B — `activitylink` — single link to register B
|
|
233
|
+
- Junction Payload — `numericunit` — optional override/qualifier on the pairing (price, level, role)
|
|
234
|
+
- Note — `string` — optional
|
|
235
|
+
|
|
236
|
+
**Phase skeleton:** All (single flat phase)
|
|
237
|
+
|
|
238
|
+
**Linking mechanic:** Exactly two (or three) single-value ActivityLink fields, each to a different register, on a flat one-phase workflow. The many-to-many is realized by having MANY junction records — never by an array field. Either parent register can surface its related set via a `<` backlink function reading the junction.
|
|
239
|
+
|
|
240
|
+
**Owning skill:** `sdk-ws-config-skill`
|
|
241
|
+
|
|
242
|
+
**Real exemplars:**
|
|
243
|
+
- #hailer / Partner Skills Rows — `69afd2b2712bcca4541105a2`
|
|
244
|
+
- project-logistics / supplier_products — `6998c43064b3290e69db4213`
|
|
245
|
+
- skm / Oppaiden osaamisten linkitys — `68109d4e05b8d02c7ebd026b`
|
|
246
|
+
|
|
247
|
+
**Frequency:** Recurs wherever entities cross-relate. Small count per workspace but a distinct, reusable structural answer.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
### Recurring Template
|
|
252
|
+
|
|
253
|
+
**When to use:** A template/master record that gets cloned or instantiated into time-bound or per-occurrence instances — shift templates spawning shifts, event templates spawning event bookings, recurring-task lists spawning task instances, induction checklists spawning per-employee tasks. Use when the customer reuses a defined structure on a schedule or per-trigger.
|
|
254
|
+
|
|
255
|
+
**Field skeleton:**
|
|
256
|
+
- Template Name — `string`
|
|
257
|
+
- Resource / Scope Link — `activitylink` — single link to the register the template applies to (store, role, venue)
|
|
258
|
+
- Default Period / Offset — `timerange` — start/end offsets the instance inherits
|
|
259
|
+
- Instantiation Target — `activitylink` — on the request-side workflow: single link selecting which template to spawn from
|
|
260
|
+
- Spawned Instances — `function` — optional `<` backlink count of instances created from this template
|
|
261
|
+
|
|
262
|
+
**Phase skeleton:**
|
|
263
|
+
- Template: Active / Archived
|
|
264
|
+
- Instance: Planned → Confirmed → Done / Cancelled
|
|
265
|
+
|
|
266
|
+
**Linking mechanic:** A template workflow holds the definition; an instantiation-request (or a scheduled automation) reads it via a single-value ActivityLink and creates instance activities. **The actual spawning is automation/cron-driven — function fields cannot create activities** (no async/network in isolated-vm). Template↔instance visibility is via single ActivityLink fields plus optional `<` backlink for counting instances. Recurrence cadence lives in the automation, not in the field.
|
|
267
|
+
|
|
268
|
+
**Owning skill:** `hailer-design-patterns`
|
|
269
|
+
|
|
270
|
+
**Real exemplars:**
|
|
271
|
+
- logomo / Template + Templaterivit + Tapahtuman luonti templatesta — `699ec588098b9886211555e3 / 699ec589098b9886211555e6 / 699ec519098b988621154cd9`
|
|
272
|
+
- puutarhanikkarit-v2 / Vuoropohja → Työvuoro — `6a034cfc35ef550a424960f4 / 69e1cbba4402eb9de283fab9`
|
|
273
|
+
- pohjaset / Toistuvat tehtävälistat → Toistuvat tehtävät — `67c6f6e6095c5f35125922ef / 67c6f73b095c5f35125925de`
|
|
274
|
+
|
|
275
|
+
**Frequency:** Rarest of the validated set. Appears in ~6 batches at low counts. Real and worth cataloguing, but the spawn mechanic always depends on an external automation — note that prominently when designing.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Hard guards
|
|
280
|
+
|
|
281
|
+
These are load-bearing; they override any spec or shortcut that contradicts them.
|
|
282
|
+
|
|
283
|
+
**No multi-value field types exist in Hailer.** Every field — ActivityLink, users, teams, textpredefinedoptions — holds exactly ONE value. "Multiple X linked to Y" is never solved by an array on one field. The two canonical answers: (a) use multiple separate ActivityLink fields (one per link), or (b) use a Many-to-Many Junction workflow (one record per pairing). If you find yourself writing or thinking `["id1", "id2"]` for any field value, stop — that field type does not exist.
|
|
284
|
+
|
|
285
|
+
**Function fields are synchronous isolated-vm JavaScript.** No async, no fetch, no import, no TypeScript syntax, no `linkedFrom()`. Cross-workflow reads work ONLY via declared `functionVariables`:
|
|
286
|
+
- `<` backlink (`data: [sourceWorkflowId, fieldId]`) — returns a parallel, same-length array of children's field values; reduce/sum/filter it synchronously in the body
|
|
287
|
+
- `>` forward-link (`data: [linkFieldId, targetFieldId]`) — returns that single linked activity's value
|
|
288
|
+
|
|
289
|
+
Ad-hoc reads of un-wired activities do not work. Async network access does not work.
|
|
290
|
+
|
|
291
|
+
**Function fields cannot create or move activities.** Request ↔ Fulfilment cascades, Recurring Template spawns, and Event-Aggregate Hub phase cascades all work by having the function field emit a JSON payload that an **external automation** (monolith webhook, cron, microservice) reads and then calls `v3.activity.updateMany` or `v3.activity.create`. The function field is read-only compute inside isolated-vm; it has no network access to act on its own output.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Cross-links
|
|
296
|
+
|
|
297
|
+
This skill is the WHICH-SHAPE. The HOW lives in:
|
|
298
|
+
|
|
299
|
+
- `hailer-design-patterns` — principles for when to split workflows vs keep flat; when to reach for which archetype at a higher level; approval gate and pipeline patterns
|
|
300
|
+
- `sdk-ws-config-skill` — field/phase TypeScript config, push/pull commands, numericunit/textpredefinedoptions formats
|
|
301
|
+
- `sdk-function-fields` — function field body syntax, isolated-vm constraints, `<` backlink aggregation, `>` forward-link, phase-cascade JSON payload, null-return debugging
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: insight-join-patterns
|
|
3
|
+
description: Correct JOIN syntax for Hailer insights with ActivityLink, user, and dropdown fields. Load when writing a cross-workflow insight JOIN, resolving NULL join results, joining user fields to names, or handling polymorphic ActivityLink fields (one field pointing to multiple target workflows). Includes the field-type JOIN matrix, row-multiplication trap for one-to-many backlink JOINs, and how to surface orphan rows when a link target has been deleted.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Insight JOIN Patterns
|
|
7
|
+
|
|
8
|
+
**Prerequisite:** Review `sdk-insight-queries` for basic insight syntax and single-workflow queries.
|
|
9
|
+
|
|
10
|
+
## Core Rules
|
|
11
|
+
|
|
12
|
+
1. Both workflows need `{ name: 'id', meta: '_id' }` in their fields array
|
|
13
|
+
2. JOIN condition: `source1.activityLinkFieldName = source2.id`
|
|
14
|
+
3. Use LEFT JOIN for optional relationships (activitylink can be null)
|
|
15
|
+
4. Use INNER JOIN only when relationship must exist
|
|
16
|
+
|
|
17
|
+
**Status: EMPIRICALLY CONFIRMED** — S235 (2026-05-28), 4 calls, 55 populated rows matched. ON-clause syntax verified against a live cross-workflow join (Deals → Accounts).
|
|
18
|
+
|
|
19
|
+
## Columns Are Declared, Not Built-In
|
|
20
|
+
|
|
21
|
+
Every SQL column comes from a source's `fields[]` entry — either `{ name: '<your column>', meta: '<key>' }` or `{ name: '<your column>', fieldId: '<field id>' }`. Nothing is auto-injected: if you `SELECT` a column, a source must declare it first, or you get "no such column". The `name` is the SQL column name you choose; `meta`/`fieldId` is where its value comes from.
|
|
22
|
+
|
|
23
|
+
Valid `meta` keys (activity metadata): `_id`, `name`, `created`, `updated`, `phaseId`, `phaseName`, `phaseLastMove`, `workflowId`, `workflowName`, `team`, `createdBy` (alias `uid`), `priority`. There is **no `activityName` / `activityId` meta** — the activity name is `meta: 'name'`, the id is `meta: '_id'`. Custom fields use `fieldId`, never `meta`.
|
|
24
|
+
|
|
25
|
+
## Correct Pattern
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
{
|
|
29
|
+
sources: [
|
|
30
|
+
{
|
|
31
|
+
name: 'p',
|
|
32
|
+
workflowId: '68446dc05b30685f67c6fcd4',
|
|
33
|
+
fields: [
|
|
34
|
+
{ name: 'player_name', meta: 'name' },
|
|
35
|
+
{ name: 'id', meta: '_id' }, // Required!
|
|
36
|
+
{ name: 'club', fieldId: '684d5e45...' } // ActivityLink field — use fieldId, NOT key
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'c',
|
|
41
|
+
workflowId: '691ea936ccb6bdeebc0cbf77',
|
|
42
|
+
fields: [
|
|
43
|
+
{ name: 'club_name', meta: 'name' },
|
|
44
|
+
{ name: 'id', meta: '_id' } // Required!
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
query: 'SELECT p.player_name, c.club_name FROM p LEFT JOIN c ON p.club = c.id'
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Wrong Pattern
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
// ❌ WRONG - Missing _id in clubs source → "no such column: c.id"
|
|
56
|
+
{
|
|
57
|
+
sources: [
|
|
58
|
+
{ name: 'c', workflowId: 'clubs-id', fields: [{ name: 'club_name', meta: 'name' }] }
|
|
59
|
+
// Missing: { name: 'id', meta: '_id' }
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Three-Way JOIN
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
{
|
|
70
|
+
sources: [
|
|
71
|
+
{
|
|
72
|
+
name: 't',
|
|
73
|
+
workflowId: 'tasks-workflow-id',
|
|
74
|
+
fields: [
|
|
75
|
+
{ name: 'task_name', meta: 'name' },
|
|
76
|
+
{ name: 'id', meta: '_id' },
|
|
77
|
+
{ name: 'topic', fieldId: 'topic-field-id' }
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'top',
|
|
82
|
+
workflowId: 'topics-workflow-id',
|
|
83
|
+
fields: [
|
|
84
|
+
{ name: 'topic_name', meta: 'name' },
|
|
85
|
+
{ name: 'id', meta: '_id' },
|
|
86
|
+
{ name: 'project', fieldId: 'project-field-id' }
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'p',
|
|
91
|
+
workflowId: 'projects-workflow-id',
|
|
92
|
+
fields: [{ name: 'project_name', meta: 'name' }, { name: 'id', meta: '_id' }]
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
query: `
|
|
96
|
+
SELECT t.task_name, top.topic_name, p.project_name
|
|
97
|
+
FROM t
|
|
98
|
+
LEFT JOIN top ON t.topic = top.id
|
|
99
|
+
LEFT JOIN p ON top.project = p.id
|
|
100
|
+
`
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Troubleshooting
|
|
107
|
+
|
|
108
|
+
**Error: "no such column: c.id"** — missing `{ name: 'id', meta: '_id' }` in target workflow source.
|
|
109
|
+
|
|
110
|
+
**Error: "no such column: p.club"** — ActivityLink field not included in source fields array; check you used the correct `fieldId` from `describe_workflows`.
|
|
111
|
+
|
|
112
|
+
**NULL results for joined data:**
|
|
113
|
+
- ActivityLink field is empty/null for some activities (expected with LEFT JOIN)
|
|
114
|
+
- **The link field must have actual data populated.** A JOIN against a workflow where the ActivityLink field is unpopulated returns NULL on all joined columns regardless of ON-clause correctness. This is the most common false-negative — verify with `mcp__hailer__list_activities` that at least some activities have the link field set before concluding the JOIN syntax is wrong.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Polymorphic Parent JOINs
|
|
119
|
+
|
|
120
|
+
When an ActivityLink field can point to multiple workflows, LEFT JOIN both and COALESCE:
|
|
121
|
+
|
|
122
|
+
```sql
|
|
123
|
+
SELECT
|
|
124
|
+
b.nimi AS varaus,
|
|
125
|
+
COALESCE(e.nimi, v.nimi, '(ei linkitetty)') AS ylätaso,
|
|
126
|
+
COALESCE(e.pvm, v.pvm) AS pvm
|
|
127
|
+
FROM b
|
|
128
|
+
LEFT JOIN e ON b.tapahtuma = e.id
|
|
129
|
+
LEFT JOIN v ON b.verkkovaraus = v.id
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Each ActivityLink field gets its own source + JOIN. `COALESCE` picks the first non-null result — works because only one link is populated per activity.
|
|
133
|
+
|
|
134
|
+
### Orphan Link Rows — Target Workflow Deleted or Unlisted
|
|
135
|
+
|
|
136
|
+
If a target workflow is deleted or unlisted after activities were already linked to it, those link rows still exist in the source workflow but JOIN to that target returns nothing.
|
|
137
|
+
|
|
138
|
+
**Diagnostic:**
|
|
139
|
+
|
|
140
|
+
```sql
|
|
141
|
+
SELECT
|
|
142
|
+
src.activityId,
|
|
143
|
+
src.link_field,
|
|
144
|
+
CASE
|
|
145
|
+
WHEN target1.id IS NOT NULL THEN 'target1'
|
|
146
|
+
WHEN target2.id IS NOT NULL THEN 'target2'
|
|
147
|
+
ELSE 'ORPHAN — target deleted/unlisted'
|
|
148
|
+
END AS link_status
|
|
149
|
+
FROM src
|
|
150
|
+
LEFT JOIN target1 ON src.link_field = target1.id
|
|
151
|
+
LEFT JOIN target2 ON src.link_field = target2.id
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### New Link Target Added — Audit All Name-Resolving Insights
|
|
155
|
+
|
|
156
|
+
When a workflow is added as a new valid link target on an ActivityLink field, every insight that resolves that field's name via LEFT JOIN needs a parallel source + JOIN + COALESCE branch — otherwise rows linking to the new target return NULL silently.
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
// Before: 3-target COALESCE
|
|
160
|
+
COALESCE(a.name, b.name, c.name, '(ei linkitetty)') AS tuote
|
|
161
|
+
|
|
162
|
+
// After: add proj source, JOIN, and COALESCE slot
|
|
163
|
+
COALESCE(a.name, b.name, c.name, proj.name, '(ei linkitetty)') AS tuote
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Human-Readable Output
|
|
169
|
+
|
|
170
|
+
When an insight will be read by humans, JOIN every ActivityLink to its target and SELECT the name — not the link field itself.
|
|
171
|
+
|
|
172
|
+
```sql
|
|
173
|
+
-- ❌ Raw IDs — useless for humans
|
|
174
|
+
SELECT t.task_name, t.project FROM t
|
|
175
|
+
|
|
176
|
+
-- ✅ Human-readable
|
|
177
|
+
SELECT t.task_name, p.project_name
|
|
178
|
+
FROM t LEFT JOIN p ON t.project = p.id
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Same for user fields:
|
|
182
|
+
|
|
183
|
+
```sql
|
|
184
|
+
SELECT t.task_name, u.name AS assignee
|
|
185
|
+
FROM t LEFT JOIN user u ON u._id = t.assignee
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## ActivityLink Display Value — activityName vs a Specific Field
|
|
191
|
+
|
|
192
|
+
**Option A — Show the linked activity's display name:** use `meta: 'name'` in the linked source's `fields[]`.
|
|
193
|
+
|
|
194
|
+
**Option B — Show a specific field's value from the linked workflow:** add the field to the linked source's `fields[]` using `fieldId`.
|
|
195
|
+
|
|
196
|
+
Ask which one is wanted before writing the JOIN — wrong choice means rewriting the source definitions.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Date Bucketing — Prefer Underlying datetimerange Over Function Fields
|
|
201
|
+
|
|
202
|
+
Function-field-derived date values can be null across large portions of a workflow — the computation may not have run. In SQL `null` in date math evaluates to `0`, which formats as `1970-01`. A few activities with valid values look fine in `run_insight` ad-hoc (up to 200 rows); the full pushed insight may be nearly all epoch values.
|
|
203
|
+
|
|
204
|
+
**Prefer the underlying `datetimerange` field over a function-field derivative when bucketing dates.** The datetimerange field is always populated if the user entered a value.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Field-Type JOIN Matrix
|
|
209
|
+
|
|
210
|
+
| Source field type | JOIN needed? | Syntax / alternative |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| text, textarea | No | raw string |
|
|
213
|
+
| number, numericunit | No | raw number |
|
|
214
|
+
| date, datetime | No (format inline) | `strftime('%Y-%m-%d', field, 'unixepoch')` — custom fields are seconds. Meta `created`/`updated` are ms: `strftime('%Y-%m-%d', field/1000, 'unixepoch')`. |
|
|
215
|
+
| dropdown | No — inline CASE | `CASE field WHEN 'key1' THEN 'Label 1' ... ELSE field END AS label` |
|
|
216
|
+
| user (single) | Yes | `LEFT JOIN user u ON u._id = src.ownerField` → `u.name` |
|
|
217
|
+
| activitylink (single target wf) | Yes — one JOIN | `LEFT JOIN targetwf t ON t.id = src.linkField` → `t.name` |
|
|
218
|
+
| activitylink (multi target wf — polymorphic) | Yes — one JOIN per target, `COALESCE` | See polymorphic section above |
|
|
219
|
+
| boolean | No (format inline) | `CASE WHEN field = true THEN 'Yes' ELSE 'No' END` |
|
|
220
|
+
|
|
221
|
+
### User Field
|
|
222
|
+
|
|
223
|
+
The `user` table is a pseudo-table — you don't declare it in `sources[]`, it's always joinable. Column names: `_id`, `name`. (`email`, `firstname`, `lastname` do not exist — error "no such column".)
|
|
224
|
+
|
|
225
|
+
```sql
|
|
226
|
+
SELECT d.name AS deal, COALESCE(u.name, 'Unassigned') AS owner
|
|
227
|
+
FROM deals d
|
|
228
|
+
LEFT JOIN user u ON u._id = d.owner
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
No multi-user field types in Hailer — every user field stores a scalar user ID.
|
|
232
|
+
|
|
233
|
+
### Dropdown Label Display
|
|
234
|
+
|
|
235
|
+
Dropdowns store the option key (e.g. `"saas"`), not the visible label (`"SaaS"`). Decode inline:
|
|
236
|
+
|
|
237
|
+
```sql
|
|
238
|
+
SELECT
|
|
239
|
+
a.name AS account,
|
|
240
|
+
CASE a.industry
|
|
241
|
+
WHEN 'saas' THEN 'SaaS'
|
|
242
|
+
WHEN 'retail' THEN 'Retail'
|
|
243
|
+
WHEN 'manufacturing' THEN 'Manufacturing'
|
|
244
|
+
ELSE 'Other'
|
|
245
|
+
END AS industry_label
|
|
246
|
+
FROM accounts a
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Default: inline CASE for 3–10 options, lookup table for more.
|
|
250
|
+
|
|
251
|
+
### Phase Name Display
|
|
252
|
+
|
|
253
|
+
**Do not use CASE on `phaseId` to map phase IDs to phase names.** The built-in `phaseName` column already contains the current phase's display name — just SELECT it directly.
|
|
254
|
+
|
|
255
|
+
```sql
|
|
256
|
+
-- Use this
|
|
257
|
+
SELECT activityName, phaseName FROM src
|
|
258
|
+
|
|
259
|
+
-- Do not do this — unnecessary and brittle
|
|
260
|
+
SELECT activityName, CASE phaseId WHEN '64a1b2c3...' THEN 'New' ... END AS phase FROM src
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## One-to-Many Backlink JOINs — Row Multiplication Trap
|
|
266
|
+
|
|
267
|
+
There are no multi-value field types in Hailer — every field holds a scalar. Row multiplication in insights comes from **one-to-many backlink JOINs**, where you join from the "one" side (a parent workflow) to the "many" side (child activities that link back to it).
|
|
268
|
+
|
|
269
|
+
**Classic case:** `Accounts LEFT JOIN Deals ON deals.account_link = accounts.id` — one account, many deals → one row per deal, not per account. `SUM(account.revenue)` then doubles or triples.
|
|
270
|
+
|
|
271
|
+
**Detection test:** compare `COUNT(*)` in the insight against the known activity count in the parent workflow. If `COUNT(*) > parent activity count`, you have row multiplication.
|
|
272
|
+
|
|
273
|
+
### Fix Patterns
|
|
274
|
+
|
|
275
|
+
**1. `COUNT(DISTINCT a._id)` — count unique parent activities, not rows**
|
|
276
|
+
|
|
277
|
+
**2. Subquery pre-aggregation (safest):**
|
|
278
|
+
|
|
279
|
+
```sql
|
|
280
|
+
SELECT
|
|
281
|
+
a.account_name,
|
|
282
|
+
COALESCE(ds.deal_count, 0) AS deal_count,
|
|
283
|
+
COALESCE(ds.pipeline, 0) AS pipeline
|
|
284
|
+
FROM accounts a
|
|
285
|
+
LEFT JOIN (
|
|
286
|
+
SELECT d.account_link, COUNT(*) AS deal_count, SUM(d.amount) AS pipeline
|
|
287
|
+
FROM deals d
|
|
288
|
+
GROUP BY d.account_link
|
|
289
|
+
) ds ON ds.account_link = a.id
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**3. `GROUP_CONCAT(DISTINCT ...)` — roll child names into a single display string:**
|
|
293
|
+
|
|
294
|
+
```sql
|
|
295
|
+
SELECT
|
|
296
|
+
a.account_name,
|
|
297
|
+
COUNT(DISTINCT d.id) AS deal_count,
|
|
298
|
+
GROUP_CONCAT(DISTINCT d.deal_name) AS deal_names
|
|
299
|
+
FROM accounts a
|
|
300
|
+
LEFT JOIN deals d ON d.account_link = a.id
|
|
301
|
+
GROUP BY a.id, a.account_name
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Checklist Before Creating an Insight with JOINs
|
|
307
|
+
|
|
308
|
+
- [ ] Both workflow sources include `{ name: 'id', meta: '_id' }`
|
|
309
|
+
- [ ] ActivityLink field uses `fieldId` (NOT `key`)
|
|
310
|
+
- [ ] JOIN condition uses column names from sources (e.g., `p.club = c.id`)
|
|
311
|
+
- [ ] Using LEFT JOIN (unless relationship required)
|
|
312
|
+
- [ ] If insight is for humans: ActivityLink fields resolve to names, not IDs
|
|
313
|
+
- [ ] Tested with `mcp__hailer__run_insight` first
|