<!-- @generated by packages/sdk/scripts/copy-reference-docs.mjs -- DO NOT EDIT -->
<!-- Regenerate: pnpm scaffold:sync -->

﻿---
title: Build and Extend Lead Gen
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.
---

# Build and Extend Lead Gen

Use this recipe when a downstream project wants to build on the shared lead-gen system instead of forking it.

Good trigger phrases:

- "Add a lead-gen reports page."
- "Build a campaign creator surface."
- "Customize the lead-gen sidebar."
- "Show artifacts for a campaign."
- "Read or update lead-gen lists from a workflow."
- "Change the lead-gen lifecycle for this business."

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.

- **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.
- **Shared UI:** lead-gen overview, lists, list detail, companies, contacts, sidebars, drawer components, `useLeadGenConfig`, and list-builder state derivation live in `@elevasis/ui`.
- **Headless hooks:** lists, companies, contacts, artifacts, list members, transitions, and derived actions live under `@elevasis/ui/hooks`.
- **Acquisition substrate:** `acq_lists`, `acq_companies`, `acq_contacts`, `acq_artifacts`, list members, list companies, telemetry, and stateful transition shapes are exposed through generated contracts.
- **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.
- **Workflow adapters:** `acqDb`, `list`, and `listBuilderWorkflow` from `@elevasis/sdk/worker` let workflows read and mutate lead-gen data through platform tools.
- **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`.

## Decision Table

| User wants                                                                                              | Start here                                                                                                                                      | Notes                                                                                                           |
| ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| 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. |
| 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.                                           |
| 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.                               |
| 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.         |
| 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. |
| 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. |
| 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.         |
| 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.              |

## Published Lead-Gen Surfaces

| Surface                                                                                                                                                      | Import from                      | Use for                                                                                                                                                                   |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `leadGenManifest`, `LEAD_GEN_ITEMS`, `LeadGenSidebar`, `LeadGenSidebarTop`, `LeadGenSidebarMiddle`                                                           | `@elevasis/ui/features/lead-gen` | Feature registration and sidebar composition                                                                                                                              |
| `LeadGenOverviewPage`, `LeadGenListsPage`, `LeadGenListDetailPage`, `ListBuilderPage`, `LeadGenCompaniesPage`, `LeadGenContactsPage`                         | `@elevasis/ui/features/lead-gen` | Shared lead-gen pages you can route to or wrap                                                                                                                            |
| `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                                             |
| `ListActionsProvider`, `useListActions`, `ListBuilderWorkflow`, `ListBuilderRegistry`, `LeadGenActionKey`                                                    | `@elevasis/ui/features/lead-gen` | List Builder workflow registry, slot-based field contracts, and project-owned action wiring                                                                               |
| `LeadGenRouteShell`                                                                                                                                            | `@elevasis/ui/features/lead-gen` | Route shell helper (contact/company detail surfaces are now `ContactDetailPage` / `CompanyDetailPage` from `@elevasis/ui/features/crm`)                                  |
| `useLists`, `useList`, `useListsTelemetry`, `useListProgress`, `useListExecutions`, `useCreateList`, `useUpdateList`, `useUpdateListConfig`, `useDeleteList` | `@elevasis/ui/hooks`             | Headless list and telemetry data access                                                                                                                                   |
| `useWorkflowExecution`, `useExecutionSSE`, `useAddCompaniesToList`, `useRemoveCompaniesFromList`, `useAddContactsToList`                                     | `@elevasis/ui/hooks`             | List Builder workflow triggering, live execution tailing, and list membership mutations                                                                                   |
| `useCompanies`, `useCompany`, `useContacts`, `useContact`                                                                                                    | `@elevasis/ui/hooks`             | Acquisition company/contact data access                                                                                                                                   |
| `useArtifacts`, `useCreateArtifact`, `useListMembers`, `useListMember`                                                                                       | `@elevasis/ui/hooks`             | Lead-gen substrate data access                                                                                                                                            |
| `useTransitionList`, `useTransitionListMember`, `useTransitionListCompany`, `useDeriveActions`                                                               | `@elevasis/ui/hooks`             | Stateful transition mutations and contextual action derivation                                                                                                            |
| `ElevasisUIProvider`, `ElevasisCoreProvider`, `useElevasisServices`                                                                                          | `@elevasis/ui/provider`          | Provider setup, API access, organization context, and `listActions` registry injection                                                                                    |
| `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                                             |
| `getLeadGenStageCatalog`, `getAllBuildTemplates`, `getProspectingBuildTemplateOptions`, `createBuildPlanSnapshotFromTemplateId`                              | `@elevasis/core`                 | Generic Organization Model and template readers. Supply tenant templates/model values from the project.                                                                   |
| `listBuilderWorkflow`                                                                                                                                        | `@elevasis/sdk/worker`           | Generic list-builder workflow runtime for tenant-owned step handlers                                                                                                      |
| `acqDb`, `list`                                                                                                                                              | `@elevasis/sdk/worker`           | Workflow-side acquisition and list-scoped platform adapters                                                                                                               |

Read the generated contracts before changing typed boundaries:

`node_modules/@elevasis/sdk/reference/scaffold/reference/contracts.md`

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.

## 1. Extend Lead-Gen Navigation or Sidebar

For a simple nav addition, extend `LEAD_GEN_ITEMS` and override the lead-gen system manifest:

```tsx
// ui/src/routes/__root.tsx
import { leadGenManifest, LEAD_GEN_ITEMS, LeadGenSidebar, LeadGenSidebarMiddle } from '@elevasis/ui/features/lead-gen'
import type { SystemModule } from '@elevasis/ui/provider'
import type { NavItem } from '@elevasis/ui/layout'
import { IconChartBar } from '@tabler/icons-react'

const customLeadGenItems: NavItem[] = [
  ...LEAD_GEN_ITEMS,
  { label: 'Reports', to: '/lead-gen/reports', icon: IconChartBar, exact: false }
]

const CustomLeadGenSidebar = () => (
  <LeadGenSidebar>
    <LeadGenSidebarMiddle items={customLeadGenItems} />
  </LeadGenSidebar>
)

export const customLeadGenManifest: SystemModule = {
  ...leadGenManifest,
  sidebar: CustomLeadGenSidebar
}
```

Then replace `leadGenManifest` with `customLeadGenManifest` in the local `SYSTEM_MANIFESTS` array and add the matching route under `ui/src/routes/lead-gen/`.

For structural changes, compose `LeadGenSidebarTop`, `SubshellNavList`, and `SubshellSidebarSection`. The full sidebar decision tree lives in `node_modules/@elevasis/sdk/reference/scaffold/ui/customization.md`.

## 2. Wrap Shared Lead-Gen Pages

If the shared page is close, keep it and wrap it:

```tsx
// ui/src/routes/lead-gen/lists.index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { LeadGenListsPage } from '@elevasis/ui/features/lead-gen'
import { Stack } from '@mantine/core'
import { ProjectAnnouncementBanner } from '@/lib/components/ProjectAnnouncementBanner'

export const Route = createFileRoute('/lead-gen/lists/')({
  component: ListsRoute
})

function ListsRoute() {
  return (
    <Stack gap={0}>
      <ProjectAnnouncementBanner context="lead-gen-lists" />
      <LeadGenListsPage />
    </Stack>
  )
}
```

For list detail routes, pass the route param into `LeadGenListDetailPage`:

```tsx
import { createFileRoute } from '@tanstack/react-router'
import { LeadGenListDetailPage } from '@elevasis/ui/features/lead-gen'

export const Route = createFileRoute('/lead-gen/lists/$listId')({
  component: ListDetailRoute
})

function ListDetailRoute() {
  const { listId } = Route.useParams()
  return <LeadGenListDetailPage listId={listId} />
}
```

Use the same wrapping pattern for `LeadGenOverviewPage`, `LeadGenCompaniesPage`, and `LeadGenContactsPage`.

For the real-time List Builder workspace, add a thin route that passes the route param into `ListBuilderPage`:

```tsx
import { createFileRoute } from '@tanstack/react-router'
import { ListBuilderPage } from '@elevasis/ui/features/lead-gen'

export const Route = createFileRoute('/lead-gen/list-builder/$listId')({
  component: ListBuilderRoute
})

function ListBuilderRoute() {
  const { listId } = Route.useParams()
  return <ListBuilderPage listId={listId} />
}
```

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.

The shared pages call `useLeadGenConfig()` internally. That hook reads the provider-injected `organizationModel` and derives:

- `stageCatalog` from `getLeadGenStageCatalog(organizationModel)`
- `buildTemplates` and `defaultBuildSteps` from `getAllBuildTemplates(organizationModel)`
- `defaultBuildTemplateId` from the `sales.lead-gen` system config or first available template
- `exportWorkflowId` from the lead-gen workflow resource whose action matches export semantics
- `canUseLeadGenApiActions` from derived `sales.lead-gen.apiInterface` readiness

If the provider has no Organization Model, shared UI renders with empty config instead of falling back to Elevasis-owned defaults. Read-only shells can render without API readiness, but shared API-backed actions such as list creation, list deletion, data-mode updates, build runs, exports, and member transitions stay disabled until `sales.lead-gen.apiInterface` is active and ready. A tenant that wants list creation and the build runner to be useful must provide lead-gen catalogs/templates/actions/resources/topology in `core/config/organization-model.ts`; readiness is derived from those facts rather than a hand-authored requirements list.

In external projects, pass the local list-action registry through the app factory and the tenant Organization Model through the authenticated shell:

```tsx
// ui/src/main.tsx
import { createElevasisApp } from '@elevasis/ui/app'
import { listActions } from '@/config/listActions'

const App = createElevasisApp({
  listActions,
  // other app config...
})
```

```tsx
// ui/src/routes/__root.tsx
import { ElevasisAuthenticatedShell } from '@elevasis/ui/app'
import { canonicalOrganizationModel } from '@core/config/organization-model'

export function RootLayout() {
  return (
    <ElevasisAuthenticatedShell
      systems={SYSTEM_MANIFESTS}
      organizationModel={canonicalOrganizationModel}
      // other shell config...
    />
  )
}
```

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.

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.

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.

```tsx
import * as z from 'zod'
import { leadGenManifest, type ListBuilderRegistry } from '@elevasis/ui/features/lead-gen'
import type { StepConfigLayout } from '@elevasis/ui/components/forms'
import { ElevasisSystemsProvider, ElevasisUIProvider } from '@elevasis/ui/provider'
import { organizationModel, resourceDescriptors } from '@core/config/organization-model'

const companyCleanupInputSchema = z.object({
  listId: z.string().uuid(),
  targetDescription: z.string(),
  dryRun: z.boolean().default(true),
  batchSize: z.number().int().min(1).default(20)
})

type CompanyCleanupInput = z.infer<typeof companyCleanupInputSchema>
const companyCleanupResource = resourceDescriptors.companyCleanup

const companyCleanupLayout: StepConfigLayout<CompanyCleanupInput> = {
  sections: [
    {
      id: 'configuration',
      fields: [
        {
          path: 'targetDescription',
          component: 'textarea',
          label: 'Target description',
          placeholder: 'independent veterinary clinics in Orange County'
        },
        { path: 'dryRun', component: 'switch', label: 'Dry run' }
      ]
    }
  ],
  advanced: {
    id: 'advanced',
    fields: [{ path: 'batchSize', component: 'numberinput', label: 'Batch size', min: 1 }]
  }
}

const listActions: ListBuilderRegistry = [
  {
    resourceId: companyCleanupResource.id,
    workflowId: companyCleanupResource.id,
    actionKey: companyCleanupResource.ontology.primaryAction ?? companyCleanupResource.ontology.actions[0],
    label: companyCleanupResource.title,
    description: companyCleanupResource.description,
    category: 'utility',
    stagesAffected: ['populated'],
    schema: companyCleanupInputSchema,
    layout: companyCleanupLayout,
    defaultInput: (list) => ({
      listId: list.id,
      targetDescription: '',
      dryRun: true,
      batchSize: 20
    })
  }
]

const SYSTEM_MANIFESTS = [
  leadGenManifest,
  // Add any other shared/local system manifests here.
]

export function App() {
  return (
    <ElevasisUIProvider listActions={listActions} {...providerProps}>
      <ElevasisSystemsProvider systems={SYSTEM_MANIFESTS} organizationModel={organizationModel}>
        {children}
      </ElevasisSystemsProvider>
    </ElevasisUIProvider>
  )
}
```

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.

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`.

Per-section split rule (operator-grade vs power-user):

- **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.
- **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.
- **`defaultInput`:** seeds the form before the user touches anything; must include `listId: list.id`.

## 3. Build a Custom Campaign Workspace

When the project needs custom layout or vertical-specific rendering, use the hooks directly:

```tsx
import { Badge, Button, Card, Group, Stack, Text } from '@mantine/core'
import { useArtifacts, useList, useListMembers, useTransitionListMember } from '@elevasis/ui/hooks'

export function CampaignWorkspace({ listId }: { listId: string }) {
  const listQuery = useList(listId)
  const membersQuery = useListMembers({ listId })
  const artifactsQuery = useArtifacts({ ownerKind: 'list', ownerId: listId })
  const transitionMember = useTransitionListMember()

  const list = listQuery.data
  if (!list) return null

  const firstMember = membersQuery.data?.members[0]

  return (
    <Stack>
      <Group justify="space-between">
        <div>
          <Text fw={700}>{list.name}</Text>
          <Text size="sm" c="dimmed">{list.description}</Text>
        </div>
        <Badge>{list.stateKey}</Badge>
      </Group>

      <Card withBorder>
        <Text size="sm">Artifacts: {artifactsQuery.data?.artifacts.length ?? 0}</Text>
      </Card>

      {firstMember ? (
        <Button
          onClick={() =>
            transitionMember.mutate({
              memberId: firstMember.id,
              listId,
              pipelineKey: 'lead-gen',
              stageKey: 'prospecting',
              stateKey: 'verified'
            })
          }
        >
          Mark First Member Verified
        </Button>
      ) : null}
    </Stack>
  )
}
```

Use `useArtifacts({ ownerKind, ownerId })` for durable JSON artifacts like audits, research summaries, snapshots, exports, or model outputs.

## 4. Read and Mutate Lead-Gen Data in Workflows

Inside deployed workflows, use worker adapters instead of browser hooks or direct database access:

External projects should define workflow input/output schemas in `@shared/types`; the example below assumes those shared schemas already exist.

```ts
// operations/src/sales/qualify-list.ts
import type { WorkflowDefinition } from '@elevasis/sdk'
import { acqDb, list } from '@elevasis/sdk/worker'
import { qualifyListInputSchema, qualifyListOutputSchema } from '@shared/types'
import { resourceDescriptors } from '@core/config/organization-model'

export const qualifyListWorkflow: WorkflowDefinition = {
  config: {
    resource: resourceDescriptors.qualifyList,
    resourceId: resourceDescriptors.qualifyList.id,
    name: 'Qualify List',
    type: resourceDescriptors.qualifyList.kind,
    version: '1.0.0',
    status: 'dev',
  },
  contract: { inputSchema: qualifyListInputSchema, outputSchema: qualifyListOutputSchema },
  steps: {
    qualify: {
      id: 'qualify',
      name: 'Qualify',
      inputSchema: qualifyListInputSchema,
      outputSchema: qualifyListOutputSchema,
      next: null,
      handler: async (input, context) => {
        const config = await list.getConfig({ listId: input.listId })
        const contacts = await acqDb.listContacts({ listId: input.listId, limit: 100, offset: 0 })

        for (const contact of contacts.data) {
          await list.updateContactStage({
            listId: input.listId,
            contactId: contact.id,
            stage: 'verified',
            executionId: context.executionId
          })
        }

        await list.recordExecution({
          listId: input.listId,
          executionId: context.executionId,
          configSnapshot: config
        })

        return { touched: contacts.data.length }
      }
    }
  },
  entryPoint: 'qualify'
}
```

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.

Keep these boundaries straight:

- Browser/UI code uses `@elevasis/ui/hooks`.
- Workflow code uses `@elevasis/sdk/worker` adapters.
- `organizationId` is injected by the platform dispatcher. Do not pass it from workflow code.
- Persisted API shapes are owned by core/API contracts, not route-local TypeScript.

## 5. Customize Lead-Gen Semantics

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`.

For feature toggles, labels, lifecycle semantics, stage catalogs, build templates, actions, resource descriptors, or runtime topology, update the project model:

`core/config/organization-model.ts`

Tenant-owned list-builder values belong in the main Organization Model:

- stage vocabulary in lead-gen ontology catalogs
- build templates and template-step catalogs in lead-gen ontology catalogs
- action ids in the tenant action layer
- workflow resources in the tenant resources map with ontology bindings and `primaryAction`
- topology relationships in the tenant topology map
- `sales.lead-gen.apiInterface` with `lifecycle: 'active'`, `readinessProfile: 'sales.lead-gen.api'`, and the scoped API resource ids when API-backed lead-gen actions are ready
- 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`

Use `core/config/extensions/*` for entity schema variants, not for list-builder stage catalogs or workflow bindings.

The platform-side lead-gen stateful vocabulary is defined for:

- `acq.list`
- `acq.list-member`
- `acq.list-company`

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.

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.

## Verify

Run the checks for the surfaces you touched:

```bash
pnpm -C ui run check
pnpm -C operations run check
pnpm -C operations exec elevasis-sdk check
```

If you changed platform-level lead-gen contracts in the monorepo, the platform maintainer must also regenerate and verify scaffold output:

```bash
pnpm scaffold:sync
pnpm scaffold:verify
```
