@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.
- package/dist/cli.cjs +1 -1
- package/dist/index.d.ts +287 -55
- package/dist/index.js +62 -117
- package/dist/test-utils/index.d.ts +275 -15
- package/dist/test-utils/index.js +10 -1
- package/dist/worker/index.js +10 -1
- package/package.json +1 -1
- package/reference/claude-config/rules/agent-start-here.md +13 -7
- package/reference/claude-config/rules/organization-os.md +10 -6
- package/reference/claude-config/rules/platform.md +2 -1
- package/reference/claude-config/rules/ui.md +8 -6
- package/reference/claude-config/rules/vibe.md +11 -5
- package/reference/claude-config/sync-notes/2026-04-27-crm-hitl-action-layer-cutover.md +1 -5
- package/reference/claude-config/sync-notes/2026-04-27-lead-gen-substrate-train.md +110 -0
- package/reference/packages/ui/src/test-utils/README.md +2 -0
- package/reference/scaffold/index.mdx +8 -5
- package/reference/scaffold/recipes/customize-crm-actions.md +411 -0
- package/reference/scaffold/recipes/extend-crm.md +255 -0
- package/reference/scaffold/recipes/extend-lead-gen.md +297 -0
- package/reference/scaffold/recipes/index.md +20 -11
- package/reference/scaffold/reference/contracts.md +2612 -0
- package/reference/scaffold/ui/customization.md +4 -0
|
@@ -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
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
```
|