@elevasis/sdk 1.36.5 → 1.38.0

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/dist/cli.cjs +64 -22
  2. package/dist/index.d.ts +105 -7
  3. package/dist/index.js +81 -33
  4. package/dist/node/index.d.ts +6 -5
  5. package/dist/test-utils/index.d.ts +6 -5
  6. package/dist/test-utils/index.js +70 -32
  7. package/dist/worker/index.js +7 -11
  8. package/package.json +4 -4
  9. package/reference/claude-config/Overview.md +140 -32
  10. package/reference/claude-config/rules/active-change-index.md +13 -2
  11. package/reference/claude-config/rules/agent-start-here.md +13 -2
  12. package/reference/claude-config/rules/deployment.md +13 -2
  13. package/reference/claude-config/rules/error-handling.md +13 -2
  14. package/reference/claude-config/rules/execution.md +13 -2
  15. package/reference/claude-config/rules/frontend.md +13 -2
  16. package/reference/claude-config/rules/observability.md +13 -2
  17. package/reference/claude-config/rules/operations.md +13 -2
  18. package/reference/claude-config/rules/organization-model.md +1 -1
  19. package/reference/claude-config/rules/organization-os.md +1 -1
  20. package/reference/claude-config/rules/package-taxonomy.md +13 -2
  21. package/reference/claude-config/rules/platform.md +13 -2
  22. package/reference/claude-config/rules/shared-types.md +13 -2
  23. package/reference/claude-config/rules/task-tracking.md +13 -2
  24. package/reference/claude-config/rules/topbar-actions.md +2 -2
  25. package/reference/claude-config/rules/ui.md +13 -2
  26. package/reference/claude-config/rules/vibe.md +13 -2
  27. package/reference/claude-config/settings.json +30 -34
  28. package/reference/claude-config/skills/deploy/SKILL.md +159 -156
  29. package/reference/claude-config/skills/elevasis/SKILL.md +11 -4
  30. package/reference/claude-config/skills/explore/SKILL.md +78 -78
  31. package/reference/claude-config/skills/git-sync/SKILL.md +166 -126
  32. package/reference/claude-config/skills/om/SKILL.md +15 -15
  33. package/reference/claude-config/skills/om/operations/build.md +2 -2
  34. package/reference/claude-config/skills/project/SKILL.md +1 -1
  35. package/reference/claude-config/skills/save/SKILL.md +183 -183
  36. package/reference/claude-config/skills/setup/SKILL.md +9 -3
  37. package/reference/claude-config/skills/status/SKILL.md +59 -59
  38. package/reference/claude-config/skills/sync/SKILL.md +47 -47
  39. package/reference/claude-config/skills/tutorial/SKILL.md +1 -1
  40. package/reference/claude-config/skills/tutorial/technical.md +11 -11
  41. package/reference/claude-config/sync-notes/2026-06-15-session-chat-zero-wiring.md +46 -0
  42. package/reference/claude-config/sync-notes/2026-06-17-agent-session-ux-features.md +34 -0
  43. package/reference/claude-config/sync-notes/2026-06-25-shared-page-scroll-contract-guard.md +52 -0
  44. package/reference/claude-config/sync-notes/2026-06-26-leadgen-overview-om-telemetry.md +47 -0
  45. package/reference/claude-config/sync-notes/2026-07-21-agent-scaffold-hardening.md +75 -0
  46. package/reference/rules/active-change-index.md +5 -5
  47. package/reference/rules/agent-start-here.md +34 -30
  48. package/reference/rules/deployment.md +21 -8
  49. package/reference/rules/frontend.md +4 -4
  50. package/reference/rules/observability.md +1 -1
  51. package/reference/rules/organization-model.md +1 -1
  52. package/reference/rules/organization-os.md +29 -29
  53. package/reference/rules/ui.md +205 -202
  54. package/reference/rules/vibe.md +5 -4
  55. package/reference/scaffold/operations/propagation-pipeline.md +1 -1
  56. package/reference/scaffold/recipes/extend-lead-gen.md +505 -332
  57. package/reference/scaffold/reference/contracts.md +14 -21
@@ -1,24 +1,24 @@
1
1
  <!-- @generated by packages/sdk/scripts/copy-reference-docs.mjs -- DO NOT EDIT -->
2
2
  <!-- Regenerate: pnpm scaffold:sync -->
3
3
 
4
- ---
5
- title: Build and Extend Lead Gen
4
+ ---
5
+ title: Build and Extend Lead Gen
6
6
  description: Map the lead-gen platform primitives available to SDK projects: shared UI pages, provider-injected Organization Model config, data hooks, list/member state, artifacts, workflow adapters, System Interfaces, and tenant-owned extension boundaries.
7
- ---
8
-
9
- # Build and Extend Lead Gen
10
-
11
- Use this recipe when a downstream project wants to build on the shared lead-gen system instead of forking it.
12
-
13
- Good trigger phrases:
14
-
15
- - "Add a lead-gen reports page."
16
- - "Build a campaign creator surface."
17
- - "Customize the lead-gen sidebar."
18
- - "Show artifacts for a campaign."
19
- - "Read or update lead-gen lists from a workflow."
20
- - "Change the lead-gen lifecycle for this business."
21
-
7
+ ---
8
+
9
+ # Build and Extend Lead Gen
10
+
11
+ Use this recipe when a downstream project wants to build on the shared lead-gen system instead of forking it.
12
+
13
+ Good trigger phrases:
14
+
15
+ - "Add a lead-gen reports page."
16
+ - "Build a campaign creator surface."
17
+ - "Customize the lead-gen sidebar."
18
+ - "Show artifacts for a campaign."
19
+ - "Read or update lead-gen lists from a workflow."
20
+ - "Change the lead-gen lifecycle for this business."
21
+
22
22
  Lead gen is a layered platform surface, not one component. Shared packages own shapes, readers, runtime mechanics, and reusable UI. Tenant projects own the values that make a list builder specific to a business.
23
23
 
24
24
  - **Organization OS:** prospecting semantics, System access, pipeline labels, quick access, stage catalogs, build templates, workflow actions, resources, and topology live in the tenant organization model.
@@ -28,137 +28,137 @@ Lead gen is a layered platform surface, not one component. Shared packages own s
28
28
  - **Core readers:** `getLeadGenStageCatalog`, `getAllBuildTemplates`, `getProspectingBuildTemplateOptions`, and `createBuildPlanSnapshotFromTemplateId` from `@elevasis/core` read caller-supplied Organization Model/template values. They do not ship tenant catalogs or default templates.
29
29
  - **Workflow adapters:** `acqDb`, `list`, and `listBuilderWorkflow` from `@elevasis/sdk/worker` let workflows read and mutate lead-gen data through platform tools.
30
30
  - **System API Interface:** `sales.lead-gen` uses the flat `system.apiInterface` marker for its API-backed lead-gen boundary. Readiness is derived from scoped resources, ontology bindings, required catalogs, and scoped topology grants. Lead-gen-to-CRM handoff uses scoped topology, not a top-level Bridge Contract or a hand-authored handoff interface field. Generated contract docs expose the current lead-gen shapes in `node_modules/@elevasis/sdk/reference/scaffold/reference/contracts.md`.
31
-
32
- ## Decision Table
33
-
34
- | User wants | Start here | Notes |
35
- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
31
+
32
+ ## Decision Table
33
+
34
+ | User wants | Start here | Notes |
35
+ | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
36
36
  | Change lead-gen System availability, labels, quick access, stage catalogs, build templates, workflow actions, resources, topology, or API readiness | `core/config/organization-model.ts` | Treat this as Organization OS work. Tenant projects own these values. Mark `sales.lead-gen.apiInterface` active only when scoped resources and ontology are ready for API-backed lead-gen actions. |
37
- | Add lead-gen sidebar nav or a lead-gen route | `@elevasis/ui/features/lead-gen` and `node_modules/@elevasis/sdk/reference/scaffold/ui/customization.md` | Prefer manifest/sidebar composition. Do not fork shared source first. |
38
- | Wrap a shared lead-gen page with project chrome | `LeadGenOverviewPage`, `LeadGenListsPage`, `LeadGenListDetailPage`, `ListBuilderPage`, `LeadGenCompaniesPage`, `LeadGenContactsPage` | Keep route files thin and put project-specific behavior in local feature modules. |
39
- | Build a custom campaign/list workspace | `ListBuilderPage`, `useLists`, `useList`, `useListProgress`, `useListExecutions`, `useWorkflowExecution`, `useExecutionSSE` from `@elevasis/ui` | Use the shared builder when possible; otherwise compose hooks for platform data and workflow execution. |
40
- | Add or change list-builder workflow buttons/forms | `ui/src/config/listActions.ts` plus `ElevasisUIProvider listActions={listActions}` / `createElevasisApp({ listActions })` | The shared UI owns the registry contract. The project owns workflow ids, action keys, Zod schemas, layouts, and default inputs. |
41
- | Render artifacts or list-member detail | `LeadGenListDetailPage`, `useArtifacts`, `useCreateArtifact`, `useListMember` | Artifacts are a substrate primitive. Keep vertical-specific rendering local until there are repeated use cases. |
42
- | Read or mutate lead-gen data inside a workflow | `acqDb` or `list` from `@elevasis/sdk/worker` | `organizationId` is injected server-side by the platform dispatcher. Do not pass it from workflow code. |
43
- | Add a new persisted lead-gen field, artifact kind, or transition API | Platform/API migration work, not just scaffold work | Update DB, core schemas/types, API service/handlers, hooks, docs, and scaffold contracts together. |
44
-
45
- ## Published Lead-Gen Surfaces
46
-
47
- | Surface | Import from | Use for |
48
- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
49
- | `leadGenManifest`, `LEAD_GEN_ITEMS`, `LeadGenSidebar`, `LeadGenSidebarTop`, `LeadGenSidebarMiddle` | `@elevasis/ui/features/lead-gen` | Feature registration and sidebar composition |
50
- | `LeadGenOverviewPage`, `LeadGenListsPage`, `LeadGenListDetailPage`, `ListBuilderPage`, `LeadGenCompaniesPage`, `LeadGenContactsPage` | `@elevasis/ui/features/lead-gen` | Shared lead-gen pages you can route to or wrap |
51
- | `useLeadGenConfig`, `LeadGenBuildConfig`, build-state helpers | `@elevasis/ui/features/lead-gen` | Provider-backed derivation of stage catalog, build templates, default build steps, default template id, and export workflow id |
52
- | `ListActionsProvider`, `useListActions`, `ListBuilderWorkflow`, `ListBuilderRegistry`, `LeadGenActionKey` | `@elevasis/ui/features/lead-gen` | List Builder workflow registry, slot-based field contracts, and project-owned action wiring |
53
- | `LeadGenRouteShell` | `@elevasis/ui/features/lead-gen` | Route shell helper (contact/company detail surfaces are now `ContactDetailPage` / `CompanyDetailPage` from `@elevasis/ui/features/crm`) |
54
- | `useLists`, `useList`, `useListsTelemetry`, `useListProgress`, `useListExecutions`, `useCreateList`, `useUpdateList`, `useUpdateListConfig`, `useDeleteList` | `@elevasis/ui/hooks` | Headless list and telemetry data access |
55
- | `useWorkflowExecution`, `useExecutionSSE`, `useAddCompaniesToList`, `useRemoveCompaniesFromList`, `useAddContactsToList` | `@elevasis/ui/hooks` | List Builder workflow triggering, live execution tailing, and list membership mutations |
56
- | `useCompanies`, `useCompany`, `useContacts`, `useContact` | `@elevasis/ui/hooks` | Acquisition company/contact data access |
57
- | `useArtifacts`, `useCreateArtifact`, `useListMembers`, `useListMember` | `@elevasis/ui/hooks` | Lead-gen substrate data access |
58
- | `useTransitionList`, `useTransitionListMember`, `useTransitionListCompany`, `useDeriveActions` | `@elevasis/ui/hooks` | Stateful transition mutations and contextual action derivation |
59
- | `ElevasisUIProvider`, `ElevasisCoreProvider`, `useElevasisServices` | `@elevasis/ui/provider` | Provider setup, API access, organization context, and `listActions` registry injection |
60
- | `ElevasisSystemsProvider`, `ElevasisAuthenticatedShell` | `@elevasis/ui/provider`, `@elevasis/ui/app` | Organization Model injection for system-aware surfaces such as lead-gen config, sidebar projection, and Organization OS views |
61
- | `getLeadGenStageCatalog`, `getAllBuildTemplates`, `getProspectingBuildTemplateOptions`, `createBuildPlanSnapshotFromTemplateId` | `@elevasis/core` | Generic Organization Model and template readers. Supply tenant templates/model values from the project. |
62
- | `listBuilderWorkflow` | `@elevasis/sdk/worker` | Generic list-builder workflow runtime for tenant-owned step handlers |
63
- | `acqDb`, `list` | `@elevasis/sdk/worker` | Workflow-side acquisition and list-scoped platform adapters |
64
-
65
- Read the generated contracts before changing typed boundaries:
66
-
67
- `node_modules/@elevasis/sdk/reference/scaffold/reference/contracts.md`
68
-
69
- Look for the **Lead Gen Platform Primitives** section. It includes list shapes, company/contact shapes, list config, telemetry, artifacts, list members, stateful pipeline definitions, and workflow adapter method maps.
70
-
71
- ## 1. Extend Lead-Gen Navigation or Sidebar
72
-
73
- For a simple nav addition, extend `LEAD_GEN_ITEMS` and override the lead-gen system manifest:
74
-
75
- ```tsx
76
- // ui/src/routes/__root.tsx
77
- import { leadGenManifest, LEAD_GEN_ITEMS, LeadGenSidebar, LeadGenSidebarMiddle } from '@elevasis/ui/features/lead-gen'
78
- import type { SystemModule } from '@elevasis/ui/provider'
79
- import type { NavItem } from '@elevasis/ui/layout'
80
- import { IconChartBar } from '@tabler/icons-react'
81
-
82
- const customLeadGenItems: NavItem[] = [
83
- ...LEAD_GEN_ITEMS,
84
- { label: 'Reports', to: '/lead-gen/reports', icon: IconChartBar, exact: false }
85
- ]
86
-
87
- const CustomLeadGenSidebar = () => (
88
- <LeadGenSidebar>
89
- <LeadGenSidebarMiddle items={customLeadGenItems} />
90
- </LeadGenSidebar>
91
- )
92
-
93
- export const customLeadGenManifest: SystemModule = {
94
- ...leadGenManifest,
95
- sidebar: CustomLeadGenSidebar
96
- }
97
- ```
98
-
99
- Then replace `leadGenManifest` with `customLeadGenManifest` in the local `SYSTEM_MANIFESTS` array and add the matching route under `ui/src/routes/lead-gen/`.
100
-
101
- For structural changes, compose `LeadGenSidebarTop`, `SubshellNavList`, and `SubshellSidebarSection`. The full sidebar decision tree lives in `node_modules/@elevasis/sdk/reference/scaffold/ui/customization.md`.
102
-
103
- ## 2. Wrap Shared Lead-Gen Pages
104
-
105
- If the shared page is close, keep it and wrap it:
106
-
107
- ```tsx
108
- // ui/src/routes/lead-gen/lists.index.tsx
109
- import { createFileRoute } from '@tanstack/react-router'
110
- import { LeadGenListsPage } from '@elevasis/ui/features/lead-gen'
111
- import { Stack } from '@mantine/core'
112
- import { ProjectAnnouncementBanner } from '@/lib/components/ProjectAnnouncementBanner'
113
-
114
- export const Route = createFileRoute('/lead-gen/lists/')({
115
- component: ListsRoute
116
- })
117
-
118
- function ListsRoute() {
119
- return (
120
- <Stack gap={0}>
121
- <ProjectAnnouncementBanner context="lead-gen-lists" />
122
- <LeadGenListsPage />
123
- </Stack>
124
- )
125
- }
126
- ```
127
-
128
- For list detail routes, pass the route param into `LeadGenListDetailPage`:
129
-
130
- ```tsx
131
- import { createFileRoute } from '@tanstack/react-router'
132
- import { LeadGenListDetailPage } from '@elevasis/ui/features/lead-gen'
133
-
134
- export const Route = createFileRoute('/lead-gen/lists/$listId')({
135
- component: ListDetailRoute
136
- })
137
-
138
- function ListDetailRoute() {
139
- const { listId } = Route.useParams()
140
- return <LeadGenListDetailPage listId={listId} />
141
- }
142
- ```
143
-
144
- Use the same wrapping pattern for `LeadGenOverviewPage`, `LeadGenCompaniesPage`, and `LeadGenContactsPage`.
145
-
37
+ | Add lead-gen sidebar nav or a lead-gen route | `@elevasis/ui/features/lead-gen` and `node_modules/@elevasis/sdk/reference/scaffold/ui/customization.md` | Prefer manifest/sidebar composition. Do not fork shared source first. |
38
+ | Wrap a shared lead-gen page with project chrome | `LeadGenOverviewPage`, `LeadGenListsPage`, `LeadGenListDetailPage`, `ListBuilderPage`, `LeadGenCompaniesPage`, `LeadGenContactsPage` | Keep route files thin and put project-specific behavior in local feature modules. |
39
+ | Build a custom campaign/list workspace | `ListBuilderPage`, `useLists`, `useList`, `useListProgress`, `useListExecutions`, `useWorkflowExecution`, `useExecutionSSE` from `@elevasis/ui` | Use the shared builder when possible; otherwise compose hooks for platform data and workflow execution. |
40
+ | Add or change list-builder workflow buttons/forms | `ui/src/config/listActions.ts` plus `ElevasisUIProvider listActions={listActions}` / `createElevasisApp({ listActions })` | The shared UI owns the registry contract. The project owns workflow ids, action keys, Zod schemas, layouts, and default inputs. |
41
+ | Render artifacts or list-member detail | `LeadGenListDetailPage`, `useArtifacts`, `useCreateArtifact`, `useListMember` | Artifacts are a substrate primitive. Keep vertical-specific rendering local until there are repeated use cases. |
42
+ | Read or mutate lead-gen data inside a workflow | `acqDb` or `list` from `@elevasis/sdk/worker` | `organizationId` is injected server-side by the platform dispatcher. Do not pass it from workflow code. |
43
+ | Add a new persisted lead-gen field, artifact kind, or transition API | Platform/API migration work, not just scaffold work | Update DB, core schemas/types, API service/handlers, hooks, docs, and scaffold contracts together. |
44
+
45
+ ## Published Lead-Gen Surfaces
46
+
47
+ | Surface | Import from | Use for |
48
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
49
+ | `leadGenManifest`, `LEAD_GEN_ITEMS`, `LeadGenSidebar`, `LeadGenSidebarTop`, `LeadGenSidebarMiddle` | `@elevasis/ui/features/lead-gen` | Feature registration and sidebar composition |
50
+ | `LeadGenOverviewPage`, `LeadGenListsPage`, `LeadGenListDetailPage`, `ListBuilderPage`, `LeadGenCompaniesPage`, `LeadGenContactsPage` | `@elevasis/ui/features/lead-gen` | Shared lead-gen pages you can route to or wrap |
51
+ | `useLeadGenConfig`, `LeadGenBuildConfig`, build-state helpers | `@elevasis/ui/features/lead-gen` | Provider-backed derivation of stage catalog, build templates, default build steps, default template id, and export workflow id |
52
+ | `ListActionsProvider`, `useListActions`, `ListBuilderWorkflow`, `ListBuilderRegistry`, `LeadGenActionKey` | `@elevasis/ui/features/lead-gen` | List Builder workflow registry, slot-based field contracts, and project-owned action wiring |
53
+ | `LeadGenRouteShell` | `@elevasis/ui/features/lead-gen` | Route shell helper (contact/company detail surfaces are now `ContactDetailPage` / `CompanyDetailPage` from `@elevasis/ui/features/crm`) |
54
+ | `useLists`, `useList`, `useListsTelemetry`, `useListProgress`, `useListExecutions`, `useCreateList`, `useUpdateList`, `useUpdateListConfig`, `useDeleteList` | `@elevasis/ui/hooks` | Headless list and telemetry data access |
55
+ | `useWorkflowExecution`, `useExecutionSSE`, `useAddCompaniesToList`, `useRemoveCompaniesFromList`, `useAddContactsToList` | `@elevasis/ui/hooks` | List Builder workflow triggering, live execution tailing, and list membership mutations |
56
+ | `useCompanies`, `useCompany`, `useContacts`, `useContact` | `@elevasis/ui/hooks` | Acquisition company/contact data access |
57
+ | `useArtifacts`, `useCreateArtifact`, `useListMembers`, `useListMember` | `@elevasis/ui/hooks` | Lead-gen substrate data access |
58
+ | `useTransitionList`, `useTransitionListMember`, `useTransitionListCompany`, `useDeriveActions` | `@elevasis/ui/hooks` | Stateful transition mutations and contextual action derivation |
59
+ | `ElevasisUIProvider`, `ElevasisCoreProvider`, `useElevasisServices` | `@elevasis/ui/provider` | Provider setup, API access, organization context, and `listActions` registry injection |
60
+ | `ElevasisSystemsProvider`, `ElevasisAuthenticatedShell` | `@elevasis/ui/provider`, `@elevasis/ui/app` | Organization Model injection for system-aware surfaces such as lead-gen config, sidebar projection, and Organization OS views |
61
+ | `getLeadGenStageCatalog`, `getAllBuildTemplates`, `getProspectingBuildTemplateOptions`, `createBuildPlanSnapshotFromTemplateId` | `@elevasis/core` | Generic Organization Model and template readers. Supply tenant templates/model values from the project. |
62
+ | `listBuilderWorkflow` | `@elevasis/sdk/worker` | Generic list-builder workflow runtime for tenant-owned step handlers |
63
+ | `acqDb`, `list` | `@elevasis/sdk/worker` | Workflow-side acquisition and list-scoped platform adapters |
64
+
65
+ Read the generated contracts before changing typed boundaries:
66
+
67
+ `node_modules/@elevasis/sdk/reference/scaffold/reference/contracts.md`
68
+
69
+ Look for the **Lead Gen Platform Primitives** section. It includes list shapes, company/contact shapes, list config, telemetry, artifacts, list members, stateful pipeline definitions, and workflow adapter method maps.
70
+
71
+ ## 1. Extend Lead-Gen Navigation or Sidebar
72
+
73
+ For a simple nav addition, extend `LEAD_GEN_ITEMS` and override the lead-gen system manifest:
74
+
75
+ ```tsx
76
+ // ui/src/routes/__root.tsx
77
+ import { leadGenManifest, LEAD_GEN_ITEMS, LeadGenSidebar, LeadGenSidebarMiddle } from '@elevasis/ui/features/lead-gen'
78
+ import type { SystemModule } from '@elevasis/ui/provider'
79
+ import type { NavItem } from '@elevasis/ui/layout'
80
+ import { IconChartBar } from '@tabler/icons-react'
81
+
82
+ const customLeadGenItems: NavItem[] = [
83
+ ...LEAD_GEN_ITEMS,
84
+ { label: 'Reports', to: '/lead-gen/reports', icon: IconChartBar, exact: false }
85
+ ]
86
+
87
+ const CustomLeadGenSidebar = () => (
88
+ <LeadGenSidebar>
89
+ <LeadGenSidebarMiddle items={customLeadGenItems} />
90
+ </LeadGenSidebar>
91
+ )
92
+
93
+ export const customLeadGenManifest: SystemModule = {
94
+ ...leadGenManifest,
95
+ sidebar: CustomLeadGenSidebar
96
+ }
97
+ ```
98
+
99
+ Then replace `leadGenManifest` with `customLeadGenManifest` in the local `SYSTEM_MANIFESTS` array and add the matching route under `ui/src/routes/lead-gen/`.
100
+
101
+ For structural changes, compose `LeadGenSidebarTop`, `SubshellNavList`, and `SubshellSidebarSection`. The full sidebar decision tree lives in `node_modules/@elevasis/sdk/reference/scaffold/ui/customization.md`.
102
+
103
+ ## 2. Wrap Shared Lead-Gen Pages
104
+
105
+ If the shared page is close, keep it and wrap it:
106
+
107
+ ```tsx
108
+ // ui/src/routes/lead-gen/lists.index.tsx
109
+ import { createFileRoute } from '@tanstack/react-router'
110
+ import { LeadGenListsPage } from '@elevasis/ui/features/lead-gen'
111
+ import { Stack } from '@mantine/core'
112
+ import { ProjectAnnouncementBanner } from '@/lib/components/ProjectAnnouncementBanner'
113
+
114
+ export const Route = createFileRoute('/lead-gen/lists/')({
115
+ component: ListsRoute
116
+ })
117
+
118
+ function ListsRoute() {
119
+ return (
120
+ <Stack gap={0}>
121
+ <ProjectAnnouncementBanner context="lead-gen-lists" />
122
+ <LeadGenListsPage />
123
+ </Stack>
124
+ )
125
+ }
126
+ ```
127
+
128
+ For list detail routes, pass the route param into `LeadGenListDetailPage`:
129
+
130
+ ```tsx
131
+ import { createFileRoute } from '@tanstack/react-router'
132
+ import { LeadGenListDetailPage } from '@elevasis/ui/features/lead-gen'
133
+
134
+ export const Route = createFileRoute('/lead-gen/lists/$listId')({
135
+ component: ListDetailRoute
136
+ })
137
+
138
+ function ListDetailRoute() {
139
+ const { listId } = Route.useParams()
140
+ return <LeadGenListDetailPage listId={listId} />
141
+ }
142
+ ```
143
+
144
+ Use the same wrapping pattern for `LeadGenOverviewPage`, `LeadGenCompaniesPage`, and `LeadGenContactsPage`.
145
+
146
146
  For the real-time List Builder workspace, add a thin route that passes the route param into `ListBuilderPage`:
147
-
148
- ```tsx
149
- import { createFileRoute } from '@tanstack/react-router'
150
- import { ListBuilderPage } from '@elevasis/ui/features/lead-gen'
151
-
152
- export const Route = createFileRoute('/lead-gen/list-builder/$listId')({
153
- component: ListBuilderRoute
154
- })
155
-
156
- function ListBuilderRoute() {
157
- const { listId } = Route.useParams()
158
- return <ListBuilderPage listId={listId} />
159
- }
160
- ```
161
-
147
+
148
+ ```tsx
149
+ import { createFileRoute } from '@tanstack/react-router'
150
+ import { ListBuilderPage } from '@elevasis/ui/features/lead-gen'
151
+
152
+ export const Route = createFileRoute('/lead-gen/list-builder/$listId')({
153
+ component: ListBuilderRoute
154
+ })
155
+
156
+ function ListBuilderRoute() {
157
+ const { listId } = Route.useParams()
158
+ return <ListBuilderPage listId={listId} />
159
+ }
160
+ ```
161
+
162
162
  Register project workflow actions at the provider boundary. The shared UI owns the contract; each project owns workflow ids, action keys, default inputs, and field components.
163
163
 
164
164
  The shared pages call `useLeadGenConfig()` internally. That hook reads the provider-injected `organizationModel` and derives:
@@ -201,49 +201,49 @@ export function RootLayout() {
201
201
  ```
202
202
 
203
203
  Data sourcing mode is list-wide. Read `list.pipelineConfig.dataMode` or the workflow-side `list.getConfig()` result when a workflow must choose mock versus live sourcing. Do not add per-action `mock` / `live` controls for Apollo, crawl, enrichment, or scoring steps. Export mode is separate: `preview` versus `export` controls whether a destination write happens.
204
-
204
+
205
205
  Each registry entry declares a Zod `schema` and a `layout` of declarative field hints (`StepConfigLayout<Input>`). The shared `StepConfigForm` renders the layout, validates against the schema, and wires `value`/`onChange` for you — no per-action React components. The List Builder right column renders the form as `Configuration | Advanced | Runs` tabs with a sticky action footer. Omit the `advanced:` section when a step has none.
206
-
207
- Available field component variants: `textinput`, `textarea`, `numberinput`, `switch`, `segmented`, `select`, `multiselect`, `tags`, `json`. Field hints support `label`, `description`, `placeholder`, `min`/`max`/`step` (numbers), `options` (selects), and `when: (values) => boolean` for conditional visibility.
208
-
209
- ```tsx
206
+
207
+ Available field component variants: `textinput`, `textarea`, `numberinput`, `switch`, `segmented`, `select`, `multiselect`, `tags`, `json`. Field hints support `label`, `description`, `placeholder`, `min`/`max`/`step` (numbers), `options` (selects), and `when: (values) => boolean` for conditional visibility.
208
+
209
+ ```tsx
210
210
  import * as z from 'zod'
211
211
  import { leadGenManifest, type ListBuilderRegistry } from '@elevasis/ui/features/lead-gen'
212
212
  import type { StepConfigLayout } from '@elevasis/ui/components/forms'
213
213
  import { ElevasisSystemsProvider, ElevasisUIProvider } from '@elevasis/ui/provider'
214
214
  import { organizationModel, resourceDescriptors } from '@core/config/organization-model'
215
-
215
+
216
216
  const companyCleanupInputSchema = z.object({
217
217
  listId: z.string().uuid(),
218
218
  targetDescription: z.string(),
219
219
  dryRun: z.boolean().default(true),
220
220
  batchSize: z.number().int().min(1).default(20)
221
221
  })
222
-
223
- type CompanyCleanupInput = z.infer<typeof companyCleanupInputSchema>
224
- const companyCleanupResource = resourceDescriptors.companyCleanup
225
-
226
- const companyCleanupLayout: StepConfigLayout<CompanyCleanupInput> = {
227
- sections: [
228
- {
229
- id: 'configuration',
230
- fields: [
231
- {
232
- path: 'targetDescription',
233
- component: 'textarea',
234
- label: 'Target description',
235
- placeholder: 'independent veterinary clinics in Orange County'
236
- },
237
- { path: 'dryRun', component: 'switch', label: 'Dry run' }
238
- ]
239
- }
240
- ],
222
+
223
+ type CompanyCleanupInput = z.infer<typeof companyCleanupInputSchema>
224
+ const companyCleanupResource = resourceDescriptors.companyCleanup
225
+
226
+ const companyCleanupLayout: StepConfigLayout<CompanyCleanupInput> = {
227
+ sections: [
228
+ {
229
+ id: 'configuration',
230
+ fields: [
231
+ {
232
+ path: 'targetDescription',
233
+ component: 'textarea',
234
+ label: 'Target description',
235
+ placeholder: 'independent veterinary clinics in Orange County'
236
+ },
237
+ { path: 'dryRun', component: 'switch', label: 'Dry run' }
238
+ ]
239
+ }
240
+ ],
241
241
  advanced: {
242
242
  id: 'advanced',
243
243
  fields: [{ path: 'batchSize', component: 'numberinput', label: 'Batch size', min: 1 }]
244
244
  }
245
245
  }
246
-
246
+
247
247
  const listActions: ListBuilderRegistry = [
248
248
  {
249
249
  resourceId: companyCleanupResource.id,
@@ -254,13 +254,13 @@ const listActions: ListBuilderRegistry = [
254
254
  category: 'utility',
255
255
  stagesAffected: ['populated'],
256
256
  schema: companyCleanupInputSchema,
257
- layout: companyCleanupLayout,
258
- defaultInput: (list) => ({
259
- listId: list.id,
260
- targetDescription: '',
261
- dryRun: true,
262
- batchSize: 20
263
- })
257
+ layout: companyCleanupLayout,
258
+ defaultInput: (list) => ({
259
+ listId: list.id,
260
+ targetDescription: '',
261
+ dryRun: true,
262
+ batchSize: 20
263
+ })
264
264
  }
265
265
  ]
266
266
 
@@ -283,134 +283,134 @@ export function App() {
283
283
  Author the `schema` in a browser-safe sibling file (no Node-only imports) so it can be reused by the workflow runtime AND the UI form. See `apollo-import-schema.ts` in the platform for the canonical split pattern.
284
284
 
285
285
  Keep the registry in the external project, usually `ui/src/config/listActions.ts`. Do not add client workflow ids, client action keys, or client default inputs to `@elevasis/ui`.
286
-
287
- Per-section split rule (operator-grade vs power-user):
288
-
286
+
287
+ Per-section split rule (operator-grade vs power-user):
288
+
289
289
  - **Configuration:** target/limit, dry-run toggle, export mode (`preview` / `export`) when the action writes to an external destination, and any field an operator changes between runs.
290
290
  - **Advanced:** concurrency, batch size, timeouts, credential names, and workflow chaining. Do not put mock/live controls here; data sourcing mode is list-wide. Omit the `advanced:` key entirely when a step has none.
291
- - **`defaultInput`:** seeds the form before the user touches anything; must include `listId: list.id`.
292
-
293
- ## 3. Build a Custom Campaign Workspace
294
-
295
- When the project needs custom layout or vertical-specific rendering, use the hooks directly:
296
-
297
- ```tsx
298
- import { Badge, Button, Card, Group, Stack, Text } from '@mantine/core'
299
- import { useArtifacts, useList, useListMembers, useTransitionListMember } from '@elevasis/ui/hooks'
300
-
301
- export function CampaignWorkspace({ listId }: { listId: string }) {
302
- const listQuery = useList(listId)
303
- const membersQuery = useListMembers({ listId })
304
- const artifactsQuery = useArtifacts({ ownerKind: 'list', ownerId: listId })
305
- const transitionMember = useTransitionListMember()
306
-
307
- const list = listQuery.data
308
- if (!list) return null
309
-
310
- const firstMember = membersQuery.data?.members[0]
311
-
312
- return (
313
- <Stack>
314
- <Group justify="space-between">
315
- <div>
316
- <Text fw={700}>{list.name}</Text>
317
- <Text size="sm" c="dimmed">{list.description}</Text>
318
- </div>
319
- <Badge>{list.stateKey}</Badge>
320
- </Group>
321
-
322
- <Card withBorder>
323
- <Text size="sm">Artifacts: {artifactsQuery.data?.artifacts.length ?? 0}</Text>
324
- </Card>
325
-
326
- {firstMember ? (
327
- <Button
328
- onClick={() =>
329
- transitionMember.mutate({
330
- memberId: firstMember.id,
331
- listId,
332
- pipelineKey: 'lead-gen',
333
- stageKey: 'prospecting',
334
- stateKey: 'verified'
335
- })
336
- }
337
- >
338
- Mark First Member Verified
339
- </Button>
340
- ) : null}
341
- </Stack>
342
- )
343
- }
344
- ```
345
-
346
- Use `useArtifacts({ ownerKind, ownerId })` for durable JSON artifacts like audits, research summaries, snapshots, exports, or model outputs.
347
-
348
- ## 4. Read and Mutate Lead-Gen Data in Workflows
349
-
350
- Inside deployed workflows, use worker adapters instead of browser hooks or direct database access:
351
-
352
- External projects should define workflow input/output schemas in `@shared/types`; the example below assumes those shared schemas already exist.
353
-
354
- ```ts
355
- // operations/src/sales/qualify-list.ts
356
- import type { WorkflowDefinition } from '@elevasis/sdk'
357
- import { acqDb, list } from '@elevasis/sdk/worker'
358
- import { qualifyListInputSchema, qualifyListOutputSchema } from '@shared/types'
359
- import { resourceDescriptors } from '@core/config/organization-model'
360
-
361
- export const qualifyListWorkflow: WorkflowDefinition = {
362
- config: {
363
- resource: resourceDescriptors.qualifyList,
364
- resourceId: resourceDescriptors.qualifyList.id,
365
- name: 'Qualify List',
366
- type: resourceDescriptors.qualifyList.kind,
367
- version: '1.0.0',
368
- status: 'dev',
369
- },
370
- contract: { inputSchema: qualifyListInputSchema, outputSchema: qualifyListOutputSchema },
371
- steps: {
372
- qualify: {
373
- id: 'qualify',
374
- name: 'Qualify',
375
- inputSchema: qualifyListInputSchema,
376
- outputSchema: qualifyListOutputSchema,
377
- next: null,
378
- handler: async (input, context) => {
379
- const config = await list.getConfig({ listId: input.listId })
380
- const contacts = await acqDb.listContacts({ listId: input.listId, limit: 100, offset: 0 })
381
-
382
- for (const contact of contacts.data) {
383
- await list.updateContactStage({
384
- listId: input.listId,
385
- contactId: contact.id,
386
- stage: 'verified',
387
- executionId: context.executionId
388
- })
389
- }
390
-
391
- await list.recordExecution({
392
- listId: input.listId,
393
- executionId: context.executionId,
394
- configSnapshot: config
395
- })
396
-
397
- return { touched: contacts.data.length }
398
- }
399
- }
400
- },
401
- entryPoint: 'qualify'
402
- }
403
- ```
404
-
405
- Use `list` for focused list-scoped workflow operations: fetch list config, record a list execution, and update company/contact list stages. Use `acqDb` when the workflow needs broader acquisition operations: create/update lists, companies, contacts, add contacts to lists, bulk import, enrichment, social posts, or CRM deal sync methods.
406
-
407
- Keep these boundaries straight:
408
-
409
- - Browser/UI code uses `@elevasis/ui/hooks`.
410
- - Workflow code uses `@elevasis/sdk/worker` adapters.
411
- - `organizationId` is injected by the platform dispatcher. Do not pass it from workflow code.
412
- - Persisted API shapes are owned by core/API contracts, not route-local TypeScript.
413
-
291
+ - **`defaultInput`:** seeds the form before the user touches anything; must include `listId: list.id`.
292
+
293
+ ## 3. Build a Custom Campaign Workspace
294
+
295
+ When the project needs custom layout or vertical-specific rendering, use the hooks directly:
296
+
297
+ ```tsx
298
+ import { Badge, Button, Card, Group, Stack, Text } from '@mantine/core'
299
+ import { useArtifacts, useList, useListMembers, useTransitionListMember } from '@elevasis/ui/hooks'
300
+
301
+ export function CampaignWorkspace({ listId }: { listId: string }) {
302
+ const listQuery = useList(listId)
303
+ const membersQuery = useListMembers({ listId })
304
+ const artifactsQuery = useArtifacts({ ownerKind: 'list', ownerId: listId })
305
+ const transitionMember = useTransitionListMember()
306
+
307
+ const list = listQuery.data
308
+ if (!list) return null
309
+
310
+ const firstMember = membersQuery.data?.members[0]
311
+
312
+ return (
313
+ <Stack>
314
+ <Group justify="space-between">
315
+ <div>
316
+ <Text fw={700}>{list.name}</Text>
317
+ <Text size="sm" c="dimmed">{list.description}</Text>
318
+ </div>
319
+ <Badge>{list.stateKey}</Badge>
320
+ </Group>
321
+
322
+ <Card withBorder>
323
+ <Text size="sm">Artifacts: {artifactsQuery.data?.artifacts.length ?? 0}</Text>
324
+ </Card>
325
+
326
+ {firstMember ? (
327
+ <Button
328
+ onClick={() =>
329
+ transitionMember.mutate({
330
+ memberId: firstMember.id,
331
+ listId,
332
+ pipelineKey: 'lead-gen',
333
+ stageKey: 'prospecting',
334
+ stateKey: 'verified'
335
+ })
336
+ }
337
+ >
338
+ Mark First Member Verified
339
+ </Button>
340
+ ) : null}
341
+ </Stack>
342
+ )
343
+ }
344
+ ```
345
+
346
+ Use `useArtifacts({ ownerKind, ownerId })` for durable JSON artifacts like audits, research summaries, snapshots, exports, or model outputs.
347
+
348
+ ## 4. Read and Mutate Lead-Gen Data in Workflows
349
+
350
+ Inside deployed workflows, use worker adapters instead of browser hooks or direct database access:
351
+
352
+ External projects should define workflow input/output schemas in `@shared/types`; the example below assumes those shared schemas already exist.
353
+
354
+ ```ts
355
+ // operations/src/sales/qualify-list.ts
356
+ import type { WorkflowDefinition } from '@elevasis/sdk'
357
+ import { acqDb, list } from '@elevasis/sdk/worker'
358
+ import { qualifyListInputSchema, qualifyListOutputSchema } from '@shared/types'
359
+ import { resourceDescriptors } from '@core/config/organization-model'
360
+
361
+ export const qualifyListWorkflow: WorkflowDefinition = {
362
+ config: {
363
+ resource: resourceDescriptors.qualifyList,
364
+ resourceId: resourceDescriptors.qualifyList.id,
365
+ name: 'Qualify List',
366
+ type: resourceDescriptors.qualifyList.kind,
367
+ version: '1.0.0',
368
+ status: 'dev',
369
+ },
370
+ contract: { inputSchema: qualifyListInputSchema, outputSchema: qualifyListOutputSchema },
371
+ steps: {
372
+ qualify: {
373
+ id: 'qualify',
374
+ name: 'Qualify',
375
+ inputSchema: qualifyListInputSchema,
376
+ outputSchema: qualifyListOutputSchema,
377
+ next: null,
378
+ handler: async (input, context) => {
379
+ const config = await list.getConfig({ listId: input.listId })
380
+ const contacts = await acqDb.listContacts({ listId: input.listId, limit: 100, offset: 0 })
381
+
382
+ for (const contact of contacts.data) {
383
+ await list.updateContactStage({
384
+ listId: input.listId,
385
+ contactId: contact.id,
386
+ stage: 'verified',
387
+ executionId: context.executionId
388
+ })
389
+ }
390
+
391
+ await list.recordExecution({
392
+ listId: input.listId,
393
+ executionId: context.executionId,
394
+ configSnapshot: config
395
+ })
396
+
397
+ return { touched: contacts.data.length }
398
+ }
399
+ }
400
+ },
401
+ entryPoint: 'qualify'
402
+ }
403
+ ```
404
+
405
+ Use `list` for focused list-scoped workflow operations: fetch list config, record a list execution, and update company/contact list stages. Use `acqDb` when the workflow needs broader acquisition operations: create/update lists, companies, contacts, add contacts to lists, bulk import, enrichment, social posts, or CRM deal sync methods.
406
+
407
+ Keep these boundaries straight:
408
+
409
+ - Browser/UI code uses `@elevasis/ui/hooks`.
410
+ - Workflow code uses `@elevasis/sdk/worker` adapters.
411
+ - `organizationId` is injected by the platform dispatcher. Do not pass it from workflow code.
412
+ - Persisted API shapes are owned by core/API contracts, not route-local TypeScript.
413
+
414
414
  ## 5. Customize Lead-Gen Semantics
415
415
 
416
416
  The lead-gen shell module key is `lead-gen`, the Organization OS System id is `sales.lead-gen`, and the action currently used by the shared manifest is `leadgen.lists.manage`.
@@ -430,30 +430,203 @@ Tenant-owned list-builder values belong in the main Organization Model:
430
430
  - lead-gen workflow resources that bind the CRM pipeline only when they intentionally hand off to CRM, plus a scoped topology grant from those lead-gen resources to `sales.crm.apiInterface`
431
431
 
432
432
  Use `core/config/extensions/*` for entity schema variants, not for list-builder stage catalogs or workflow bindings.
433
-
434
- The platform-side lead-gen stateful vocabulary is defined for:
435
-
436
- - `acq.list`
437
- - `acq.list-member`
438
- - `acq.list-company`
439
-
440
- The generated contracts expose `StatefulPipelineDefinition` and `LEAD_GEN_PIPELINE_DEFINITIONS` so downstream agents can inspect the current pipeline/stage/state vocabulary before adding UI labels or workflow transitions.
441
-
442
- Changing persisted platform stages, adding new owner kinds, or extending artifact schema requires coordinated core/API/UI updates. Relabeling, route wrapping, sidebar changes, and project-owned rendering usually stay in the downstream project.
443
-
444
- ## Verify
445
-
446
- Run the checks for the surfaces you touched:
447
-
448
- ```bash
449
- pnpm -C ui run check
450
- pnpm -C operations run check
451
- pnpm -C operations exec elevasis-sdk check
452
- ```
453
-
454
- If you changed platform-level lead-gen contracts in the monorepo, the platform maintainer must also regenerate and verify scaffold output:
455
-
456
- ```bash
457
- pnpm scaffold:sync
458
- pnpm scaffold:verify
459
- ```
433
+
434
+ The platform-side lead-gen stateful vocabulary is defined for:
435
+
436
+ - `acq.list`
437
+ - `acq.list-member`
438
+ - `acq.list-company`
439
+
440
+ The generated contracts expose `StatefulPipelineDefinition` and `LEAD_GEN_PIPELINE_DEFINITIONS` so downstream agents can inspect the current pipeline/stage/state vocabulary before adding UI labels or workflow transitions.
441
+
442
+ Changing persisted platform stages, adding new owner kinds, or extending artifact schema requires coordinated core/API/UI updates. Relabeling, route wrapping, sidebar changes, and project-owned rendering usually stay in the downstream project.
443
+
444
+ ## 6. Build Your Own List-Builder System
445
+
446
+ This section covers the end-to-end path for a tenant authoring a custom list-builder system from scratch. The platform now supports full OM divergence: your deployed snapshot is validated against its own `model_hash`, not against the Elevasis canonical OM. Diverging your OM is a supported, intentional authoring path.
447
+
448
+ ### Per-Tenant Integrity Model
449
+
450
+ When you deploy an OM snapshot, the API stamps a SHA-256 `model_hash` and stores it alongside your snapshot. At resolve time, the platform compares the in-model `snapshotHash` against that stored `model_hash`. If they match, the snapshot is intact. Canonical equality (matching Elevasis's internal model) is enforced only for Elevasis-internal/dogfood organizations — it does not apply to tenants.
451
+
452
+ Practical consequence: you can rename systems, restructure ontology, swap export channels, or add custom stages without triggering a hash-mismatch 503. A redeploy updates the stamped hash and the snapshot together. The `model_hash` is stamped server-side by the CLI deploy command — no client-side hash computation is needed or correct.
453
+
454
+ ### Declaring an OM Readiness Contract
455
+
456
+ When you define a custom `readinessProfile` id (any string that is not one of the built-in presets `sales.lead-gen.api`, `sales.crm.api`, `sales.lead-gen.crm-handoff`), you must also provide a `readinessContract` on the `apiInterface` so the platform can validate structural readiness without hardcoded constants.
457
+
458
+ ```ts
459
+ // core/config/organization-model.ts
460
+ import { registerReadinessProfile } from '@elevasis/sdk'
461
+
462
+ // Register the profile id at startup so the platform recognizes it
463
+ registerReadinessProfile('acme.prospecting.api')
464
+
465
+ export const organizationModel = {
466
+ // ...
467
+ systems: {
468
+ 'acme.prospecting': {
469
+ id: 'acme.prospecting',
470
+ label: 'Prospecting',
471
+ order: 10,
472
+ kind: 'product',
473
+ lifecycle: 'active',
474
+ apiInterface: {
475
+ lifecycle: 'active',
476
+ readinessProfile: 'acme.prospecting.api',
477
+ resourceIds: ['acme-list-build-workflow', 'acme-list-export-workflow'],
478
+ readinessContract: {
479
+ requiredObjects: [
480
+ 'acme.prospecting:object/company',
481
+ 'acme.prospecting:object/contact'
482
+ ],
483
+ requiredCatalogs: [
484
+ 'acme.prospecting:catalog/company-stage'
485
+ ]
486
+ }
487
+ }
488
+ }
489
+ }
490
+ }
491
+ ```
492
+
493
+ `readinessContract.requiredObjects` and `requiredCatalogs` are arrays of ontology IDs owned by this system. The platform validates that each declared object type exists in the OM and that each declared catalog type has at least one entry. The `requiredKinds` field is reserved for future profile extensions; omit it today.
494
+
495
+ Built-in profile ids (`sales.lead-gen.api`, etc.) derive their requirements from platform code. If you use a built-in profile on a custom system, you do not need a `readinessContract`.
496
+
497
+ The `lookupReadinessProfile`, `registerReadinessProfile`, and `isBuiltInReadinessProfile` functions are exported from `@elevasis/sdk` alongside the `SystemApiInterfaceReadinessContract`, `ReadinessProfileKind`, and `ReadinessProfileEntry` types.
498
+
499
+ ### Export Channel as Tenant Business Config
500
+
501
+ The export channel — ClickUp, CSV/XLSX file, platform push, or a custom destination — is an action + credential choice declared in your tenant OM. The platform owns the list-builder workflow primitive and the file storage adapters; you own which workflow id handles export and which credential it uses.
502
+
503
+ ```ts
504
+ // operations/src/sales/prospecting/export-list.ts
505
+ import type { WorkflowDefinition } from '@elevasis/sdk'
506
+ import { list, storage } from '@elevasis/sdk/worker'
507
+ import { exportListInputSchema, exportListOutputSchema } from '@shared/types'
508
+
509
+ export const exportListWorkflow: WorkflowDefinition = {
510
+ config: {
511
+ resource: resourceDescriptors.exportList,
512
+ resourceId: resourceDescriptors.exportList.id,
513
+ name: 'Export List',
514
+ type: 'workflow',
515
+ version: '1.0.0',
516
+ status: 'prod'
517
+ },
518
+ contract: { inputSchema: exportListInputSchema, outputSchema: exportListOutputSchema },
519
+ steps: {
520
+ export: {
521
+ id: 'export',
522
+ name: 'Export',
523
+ inputSchema: exportListInputSchema,
524
+ outputSchema: exportListOutputSchema,
525
+ next: null,
526
+ handler: async (input, context) => {
527
+ // Fetch contacts from the acquisition substrate
528
+ const contacts = await acqDb.listContacts({ listId: input.listId, limit: 5000, offset: 0 })
529
+
530
+ // Build CSV content tenant-side — no platform export format is assumed
531
+ const csv = buildCsvFromContacts(contacts.data)
532
+
533
+ // Store via the platform storage adapter
534
+ const result = await storage.upload({
535
+ bucket: 'exports',
536
+ path: `lists/${input.listId}/export.csv`,
537
+ content: csv,
538
+ contentType: 'text/csv'
539
+ })
540
+
541
+ await list.recordExecution({
542
+ listId: input.listId,
543
+ executionId: context.executionId,
544
+ configSnapshot: { mode: input.mode }
545
+ })
546
+
547
+ return { exportUrl: result.signedUrl }
548
+ }
549
+ }
550
+ },
551
+ entryPoint: 'export'
552
+ }
553
+ ```
554
+
555
+ The export destination is a workflow-internal decision. The platform never asserts that the destination must be ClickUp, a specific file bucket, or any particular channel. If the export requires a credential (an external SaaS API key), declare it as a `credentialRequirements` entry on the build template step in your ontology catalog and surface the credential name as a configuration field in the `listActions` registry entry.
556
+
557
+ ### Custom Stages and OM-Derived Telemetry
558
+
559
+ The `ListTelemetry.stageCounts` field is now a `Record<string, number>` — one key per stage key in your tenant's stage catalog. The platform derives telemetry from the OM at compute time, not from a fixed seven-key set.
560
+
561
+ To define custom stages, author them in the `catalogTypes` scope of your lead-gen system:
562
+
563
+ ```ts
564
+ ontology: {
565
+ catalogTypes: {
566
+ 'acme.prospecting:catalog/company-stage': {
567
+ id: 'acme.prospecting:catalog/company-stage',
568
+ label: 'Company Stage',
569
+ entries: {
570
+ 'sourced': { label: 'Sourced', order: 10, semanticClass: 'open' },
571
+ 'enriched': { label: 'Enriched', order: 20, semanticClass: 'open' },
572
+ 'qualified': { label: 'Qualified', order: 30, semanticClass: 'open' },
573
+ 'contacted': { label: 'Contacted', order: 40, semanticClass: 'open' },
574
+ 'disqualified':{ label: 'Disqualified',order: 50, semanticClass: 'failed' }
575
+ }
576
+ }
577
+ }
578
+ }
579
+ ```
580
+
581
+ `getLeadGenStageCatalog(organizationModel)` from `@elevasis/core` reads the caller-supplied model and returns the live stage map. The platform `useListsTelemetry` hook and the API telemetry endpoint aggregate `processingState` columns against your declared stage keys. Stages not in your catalog are not dropped — they appear under their raw key in `stageCounts`.
582
+
583
+ **`readinessTarget` — declaring the pipeline objective.** To tell the platform which stage is the "ready / next-focus" milestone for your pipeline, mark exactly one catalog entry with `readinessTarget: true`:
584
+
585
+ ```ts
586
+ entries: {
587
+ 'sourced': { label: 'Sourced', order: 10 },
588
+ 'enriched': { label: 'Enriched', order: 20 },
589
+ 'qualified': { label: 'Qualified', order: 30 },
590
+ 'contacted': { label: 'Contacted', order: 40, readinessTarget: true }
591
+ }
592
+ ```
593
+
594
+ The `LeadGenOverviewPage` uses this field to derive a "contacts ready for \<stage label\>" backlog without hardcoding stage keys. Backlog = `count(predecessor-by-order) − count(readinessTarget-stage)`, clamped at 0. The stat card label and summary line are derived from the target stage's `label` field. When no stage is flagged `readinessTarget`, the Overview falls back to a generic last-consecutive-stage-pair backlog and labels it "Next-stage backlog" — correct for any tenant with no authoring required.
595
+
596
+ ### Wiring the Custom System to the List-Builder Workflow
597
+
598
+ Point the `listBuilderWorkflow` factory at your tenant resource and step handlers. The factory validates `stageKey` inputs against your catalog at construction time, not at runtime:
599
+
600
+ ```ts
601
+ // operations/src/sales/prospecting/list-builder.ts
602
+ import { listBuilderWorkflow } from '@elevasis/sdk/worker'
603
+ import { resourceDescriptors } from '@core/config/organization-model'
604
+
605
+ export const acmeListBuilderWorkflow = listBuilderWorkflow({
606
+ resource: resourceDescriptors.acmeListBuild,
607
+ steps: {
608
+ source: sourceCompaniesStep,
609
+ enrich: enrichCompaniesStep,
610
+ qualify: qualifyCompaniesStep
611
+ }
612
+ })
613
+ ```
614
+
615
+ Register this workflow in `DeploymentSpec.workflows` alongside the export workflow so both are deployed as a pair.
616
+
617
+ ## Verify
618
+
619
+ Run the checks for the surfaces you touched:
620
+
621
+ ```bash
622
+ pnpm -C ui run check
623
+ pnpm -C operations run check
624
+ pnpm -C operations exec elevasis-sdk check
625
+ ```
626
+
627
+ If you changed platform-level lead-gen contracts in the monorepo, the platform maintainer must also regenerate and verify scaffold output:
628
+
629
+ ```bash
630
+ pnpm scaffold:sync
631
+ pnpm scaffold:verify
632
+ ```