@elevasis/sdk 1.12.0 → 1.13.1

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.
@@ -119,7 +119,7 @@ The API URL is centralized in `ui/src/lib/constants/api.ts`. In the current temp
119
119
  Current top-level app sections:
120
120
 
121
121
  - `/` -- host-local dashboard entrypoint with quick links derived from `organizationModel.navigation.quickAccessSurfaceIds`
122
- - `/lead-gen/*` -- lead generation pages (`lists`, `companies`, `contacts`, `deliverability`)
122
+ - `/lead-gen/*` -- lead generation pages (`lists`, `companies`, `contacts`)
123
123
  - `/crm/*` -- CRM overview, pipeline, and deals
124
124
  - `/projects/*` -- delivery feature pages (projects, milestones, tasks, notes)
125
125
  - `/operations/*` -- operations overview, resources, command queue, command view, sessions, task scheduler
@@ -193,10 +193,12 @@ Two customization layers are available for every shared feature sidebar:
193
193
 
194
194
  `manifest.sidebar` accepts any component, so arbitrary customization is always available. The `items` prop is an abstraction barrier for the common case, not a limit.
195
195
 
196
- See `node_modules/@elevasis/sdk/reference/scaffold/ui/customization.md` for the decision tree, page-wrapping pattern, and delivery's three-section variant. See `node_modules/@elevasis/sdk/reference/scaffold/reference/contracts.md` for `NavItem`, `FeatureModule`, and related TypeScript shapes.
197
-
198
- ## Notes
199
-
200
- - `ui/src/routeTree.gen.ts` is generated by TanStack Router tooling. Do not hand-edit it.
196
+ See `node_modules/@elevasis/sdk/reference/scaffold/ui/customization.md` for the decision tree, page-wrapping pattern, and delivery's three-section variant. For broader CRM extension work across pages, hooks, actions, workflows, and org-model boundaries, start with `node_modules/@elevasis/sdk/reference/scaffold/recipes/extend-crm.md`. For broader lead-gen extension work across pages, hooks, list/member state, artifacts, touchpoints, workflows, and org-model boundaries, start with `node_modules/@elevasis/sdk/reference/scaffold/recipes/extend-lead-gen.md`. See `node_modules/@elevasis/sdk/reference/scaffold/reference/contracts.md` for `NavItem`, `FeatureModule`, CRM platform primitives, Lead Gen platform primitives, and related TypeScript shapes.
197
+
198
+ For CRM deal action buttons, read `node_modules/@elevasis/sdk/reference/scaffold/recipes/customize-crm-actions.md` before changing `crmActions`, `DealDetailPage`, `DealDrawer`, or custom workflow buttons. Start with the shared `crmActions` provider path for action visibility, labels, ordering, and render-time configuration. In v1, platform-known/default action endpoint behavior is server-constrained; use project-owned UI that calls the workflow directly when a custom key sits outside that server-dispatched set.
199
+
200
+ ## Notes
201
+
202
+ - `ui/src/routeTree.gen.ts` is generated by TanStack Router tooling. Do not hand-edit it.
201
203
  - The template ships a broad route surface so downstream projects can trim or reshape features without having to re-derive the shared shell contract from scratch.
202
204
  - For package-export discovery, glob `node_modules/@elevasis/sdk/reference/` or `node_modules/@repo/ui/dist/` for the current SDK/UI package surface.
@@ -119,11 +119,17 @@ This routing applies to both codify levels (decision #21 -- Codify ceremony dele
119
119
  - **Level A** (config-only edits to `organization-model.ts`, feature toggles, label renames): delegate to `/configure <domain>` immediately.
120
120
  - **Level B** (new Zod extension files in `foundations/config/extensions/`): also delegate to `/configure <domain>`; `/configure` gates Level B to explicit user asks before scaffolding a new TS file.
121
121
 
122
- Vibe detects the intent and delegates in both cases. It does not run either pipeline itself.
123
-
124
- Heuristics for when to propose codification (passed to `/configure` as context):
125
-
126
- - First mention of a new attribute: note to `resume_context`, do not propose yet
122
+ Vibe detects the intent and delegates in both cases. It does not run either pipeline itself.
123
+
124
+ For "build/extend the CRM" asks, classify the structural org-model portion as Codify, then read `node_modules/@elevasis/sdk/reference/scaffold/recipes/extend-crm.md` before editing. CRM work often spans org-model sales semantics, shared UI routes, hooks, workflow adapters, and deal actions; do not reduce it to only `sales` config or only UI.
125
+
126
+ For "build/extend lead gen" / "campaign creator" / "outbound list state" asks, classify the structural org-model portion as Codify, then read `node_modules/@elevasis/sdk/reference/scaffold/recipes/extend-lead-gen.md` before editing. Lead-gen work often spans org-model prospecting semantics, shared UI routes, hooks, list/member state, artifacts, touchpoints, and workflow adapters; do not reduce it to only `prospecting` config or only UI.
127
+
128
+ For "add a custom CRM action" / "Send Quote button" asks, classify as Codify, then read `node_modules/@elevasis/sdk/reference/scaffold/recipes/customize-crm-actions.md` before editing. Start with the shared `crmActions` provider path for action visibility, labels, ordering, and render-time configuration. In v1, platform-known/default action endpoint behavior is server-constrained; use project-owned UI that calls the workflow directly when a custom key sits outside that server-dispatched set.
129
+
130
+ Heuristics for when to propose codification (passed to `/configure` as context):
131
+
132
+ - First mention of a new attribute: note to `resume_context`, do not propose yet
127
133
  - Second mention OR explicit declaration ("we're ecom"): propose extension
128
134
  - Explicit ask ("track ecom deals separately"): propose immediately with fuller scope
129
135
  - Attribute appearing across 3+ tasks: propose adding field to existing extension
@@ -36,11 +36,7 @@ Operations-only projects with no CRM surface and no `acq_deals` table can ignore
36
36
  pnpm up @elevasis/core @elevasis/ui @elevasis/sdk --latest
37
37
  ```
38
38
 
39
- 3. If your project has a project-local `acq_deals` table, run the matching atomic cutover migration. The reference SQL lives in the monorepo at `apps/docs/content/docs/in-progress/active-development/sdk-changes/org-model/_sql/`:
40
- - `00_preflight_org_model_cutover.sql`
41
- - `01_cleanup_known_cutover_outliers.sql`
42
- - `02_acq_deals_atomic_cutover.sql`
43
- - `03_postflight_org_model_cutover.sql`
39
+ 3. If your project has a project-local `acq_deals` table, run a matching atomic cutover migration. The reference SQL that drove the monorepo cutover (preflight, outlier cleanup, atomic cutover, postflight) is no longer checked into the repo -- recover it from git history under `apps/docs/content/docs/in-progress/active-development/sdk-changes/org-model/_sql/` (deleted 2026-04-27 alongside the completed task doc) if you need a starting point.
44
40
 
45
41
  The cutover is single-shot, not dual-write. Backfill `pipeline_key` / `stage_key` from `cached_stage` and drop the legacy columns in the same transaction. There is no mid-flight rollback path; fix-forward only. Confirm prod deal volume is small enough to accept that tradeoff before applying.
46
42
 
@@ -0,0 +1,110 @@
1
+ # Lead-Gen Substrate Train (Tracks A + B + C)
2
+
3
+ ## Why this note exists
4
+
5
+ This release train ships the Lead-Gen Substrate Generalization across three coordinated tracks:
6
+
7
+ - **Track A — Additive Primitives.** New `acq_artifacts` (versioned content: audit PDFs, email drafts, etc.) and `acq_touchpoints` (append-only outreach event log) tables, plus ICP rubric columns (`qualification_score numeric`, `qualification_signals jsonb`, `qualification_rubric_key text`) on `acq_companies` and `acq_contacts`. Purely additive schema — no column drops on these entities.
8
+ - **Track B — Stateful Trait Generalization.** The `(pipeline_key, stage_key, state_key, activity_log)` quartet from the CRM HITL deal cutover is now applied to `acq_lists`, `acq_list_members`, and `acq_list_companies`. Legacy `acq_lists.status`, `acq_list_*.stage`, and `acq_list_*.stage_updated_at` columns are dropped. `acq_companies` and `acq_contacts` are explicitly **excluded** (batch-ETL pattern, not state-machine).
9
+ - **Track C — Campaign Creator UI.** `@elevasis/ui` ships a hooks layer (`useArtifacts`, `useCreateArtifact`, `useTouchpoints`, `useListMembers`, `useListMember`, `useTransitionList`, `useTransitionListMember`, `useTransitionListCompany`, `useDeriveActions`) plus upgraded `LeadGenListsPage` and `LeadGenListDetailPage` (5-tab layout: Overview / Members / Activity / Artifacts / Touchpoints) and a `ListMemberDrawer` deep-linkable via `?member=<id>&memberKind=<contact|company>`.
10
+
11
+ Concretely, this train introduces:
12
+
13
+ - a `Stateful` interface + `StatefulSchema` Zod object + generic `TransitionItem<T,TEvent>` / `DeriveActions<T,TAction>` type aliases at `@elevasis/core/business/acquisition/stateful`
14
+ - three `StatefulPipelineDefinition` constants on `@elevasis/core/organization-model` — `ACQ_LISTS_LEAD_GEN_PIPELINE` (single stage `lifecycle` with five states: draft / enriching / launched / closing / archived), `ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE`, `ACQ_LIST_COMPANIES_LEAD_GEN_PIPELINE` — plus `findPipeline()` helper and `LEAD_GEN_PIPELINE_DEFINITIONS` map
15
+ - API routes `GET/POST /acquisition/artifacts`, `GET /acquisition/touchpoints`, `GET /acquisition/lists/:listId/members`, `GET /acquisition/list-members/:memberId`, `PATCH /acquisition/lists/:listId/transition`, `PATCH /acquisition/list-members/:memberId/transition`, `PATCH /acquisition/list-companies/:listCompanyId/transition`
16
+ - new flat columns on `acq_artifacts` / `acq_touchpoints` / `acq_lists` / `acq_list_members` / `acq_list_companies` and ICP rubric columns on `acq_companies` / `acq_contacts`
17
+
18
+ ## Applies to
19
+
20
+ All template-derived projects that:
21
+
22
+ - consume `@elevasis/core/business/acquisition` types or import the `Stateful` trait / `LEAD_GEN_PIPELINE_DEFINITIONS`
23
+ - render the lead-gen lists / list-detail / Campaign Creator surfaces from `@elevasis/ui/features/lead-gen`
24
+ - have a project-local `acq_*` schema that mirrors the platform tables (artifacts, touchpoints, lists, list-members, list-companies)
25
+ - author workflows under `operations/src/sales/{outreach,prospecting,qualification}/**` that read or write the `acq_lists` / `acq_list_members` / `acq_list_companies` lifecycle columns, or that produce email drafts / audit content / outreach events
26
+ - read `acq_companies.qualification_score` / `qualification_signals` / `qualification_rubric_key`, or the parallel columns on `acq_contacts`
27
+
28
+ Operations-only projects with no lead-gen surface and no `acq_*` tables can ignore this train.
29
+
30
+ ## Required actions
31
+
32
+ 1. Pull template changes with `/git-sync` so the refreshed lead-gen hook surface, `LeadGenListsPage` upgrades, and `ListMemberDrawer` deep-link wiring are available.
33
+
34
+ 2. After the release train is published, update package versions in the project:
35
+
36
+ ```bash
37
+ pnpm up @elevasis/core @elevasis/ui @elevasis/sdk --latest
38
+ ```
39
+
40
+ 3. If your project has project-local `acq_*` tables, run additive + cutover migrations:
41
+ - **Additive (Track A — safe to apply standalone):** `acq_artifacts` and `acq_touchpoints` table creation (canonical 3-policy RLS — `Platform admins have full access to <table>` / `Org members with acquisition.manage can manage <table>` / `Org members can view <table>`). ICP columns added to `acq_companies` and `acq_contacts` (`qualification_score numeric`, `qualification_signals jsonb`, `qualification_rubric_key text`, all nullable).
42
+ - **Stateful trait (Track B — atomic cutover, not dual-write):** add `pipeline_key text NOT NULL`, `stage_key text NOT NULL`, `state_key text NOT NULL`, `activity_log jsonb NOT NULL DEFAULT '[]'` to `acq_lists` / `acq_list_members` / `acq_list_companies`; drop `acq_lists.status`, `acq_list_members.{stage,stage_updated_at}`, `acq_list_companies.{stage,stage_updated_at}` in the same transaction.
43
+ - **Backfill values used in the platform cutover:** `acq_lists` -> `pipeline_key='lead-gen'`, `stage_key='lifecycle'`, `state_key=coalesce(status,'draft')`. `acq_list_members` and `acq_list_companies` -> `pipeline_key='lead-gen'`, `stage_key='outreach'`, `state_key=coalesce(stage,'pending')` (preserves `personalized` / `verified` / `discovered` / `qualified` / `populated` / `extracted` vocabularies losslessly).
44
+ - The cutover is single-shot. Confirm prod row counts on these tables are small enough to accept fix-forward semantics before applying.
45
+
46
+ 4. Replace any direct lifecycle writes on lists / list-members / list-companies with the new transition surface:
47
+ - workflow code that previously wrote `acq_lists.status`, `acq_list_members.stage`, or `acq_list_companies.stage` directly must now route through `acqDb.transitionList(...)` / `transitionListMember(...)` / `transitionListCompany(...)`
48
+ - the API server injects `organizationId` from execution context — never pass `organizationId` from worker payload
49
+ - the platform-side methods append `ActivityEventSchema`-shaped events to `activity_log`; do NOT also write activity rows from the workflow side
50
+
51
+ 5. Replace `enrichmentData.pipeline.{auditDoc, touchpointCount, lastReply}` writers with `acq_artifacts` / `acq_touchpoints` writes:
52
+ - audit PDFs / email drafts / proposals -> `acq_artifacts(kind='audit'|'email_draft'|'proposal', owner_kind='company'|'contact'|'list'|'list_member', version=N, content=<jsonb>, source_execution_id=<execution>)`
53
+ - outreach events -> `acq_touchpoints(direction='outbound'|'inbound', channel='email'|'linkedin'|'sms', kind='initial'|'followup'|'reply'|'nudge', occurred_at=<ts>, payload=<jsonb>, artifact_id=<linked draft>, source_execution_id=<execution>)`
54
+ - `pipeline_status.pipelineStatus` text enum on `acq_companies` is **untouched** (out of trait scope per Decision B1) — that JSONB key remains canonical for company-level state
55
+
56
+ 6. Adopt the qualification rubric columns:
57
+ - LLM qualifiers should write `acq_companies.qualification_score` / `qualification_signals` (jsonb shape: `{ status: 'complete' | 'pending' | ..., result?: ..., reason?: ..., completedAt?: ... }`) and stop writing `pipeline_status.qualification`
58
+ - readers (e.g. `fetch-companies` filters in email-discovery / company-qualification workflows) should use `qualificationSignals?.status === 'complete'` instead of `pipeline_status.qualification?.status`
59
+ - `qualification_rubric_key` is a free-form text key today; an Org OS rubric registry may land in a later train without a schema change
60
+
61
+ 7. If you read `activity_log` rows on lists / list-members / list-companies and pattern-match by `type`, mirror the flat schema already used for `acq_deals`:
62
+
63
+ ```ts
64
+ { type: 'state_change', timestamp, stateBefore, stateAfter, reason? }
65
+ { type: 'stage_change', timestamp, stageBefore, stageAfter, reason? }
66
+ { type: 'task_created', timestamp, taskId }
67
+ ```
68
+
69
+ 8. If you ship lead-gen UI from `@elevasis/ui/features/lead-gen`, the `lists.tsx` and `list-detail.tsx` pages now expect:
70
+ - `state_key` badge column resolved via `findPipeline('acq.list')` from `LEAD_GEN_PIPELINE_DEFINITIONS`
71
+ - ICP rubric column reading `list.config.qualification.qualificationRubricKey` (em-dash when absent)
72
+ - `stateKeyFilter` Select chip in the FilterBar (options sourced from `ACQ_LISTS_LEAD_GEN_PIPELINE.stages[0].states`)
73
+ - 5-tab detail layout (Overview / Members / Activity / Artifacts / Touchpoints) plus action toolbar fed by `useDeriveActions(list)`
74
+ - Row-click navigation on the Members tab via `window.history.pushState`, with `useSearch({ strict: false })` reading `?member=<id>&memberKind=<contact|company>` to drive `ListMemberDrawer`
75
+ - For the `?member=` deep-link to be typed, add `validateSearch` to the project's `routes/lead-gen/lists.$listId.tsx` route file (optional; the drawer reads search params untyped via `strict: false`)
76
+
77
+ 9. Redeploy `operations` after the package upgrades so workers pick up the new typed adapter surface, the artifact/touchpoint writers, and the qualification column writes:
78
+
79
+ ```bash
80
+ pnpm -C operations exec elevasis-sdk deploy --prod
81
+ ```
82
+
83
+ ## Verification
84
+
85
+ Run from the project root after package updates and any DB migration:
86
+
87
+ ```bash
88
+ pnpm -C ui check-types
89
+ pnpm -C ui build
90
+ pnpm -C operations check-types
91
+ pnpm -C operations exec elevasis-sdk check
92
+ ```
93
+
94
+ Then exercise the lead-gen surface:
95
+
96
+ - open the lists page — confirm the new state-key badge column renders, the ICP rubric column shows either the rubric key or an em-dash, and the FilterBar exposes a state-key Select chip
97
+ - open a list detail — confirm the 5-tab layout (Overview / Members / Activity / Artifacts / Touchpoints), the state-machine progression badge in the Overview tab, and that the Action toolbar in `PageTitleCaption` renders the actions returned by `useDeriveActions(list)`
98
+ - click a member row — confirm the drawer opens, the URL gains `?member=<id>&memberKind=<contact|company>`, the activity / touchpoints / artifacts tabs render, and `onClose` clears both params
99
+ - after the operations redeploy, run a campaign through the qualification + email-discovery workflows; confirm new rows in `acq_artifacts`, new rows in `acq_touchpoints`, and non-null `qualification_score` / `qualification_signals` on the affected `acq_companies` rows
100
+
101
+ If a workflow that historically wrote `acq_lists.status` / `acq_list_*.stage` throws Zod errors against `StatefulSchema` or fails on missing `state_key`, route it through `acqDb.transitionList(...)` / `transitionListMember(...)` / `transitionListCompany(...)`.
102
+
103
+ ## Not handled by /git-sync
104
+
105
+ - `/git-sync` does not publish or upgrade `@elevasis/core`, `@elevasis/ui`, or `@elevasis/sdk`.
106
+ - `/git-sync` does not run the project-local `acq_artifacts` / `acq_touchpoints` table creation, ICP-column additions, or the Stateful-trait cutover on `acq_lists` / `acq_list_members` / `acq_list_companies`. Each of those is owner-authorized and atomic.
107
+ - `/git-sync` does not redeploy the project's `operations` bundle to dev or prod.
108
+ - `/git-sync` does not rewrite project-owned workflow code that bypasses `transitionList` / `transitionListMember` / `transitionListCompany` to write list / member / company stage directly.
109
+ - `/git-sync` does not retire project-local `enrichmentData.pipeline.{auditDoc, touchpointCount, lastReply}` writers — those must be migrated to `acq_artifacts` + `acq_touchpoints` by the project owner.
110
+ - `/git-sync` does not migrate workflow qualifiers off `pipeline_status.qualification` onto `qualification_score` / `qualification_signals` / `qualification_rubric_key`.
@@ -3,3 +3,5 @@
3
3
  Published test helpers for consumers that test Elevasis UI composition.
4
4
 
5
5
  Use `@elevasis/ui/test-utils` for provider-aware React rendering, WorkOS auth mocks, MSW handlers, and test query clients. Use `@elevasis/ui/test-utils/setup` or `@elevasis/ui/test-utils/setup-integration` from Vitest `setupFiles` when a consumer wants the shared browser and auth mocks.
6
+
7
+ `renderWithProviders`, `renderHookWithProviders`, and `createTestWrapper` include `ApiClientProvider`, `ElevasisServiceProvider`, `QueryClientProvider`, and `MantineProvider` by default. Tests can override `queryClient`, `getAccessToken`, `organizationId`, `isOrganizationReady`, `apiRequest`, and `isServiceReady` per render.
@@ -21,11 +21,14 @@ This scaffold reference contains universal documentation that applies to all Ele
21
21
  ### Pathway Recipes
22
22
 
23
23
  - [Add a Feature](./recipes/add-a-feature.md) -- end-to-end from org model key through manifest, routes, gating
24
- - [Add a Resource](./recipes/add-a-resource.md) -- author and deploy a workflow or agent
25
- - [Extend a Base Entity](./recipes/extend-a-base-entity.md) -- add project-specific metadata to canonical entity shapes via the TMeta slot
26
- - [Gate by Feature or Admin](./recipes/gate-by-feature-or-admin.md) -- decision table for access control patterns
27
-
28
- ### UI Patterns
24
+ - [Add a Resource](./recipes/add-a-resource.md) -- author and deploy a workflow or agent
25
+ - [Extend a Base Entity](./recipes/extend-a-base-entity.md) -- add project-specific metadata to canonical entity shapes via the TMeta slot
26
+ - [Gate by Feature or Admin](./recipes/gate-by-feature-or-admin.md) -- decision table for access control patterns
27
+ - [Build and Extend CRM](./recipes/extend-crm.md) -- map CRM UI pages, hooks, actions, workflow adapters, contracts, and org-model boundaries
28
+ - [Build and Extend Lead Gen](./recipes/extend-lead-gen.md) -- map lead-gen UI pages, hooks, list/member state, artifacts, touchpoints, workflow adapters, contracts, and org-model boundaries
29
+ - [Customize CRM Actions](./recipes/customize-crm-actions.md) -- add, hide, or replace CRM deal actions, use the shared `crmActions` provider path, and understand workflow dispatch constraints
30
+
31
+ ### UI Patterns
29
32
 
30
33
  - [UI Recipes](./ui/recipes.md) -- copy-paste recipes for pages, nav items, components, theme tokens
31
34
  - [Feature Flags & Gating](./ui/feature-flags-and-gating.md) -- three-concept model for feature access
@@ -0,0 +1,411 @@
1
+ ---
2
+ title: Customize CRM Actions
3
+ description: Add, hide, or replace CRM deal action buttons in a template-derived project, and override default platform action workflows with project-owned implementations.
4
+ ---
5
+ <!-- @generated by packages/sdk/scripts/copy-reference-docs.mjs -- DO NOT EDIT -->
6
+ <!-- Regenerate: pnpm scaffold:sync -->
7
+
8
+
9
+ # Customize CRM Actions
10
+
11
+ CRM deal pages derive their action buttons from an `ActionDef[]` array. The shared UI reads that array from the provider, filters it with `deriveActions(deal, actions)`, and renders one-click buttons for actions without a `payloadSchema`.
12
+
13
+ For the broader CRM extension map -- pages, sidebars, hooks, workflow adapters, contracts, and org-model boundaries -- start with [Build and Extend CRM](extend-crm.md). This recipe is only the deal-action path.
14
+
15
+ **Shape reference:** The `ActionDef` flat shape and the consolidation that replaced the old `handler`/`kind` union are documented in `apps/docs/content/docs/in-progress/active-development/sdk-changes/crm/crm-current-state-assessment.mdx`.
16
+
17
+ Use this recipe when a user asks for work like:
18
+
19
+ - "Hide Close Lost from the deal drawer."
20
+ - "Add a Send Quote action to deals in Proposal."
21
+ - "Override Move to Proposal to also create a task in our project tool."
22
+ - "Build a custom deal page that runs one of our workflows."
23
+
24
+ ## How Platform Action Dispatch Works
25
+
26
+ Every default action in `DEFAULT_CRM_ACTIONS` maps to a deployed workflow via its `workflowId` field. When the shared UI calls `POST /deals/:dealId/actions/:actionKey`, the platform:
27
+
28
+ 1. Validates `isAvailableFor(deal)` server-side (security gate -- cannot be skipped).
29
+ 2. Resolves `actionDef.workflowId` to a deployed resource.
30
+ 3. Dispatches through `SingleExecutionCoordinator` -- same execution engine as every other workflow.
31
+ 4. Returns the refetched deal.
32
+
33
+ External projects override an action's behavior by deploying a workflow with the same `workflowId` as the default. The resource registry picks the project-owned workflow over the platform default. The `isAvailableFor` predicate is not overridable in V1 -- it stays in `@core` `DEFAULT_CRM_ACTIONS`. Override what the action does, not when the button shows.
34
+
35
+ ## ActionDef Shape
36
+
37
+ Read the generated contract reference before editing:
38
+
39
+ `node_modules/@elevasis/sdk/reference/scaffold/reference/contracts.md`
40
+
41
+ The current flat shape (no `handler` nesting, no `kind` discriminator):
42
+
43
+ ```ts
44
+ export interface ActionDef {
45
+ key: string
46
+ label: string
47
+ isAvailableFor: (deal: AcqDealRow) => boolean
48
+ workflowId: string
49
+ payloadSchema?: z.ZodTypeAny
50
+ }
51
+ ```
52
+
53
+ `deriveActions(deal, actions)` returns only render-time actions: `{ key, label, payloadSchema? }`. It does not expose `workflowId` or `isAvailableFor` to the browser.
54
+
55
+ A minimal entry looks like:
56
+
57
+ ```ts
58
+ {
59
+ key: 'move_to_proposal',
60
+ label: 'Move to Proposal',
61
+ isAvailableFor: (deal) => deal.stage_key === 'interested',
62
+ workflowId: 'move_to_proposal-workflow',
63
+ payloadSchema: undefined
64
+ }
65
+ ```
66
+
67
+ ## acqDb and crm Helpers
68
+
69
+ Workflows backing CRM actions use worker adapters instead of raw Supabase calls. The acquisition adapter is imported as `acqDb` from `@elevasis/sdk/worker` and exposes the action-workflow helpers:
70
+
71
+ - `acqDb.transitionDeal({ dealId, toStage, toState? })` -- moves a deal to a new stage, optionally updating state_key.
72
+ - `acqDb.recordDealActivity({ dealId, type, title, description?, payload? })` -- appends an entry to `acq_deal_activity_log`.
73
+ - `acqDb.recordTouchpoint({ dealId, direction, channel, kind, payload, contactId, sourceExecutionId })` -- writes an `acq_touchpoints` row for outbound/inbound audit.
74
+ - `acqDb.loadDeal({ dealId })` -- returns the deal row joined with contact and company.
75
+ - `acqDb.listDealTouchpoints({ dealId, kind?, limit? })` -- returns touchpoint history for a deal.
76
+
77
+ These wrap existing `LeadService` logic -- they are extraction, not new business logic. The separate `crm` adapter also exposes focused CRM methods such as `crm.recordActivity(...)`; use `acqDb` for action workflows that need deal transitions, touchpoints, and the broader acquisition substrate.
78
+
79
+ ## Override a Default Action
80
+
81
+ Deploy a workflow with the same `workflowId` as the default action you want to replace. The resource registry resolves your project-owned workflow first.
82
+
83
+ The canonical transition workflow (see `external/elevasis/operations/src/sales/crm/actions/move-to-proposal.ts` for the deployed reference):
84
+
85
+ ```ts
86
+ // operations/src/sales/crm/actions/move-to-proposal.ts
87
+ import type { WorkflowDefinition } from '@elevasis/sdk'
88
+ import { acqDb } from '@elevasis/sdk/worker'
89
+ import { ActionWorkflowInputSchema, ActionWorkflowOutputSchema } from '../shared/action-workflow-schemas.js'
90
+
91
+ export const moveToProposalWorkflow: WorkflowDefinition = {
92
+ config: {
93
+ resourceId: 'move_to_proposal-workflow',
94
+ name: 'Move to Proposal',
95
+ type: 'workflow',
96
+ version: '1.0.0',
97
+ status: 'prod',
98
+ category: 'internal',
99
+ links: [{ nodeId: 'feature:sales.crm', kind: 'operates-on' }]
100
+ },
101
+ contract: {
102
+ inputSchema: ActionWorkflowInputSchema,
103
+ outputSchema: ActionWorkflowOutputSchema
104
+ },
105
+ steps: {
106
+ transition: {
107
+ id: 'transition',
108
+ name: 'Transition to Proposal',
109
+ inputSchema: ActionWorkflowInputSchema,
110
+ outputSchema: ActionWorkflowOutputSchema,
111
+ handler: async (rawInput, context) => {
112
+ const { dealId } = rawInput as { dealId: string; organizationId: string }
113
+ context.logger.info(`[transition] Moving deal ${dealId} to proposal`)
114
+ await acqDb.transitionDeal({ dealId, toStage: 'proposal' })
115
+ return { dealId, sent: false }
116
+ },
117
+ next: null
118
+ }
119
+ },
120
+ entryPoint: 'transition'
121
+ }
122
+ ```
123
+
124
+ To add side effects (create a task, send a Slack message, record a touchpoint), extend the handler before the `return`. The `resourceId` must match the action's `workflowId` in `DEFAULT_CRM_ACTIONS` from `@elevasis/sdk` exactly.
125
+
126
+ Register the workflow in the operations manifest:
127
+
128
+ ```ts
129
+ // operations/src/index.ts
130
+ import { moveToProposalWorkflow } from './sales/crm/actions/move-to-proposal.js'
131
+
132
+ export const deploymentSpec = {
133
+ workflows: [moveToProposalWorkflow],
134
+ agents: []
135
+ }
136
+ ```
137
+
138
+ Then deploy:
139
+
140
+ ```bash
141
+ pnpm exec elevasis-sdk deploy
142
+ ```
143
+
144
+ ## 1. Override the Shared CRM Action Set (UI-Side)
145
+
146
+ To hide, reorder, or relabel buttons in the shared deal UI, create a local action config module:
147
+
148
+ ```ts
149
+ // ui/src/config/crm-actions.ts
150
+ import { DEFAULT_CRM_ACTIONS, type ActionDef } from '@elevasis/sdk'
151
+
152
+ export const crmActions: ActionDef[] = [
153
+ ...DEFAULT_CRM_ACTIONS.filter((action) => action.key !== 'move_to_closed_lost')
154
+ ]
155
+ ```
156
+
157
+ To add a new action entry alongside the defaults (note: brand-new keys are not server-dispatched until the platform knows their `workflowId`):
158
+
159
+ ```ts
160
+ // ui/src/config/crm-actions.ts
161
+ import { DEFAULT_CRM_ACTIONS, type ActionDef } from '@elevasis/sdk'
162
+
163
+ export const crmActions: ActionDef[] = [
164
+ ...DEFAULT_CRM_ACTIONS,
165
+ {
166
+ key: 'send_quote',
167
+ label: 'Send Quote',
168
+ isAvailableFor: (deal) => deal.stage_key === 'proposal',
169
+ workflowId: 'send-quote-workflow'
170
+ }
171
+ ]
172
+ ```
173
+
174
+ Wire it into the app provider. If the template uses `createElevasisApp`, pass it in the app config:
175
+
176
+ ```tsx
177
+ // ui/src/main.tsx
178
+ import { crmActions } from './config/crm-actions'
179
+
180
+ const App = createElevasisApp({
181
+ router,
182
+ apiUrl: API_URL,
183
+ auth: {
184
+ clientId: import.meta.env.VITE_WORKOS_CLIENT_ID,
185
+ redirectUri: import.meta.env.VITE_WORKOS_REDIRECT_URI,
186
+ devMode: true
187
+ },
188
+ theme: { presets: themePresets, background, loader },
189
+ queryClient,
190
+ crmActions
191
+ })
192
+ ```
193
+
194
+ If the project hand-wires providers, pass the same array to `ElevasisUIProvider` or `ElevasisCoreProvider`:
195
+
196
+ ```tsx
197
+ <ElevasisUIProvider auth={auth} apiUrl={API_URL} crmActions={crmActions}>
198
+ <AppRoutes />
199
+ </ElevasisUIProvider>
200
+ ```
201
+
202
+ This controls the shared `DealDetailPage` and `DealDrawer` action row.
203
+
204
+ ## 2. Add a Custom Workflow-Backed Action
205
+
206
+ For a brand-new action key that calls a project-owned workflow, define the workflow first.
207
+
208
+ If the action sends email, writes to another channel, or otherwise touches a customer, use `acqDb.recordTouchpoint` inside the workflow handler to write an audit row. For an advanced Instantly-thread-aware variant that prefers in-thread replies and falls back to fresh outbound, use the production CRM workflow summary as the canonical example: `apps/docs/content/docs/in-progress/active-development/sdk-changes/crm/crm-current-state-assessment.mdx`.
209
+
210
+ ### Define the Workflow Contract
211
+
212
+ ```ts
213
+ // core/types/index.ts
214
+ import { z } from 'zod'
215
+
216
+ export const sendQuoteInputSchema = z.object({
217
+ dealId: z.string().uuid(),
218
+ organizationId: z.string().uuid()
219
+ })
220
+
221
+ export const sendQuoteOutputSchema = z.object({
222
+ dealId: z.string().uuid(),
223
+ sent: z.boolean(),
224
+ messageId: z.string().optional()
225
+ })
226
+
227
+ export type SendQuoteInput = z.infer<typeof sendQuoteInputSchema>
228
+ export type SendQuoteOutput = z.infer<typeof sendQuoteOutputSchema>
229
+ ```
230
+
231
+ ### Define the Workflow
232
+
233
+ ```ts
234
+ // operations/src/sales/send-quote.ts
235
+ import type { WorkflowDefinition } from '@elevasis/sdk'
236
+ import { acqDb, createResendAdapter } from '@elevasis/sdk/worker'
237
+ import {
238
+ sendQuoteInputSchema,
239
+ sendQuoteOutputSchema
240
+ } from '@core/types'
241
+
242
+ export const sendQuoteWorkflow: WorkflowDefinition = {
243
+ config: {
244
+ resourceId: 'send-quote-workflow',
245
+ name: 'Send Quote',
246
+ type: 'workflow',
247
+ version: '1.0.0',
248
+ status: 'dev',
249
+ category: 'internal',
250
+ links: [{ nodeId: 'feature:sales.crm', kind: 'operates-on' }]
251
+ },
252
+ contract: {
253
+ inputSchema: sendQuoteInputSchema,
254
+ outputSchema: sendQuoteOutputSchema
255
+ },
256
+ steps: {
257
+ send: {
258
+ id: 'send',
259
+ name: 'Send Quote Email',
260
+ inputSchema: sendQuoteInputSchema,
261
+ outputSchema: sendQuoteOutputSchema,
262
+ handler: async (rawInput, context) => {
263
+ const { dealId } = rawInput as { dealId: string; organizationId: string }
264
+ const deal = await acqDb.loadDeal({ dealId })
265
+
266
+ const resend = createResendAdapter('my-resend-credential')
267
+ context.logger.info(`[send-quote] Sending quote for deal ${dealId}`)
268
+
269
+ const sent = await resend.sendEmail({
270
+ to: deal.contact.email,
271
+ subject: 'Your quote is ready',
272
+ html: `<p>Your quote is ready. Reply to this email with questions.</p>`
273
+ })
274
+
275
+ await acqDb.recordTouchpoint({
276
+ dealId,
277
+ direction: 'outbound',
278
+ channel: 'email',
279
+ kind: 'quote_sent',
280
+ payload: { triggered_by_action: 'send_quote' },
281
+ contactId: deal.contact.id,
282
+ sourceExecutionId: context.executionId
283
+ })
284
+
285
+ return {
286
+ dealId,
287
+ sent: true,
288
+ messageId: String(sent.id ?? '')
289
+ }
290
+ },
291
+ next: null
292
+ }
293
+ },
294
+ entryPoint: 'send'
295
+ }
296
+ ```
297
+
298
+ Register the workflow in the operations manifest and deploy:
299
+
300
+ ```ts
301
+ // operations/src/index.ts
302
+ import { sendQuoteWorkflow } from './sales/send-quote.js'
303
+
304
+ export const deploymentSpec = {
305
+ workflows: [sendQuoteWorkflow],
306
+ agents: []
307
+ }
308
+ ```
309
+
310
+ ```bash
311
+ pnpm exec elevasis-sdk deploy
312
+ ```
313
+
314
+ ### Choose the UI Path
315
+
316
+ A custom `ActionDef` entry with `workflowId: 'send-quote-workflow'` can be rendered through the shared `crmActions` provider path. Server dispatch through `POST /deals/:dealId/actions/:actionKey` is constrained by the platform-known/default action set in v1, so use a custom deal page or render slot that calls the workflow directly through `/execute` or `/execute-async` when the action key is outside that server-side set.
317
+
318
+ ```tsx
319
+ // ui/src/features/crm/components/SendQuoteButton.tsx
320
+ import { Button } from '@mantine/core'
321
+ import { useMutation } from '@tanstack/react-query'
322
+ import { useElevasisServices } from '@elevasis/ui/provider'
323
+
324
+ export function SendQuoteButton({ dealId }: { dealId: string }) {
325
+ const { apiRequest, organizationId } = useElevasisServices()
326
+
327
+ const sendQuote = useMutation({
328
+ mutationFn: async () => {
329
+ if (!organizationId) throw new Error('Organization context is not ready')
330
+
331
+ return apiRequest('/execute-async', {
332
+ method: 'POST',
333
+ body: JSON.stringify({
334
+ resourceType: 'workflow',
335
+ resourceId: 'send-quote-workflow',
336
+ input: { dealId, organizationId }
337
+ })
338
+ })
339
+ }
340
+ })
341
+
342
+ return (
343
+ <Button loading={sendQuote.isPending} onClick={() => sendQuote.mutate()}>
344
+ Send Quote
345
+ </Button>
346
+ )
347
+ }
348
+ ```
349
+
350
+ Use this button through a custom deal route or a `renderActions` slot where available.
351
+
352
+ ## 3. Build a Fully Custom Deal Page
353
+
354
+ When you own the full page, use the primitives directly:
355
+
356
+ - `useDealDetail(dealId)` loads the deal.
357
+ - `deriveActions(deal, crmActions)` filters the action set.
358
+ - `useExecuteAction({ dealId })` dispatches platform-known action keys.
359
+ - Project-owned workflow buttons call `/execute` or `/execute-async` directly when they are outside the server-dispatched action set.
360
+
361
+ ```tsx
362
+ import { DEFAULT_CRM_ACTIONS, deriveActions } from '@elevasis/sdk'
363
+ import { Group, Stack } from '@mantine/core'
364
+ import { useMemo } from 'react'
365
+ import { useDealDetail, useExecuteAction } from '@elevasis/ui/hooks'
366
+ import { SendQuoteButton } from './SendQuoteButton'
367
+
368
+ export function CustomDealPage({ dealId }: { dealId: string }) {
369
+ const { data: deal } = useDealDetail(dealId)
370
+ const executeAction = useExecuteAction({ dealId })
371
+
372
+ const platformActions = useMemo(() => {
373
+ return deal ? deriveActions(deal, DEFAULT_CRM_ACTIONS) : []
374
+ }, [deal])
375
+
376
+ if (!deal) return null
377
+
378
+ return (
379
+ <Stack>
380
+ <Group>
381
+ {platformActions.map((action) => (
382
+ <button
383
+ key={action.key}
384
+ type="button"
385
+ onClick={() => executeAction.mutate({ key: action.key })}
386
+ >
387
+ {action.label}
388
+ </button>
389
+ ))}
390
+ <SendQuoteButton dealId={deal.id} />
391
+ </Group>
392
+ </Stack>
393
+ )
394
+ }
395
+ ```
396
+
397
+ ## Verify
398
+
399
+ Run the relevant checks from the project root:
400
+
401
+ ```bash
402
+ pnpm -C operations run check
403
+ pnpm -C ui run check
404
+ ```
405
+
406
+ For a workflow-backed action, deploy or run the workflow smoke before wiring it into the UI:
407
+
408
+ ```bash
409
+ pnpm -C operations exec elevasis-sdk check
410
+ pnpm -C operations exec elevasis-sdk deploy
411
+ ```