@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.
- package/dist/cli.cjs +64 -22
- package/dist/index.d.ts +105 -7
- package/dist/index.js +81 -33
- package/dist/node/index.d.ts +6 -5
- package/dist/test-utils/index.d.ts +6 -5
- package/dist/test-utils/index.js +70 -32
- package/dist/worker/index.js +7 -11
- package/package.json +4 -4
- package/reference/claude-config/Overview.md +140 -32
- package/reference/claude-config/rules/active-change-index.md +13 -2
- package/reference/claude-config/rules/agent-start-here.md +13 -2
- package/reference/claude-config/rules/deployment.md +13 -2
- package/reference/claude-config/rules/error-handling.md +13 -2
- package/reference/claude-config/rules/execution.md +13 -2
- package/reference/claude-config/rules/frontend.md +13 -2
- package/reference/claude-config/rules/observability.md +13 -2
- package/reference/claude-config/rules/operations.md +13 -2
- package/reference/claude-config/rules/organization-model.md +1 -1
- package/reference/claude-config/rules/organization-os.md +1 -1
- package/reference/claude-config/rules/package-taxonomy.md +13 -2
- package/reference/claude-config/rules/platform.md +13 -2
- package/reference/claude-config/rules/shared-types.md +13 -2
- package/reference/claude-config/rules/task-tracking.md +13 -2
- package/reference/claude-config/rules/topbar-actions.md +2 -2
- package/reference/claude-config/rules/ui.md +13 -2
- package/reference/claude-config/rules/vibe.md +13 -2
- package/reference/claude-config/settings.json +30 -34
- package/reference/claude-config/skills/deploy/SKILL.md +159 -156
- package/reference/claude-config/skills/elevasis/SKILL.md +11 -4
- package/reference/claude-config/skills/explore/SKILL.md +78 -78
- package/reference/claude-config/skills/git-sync/SKILL.md +166 -126
- package/reference/claude-config/skills/om/SKILL.md +15 -15
- package/reference/claude-config/skills/om/operations/build.md +2 -2
- package/reference/claude-config/skills/project/SKILL.md +1 -1
- package/reference/claude-config/skills/save/SKILL.md +183 -183
- package/reference/claude-config/skills/setup/SKILL.md +9 -3
- package/reference/claude-config/skills/status/SKILL.md +59 -59
- package/reference/claude-config/skills/sync/SKILL.md +47 -47
- package/reference/claude-config/skills/tutorial/SKILL.md +1 -1
- package/reference/claude-config/skills/tutorial/technical.md +11 -11
- package/reference/claude-config/sync-notes/2026-06-15-session-chat-zero-wiring.md +46 -0
- package/reference/claude-config/sync-notes/2026-06-17-agent-session-ux-features.md +34 -0
- package/reference/claude-config/sync-notes/2026-06-25-shared-page-scroll-contract-guard.md +52 -0
- package/reference/claude-config/sync-notes/2026-06-26-leadgen-overview-om-telemetry.md +47 -0
- package/reference/claude-config/sync-notes/2026-07-21-agent-scaffold-hardening.md +75 -0
- package/reference/rules/active-change-index.md +5 -5
- package/reference/rules/agent-start-here.md +34 -30
- package/reference/rules/deployment.md +21 -8
- package/reference/rules/frontend.md +4 -4
- package/reference/rules/observability.md +1 -1
- package/reference/rules/organization-model.md +1 -1
- package/reference/rules/organization-os.md +29 -29
- package/reference/rules/ui.md +205 -202
- package/reference/rules/vibe.md +5 -4
- package/reference/scaffold/operations/propagation-pipeline.md +1 -1
- package/reference/scaffold/recipes/extend-lead-gen.md +505 -332
- 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
|
|
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
|
|
38
|
-
| Wrap a shared lead-gen page with project chrome
|
|
39
|
-
| Build a custom campaign/list workspace
|
|
40
|
-
| Add or change list-builder workflow buttons/forms
|
|
41
|
-
| Render artifacts or list-member detail
|
|
42
|
-
| Read or mutate lead-gen data inside a workflow
|
|
43
|
-
| Add a new persisted lead-gen field, artifact kind, or transition API
|
|
44
|
-
|
|
45
|
-
## Published Lead-Gen Surfaces
|
|
46
|
-
|
|
47
|
-
| Surface | Import from
|
|
48
|
-
| ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
49
|
-
| `leadGenManifest`, `LEAD_GEN_ITEMS`, `LeadGenSidebar`, `LeadGenSidebarTop`, `LeadGenSidebarMiddle` | `@elevasis/ui/features/lead-gen`
|
|
50
|
-
| `LeadGenOverviewPage`, `LeadGenListsPage`, `LeadGenListDetailPage`, `ListBuilderPage`, `LeadGenCompaniesPage`, `LeadGenContactsPage` | `@elevasis/ui/features/lead-gen`
|
|
51
|
-
| `useLeadGenConfig`, `LeadGenBuildConfig`, build-state helpers
|
|
52
|
-
| `ListActionsProvider`, `useListActions`, `ListBuilderWorkflow`, `ListBuilderRegistry`, `LeadGenActionKey` | `@elevasis/ui/features/lead-gen`
|
|
53
|
-
| `LeadGenRouteShell`
|
|
54
|
-
| `useLists`, `useList`, `useListsTelemetry`, `useListProgress`, `useListExecutions`, `useCreateList`, `useUpdateList`, `useUpdateListConfig`, `useDeleteList` | `@elevasis/ui/hooks`
|
|
55
|
-
| `useWorkflowExecution`, `useExecutionSSE`, `useAddCompaniesToList`, `useRemoveCompaniesFromList`, `useAddContactsToList` | `@elevasis/ui/hooks`
|
|
56
|
-
| `useCompanies`, `useCompany`, `useContacts`, `useContact` | `@elevasis/ui/hooks`
|
|
57
|
-
| `useArtifacts`, `useCreateArtifact`, `useListMembers`, `useListMember` | `@elevasis/ui/hooks`
|
|
58
|
-
| `useTransitionList`, `useTransitionListMember`, `useTransitionListCompany`, `useDeriveActions` | `@elevasis/ui/hooks`
|
|
59
|
-
| `ElevasisUIProvider`, `ElevasisCoreProvider`, `useElevasisServices` | `@elevasis/ui/provider`
|
|
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`
|
|
62
|
-
| `listBuilderWorkflow` | `@elevasis/sdk/worker`
|
|
63
|
-
| `acqDb`, `list` | `@elevasis/sdk/worker`
|
|
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
|
-
##
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
+
```
|