@elevasis/sdk 1.55.0 → 1.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/{chunk-72ZGICTR.js → chunk-3XEHBMWM.js} +1 -1
- package/dist/{chunk-R3J6BEPO.js → chunk-HDMMMDIR.js} +225 -67
- package/dist/cli.cjs +209 -109
- package/dist/index.d.ts +82 -20
- package/dist/index.js +1 -1
- package/dist/node/index.d.ts +1 -8
- package/dist/node/index.js +3 -14
- package/dist/test-utils/index.js +2 -2
- package/dist/worker/index.js +2 -2
- package/package.json +2 -2
- package/reference/_navigation.md +4 -4
- package/reference/_reference-manifest.json +3 -3
- package/reference/index.mdx +1 -1
- package/reference/packages/ui/src/knowledge/README.md +32 -32
- package/reference/rules/agent-start-here.md +1 -1
- package/reference/rules/error-handling.md +50 -1
- package/reference/rules/execution.md +63 -7
- package/reference/rules/organization-model.md +3 -2
- package/reference/scaffold/recipes/customize-crm-actions.md +231 -223
- package/reference/scaffold/recipes/customize-organization-model.md +3 -1
- package/reference/scaffold/recipes/extend-crm.md +4 -2
- package/reference/scaffold/recipes/extend-lead-gen.md +2 -2
- package/reference/scaffold/recipes/index.md +2 -2
- package/reference/scaffold/reference/contracts.md +49 -12
- package/reference/scaffold/reference/system-interface-capabilities.md +4 -4
- package/reference/sdk/cli-management.mdx +0 -4
- package/reference/sdk/cli.mdx +0 -4
- package/reference/sdk/concepts.mdx +0 -4
- package/reference/sdk/deployment/command-center.mdx +0 -4
- package/reference/sdk/framework/agent.mdx +0 -4
- package/reference/sdk/framework/index.mdx +0 -4
- package/reference/sdk/framework/project-structure.mdx +1 -7
- package/reference/sdk/getting-started.mdx +0 -4
- package/reference/sdk/index.mdx +4 -8
- package/reference/sdk/platform-tools/index.mdx +0 -4
- package/reference/sdk/resources/index.mdx +0 -4
- package/reference/sdk/resources/patterns.mdx +0 -4
- package/reference/sdk/runtime.mdx +0 -4
- package/reference/sdk/templates/data-enrichment.mdx +0 -4
- package/reference/sdk/templates/email-sender.mdx +0 -4
- package/reference/sdk/templates/index.mdx +0 -4
- package/reference/sdk/templates/lead-scorer.mdx +0 -4
- package/reference/sdk/templates/pdf-generator.mdx +0 -4
- package/reference/sdk/templates/recurring-job.mdx +0 -4
- package/reference/sdk/templates/text-classifier.mdx +0 -4
- package/reference/sdk/templates/web-scraper.mdx +0 -4
- package/reference/sdk/troubleshooting.mdx +0 -4
- package/reference/ui/exports.mdx +1 -1
|
@@ -1,192 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Customize CRM Actions
|
|
3
|
+
description: Add, hide, reorder, or re-implement CRM deal action buttons by authoring them in your project's Organization Model and deploying the workflows they dispatch to.
|
|
4
|
+
---
|
|
1
5
|
<!-- @generated by packages/sdk/scripts/copy-reference-docs.mjs -- DO NOT EDIT -->
|
|
2
6
|
<!-- Regenerate: pnpm scaffold:sync -->
|
|
3
7
|
|
|
4
|
-
---
|
|
5
|
-
title: Customize CRM Actions
|
|
6
|
-
description: Add, hide, or replace CRM deal action buttons in a template-derived project, and override default platform action workflows with project-owned implementations.
|
|
7
|
-
---
|
|
8
8
|
|
|
9
9
|
# Customize CRM Actions
|
|
10
10
|
|
|
11
|
-
CRM deal
|
|
11
|
+
CRM deal action buttons are authored in the Organization Model, not in code. Each button is one `actionTypes` record under the `sales.crm` System's ontology scope. The record carries the button's label, its order in the action bar, the conditions under which it appears, and the id of the deployed resource it dispatches to.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
`compileCrmActionCatalog` turns those records into a `CompiledCrmAction[]`. The shared UI reads that array from the provider and filters it per deal with `deriveCompiledCrmActions`; the API re-checks the same conditions server-side before dispatching.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
For the broader CRM extension map -- pages, sidebars, hooks, workflow adapters, System Interfaces, and org-model boundaries -- start with [Build and Extend CRM](extend-crm.md). This recipe is only the deal-action path.
|
|
16
16
|
|
|
17
17
|
Use this recipe when a user asks for work like:
|
|
18
18
|
|
|
19
19
|
- "Hide Close Lost from the deal drawer."
|
|
20
20
|
- "Add a Send Quote action to deals in Proposal."
|
|
21
|
-
- "
|
|
21
|
+
- "Change what Move to Proposal does."
|
|
22
|
+
- "Reorder the deal action buttons."
|
|
22
23
|
- "Build a custom deal page that runs one of our workflows."
|
|
23
24
|
|
|
24
|
-
##
|
|
25
|
-
|
|
26
|
-
Every action in the CRM action catalog 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 action entry. The resource registry picks the project-owned workflow over the platform default. The `isAvailableFor` predicate comes from the caller-supplied action catalog; override what the action does and keep button availability aligned with the server-side gate.
|
|
34
|
-
|
|
35
|
-
CRM action dispatch is an API-backed surface. The tenant Organization Model must expose an active and ready `sales.crm.apiInterface` marker before shared CRM action buttons are considered ready. If a lead-gen workflow hands off into CRM, model that crossing through lead-gen resources that bind the CRM pipeline plus a scoped topology grant to `sales.crm.apiInterface` instead of making base lead-gen readiness depend on CRM catalogs.
|
|
36
|
-
|
|
37
|
-
## ActionDef Shape
|
|
38
|
-
|
|
39
|
-
Read the generated contract reference before editing:
|
|
25
|
+
## One Action, One Record
|
|
40
26
|
|
|
41
|
-
`
|
|
27
|
+
An action exists exactly once, in the OM. There is no second array to keep in sync and no override-by-shadowing mechanism -- the record's `resourceId` is the single binding between the button and the code that runs.
|
|
42
28
|
|
|
43
|
-
The
|
|
29
|
+
The ontology id's local id **is** the action key used in `POST /deals/:dealId/actions/:actionKey`. `sales.crm:action/move_to_proposal` is the `move_to_proposal` button.
|
|
44
30
|
|
|
45
|
-
<!-- doc-snippet:skip:
|
|
31
|
+
<!-- doc-snippet:skip: illustrative OM excerpt -- one entry from the sales.crm System's ontology.actionTypes map -->
|
|
46
32
|
|
|
47
33
|
```ts
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
isAvailableFor: (deal: AcqDealRow) => boolean
|
|
52
|
-
workflowId: string
|
|
53
|
-
payloadSchema?: z.ZodTypeAny
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
`deriveActions(deal, actions)` returns only render-time actions: `{ key, label, payloadSchema? }`. It does not expose `workflowId` or `isAvailableFor` to the browser.
|
|
58
|
-
|
|
59
|
-
A minimal entry looks like:
|
|
60
|
-
|
|
61
|
-
<!-- doc-snippet:skip: illustrative excerpt, not a standalone compilable file -->
|
|
62
|
-
|
|
63
|
-
```ts
|
|
64
|
-
{
|
|
65
|
-
key: 'move_to_proposal',
|
|
34
|
+
// core/config/organization-model.ts, under the sales.crm System
|
|
35
|
+
'sales.crm:action/move_to_proposal': {
|
|
36
|
+
id: 'sales.crm:action/move_to_proposal',
|
|
66
37
|
label: 'Move to Proposal',
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
38
|
+
description: 'Advance a qualified CRM deal into the proposal stage.',
|
|
39
|
+
ownerSystemId: 'sales.crm',
|
|
40
|
+
order: 10,
|
|
41
|
+
actsOn: ['sales.crm:object/deal'],
|
|
42
|
+
resourceId: 'move_to_proposal-workflow',
|
|
43
|
+
availableFrom: { stageKeys: ['interested'] },
|
|
44
|
+
effects: [
|
|
45
|
+
{ kind: 'setProperty', objectType: 'sales.crm:object/deal', property: 'stage', to: 'proposal' },
|
|
46
|
+
{ kind: 'emitEvent', eventType: 'sales.crm:event/deal.stage-changed' }
|
|
47
|
+
]
|
|
70
48
|
}
|
|
71
49
|
```
|
|
72
50
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
51
|
+
| Field | Meaning |
|
|
52
|
+
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
53
|
+
| `label` | Button text. Falls back to the action key when omitted. |
|
|
54
|
+
| `order` | Position in the deal action bar. Records without one sort after every ordered record, in id order. |
|
|
55
|
+
| `resourceId` | The deployed workflow this action dispatches to. Required for any record with `availableFrom`. |
|
|
56
|
+
| `availableFrom` | When the button appears. Omit the whole field to keep a record in the ontology without making it a deal action. |
|
|
57
|
+
| `actsOn` | The ontology object the action operates on. |
|
|
58
|
+
| `effects` | Declarative record of what the action changes. Read by the graph and by knowledge nodes; it does not execute -- the workflow does that. |
|
|
80
59
|
|
|
81
|
-
|
|
60
|
+
### `availableFrom`
|
|
82
61
|
|
|
83
|
-
|
|
62
|
+
Three optional dimensions, all evaluated against the deal row:
|
|
84
63
|
|
|
85
|
-
|
|
64
|
+
```
|
|
65
|
+
stageKeys deal.stage_key (required, at least one)
|
|
66
|
+
stateKeys deal.state_key (optional)
|
|
67
|
+
nextActionKeys deal.nextAction (optional)
|
|
68
|
+
```
|
|
86
69
|
|
|
87
|
-
|
|
70
|
+
Dimensions **AND** together; membership within one dimension is **OR**; an omitted dimension is unconstrained. So this button shows only for a deal that is in the `interested` stage, sitting in one of three states, and whose derived next action is `send_reply`:
|
|
88
71
|
|
|
89
|
-
<!-- doc-snippet:skip: illustrative excerpt --
|
|
72
|
+
<!-- doc-snippet:skip: illustrative excerpt -- the availableFrom fragment of an actionTypes record -->
|
|
90
73
|
|
|
91
74
|
```ts
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
import { ActionWorkflowInputSchema, ActionWorkflowOutputSchema } from '../shared/action-workflow-schemas.js'
|
|
97
|
-
|
|
98
|
-
export const moveToProposalWorkflow: WorkflowDefinition = {
|
|
99
|
-
config: {
|
|
100
|
-
resource: resourceDescriptors.moveToProposal,
|
|
101
|
-
resourceId: resourceDescriptors.moveToProposal.id,
|
|
102
|
-
name: 'Move to Proposal',
|
|
103
|
-
description: 'Transition a deal to the proposal stage.',
|
|
104
|
-
type: resourceDescriptors.moveToProposal.kind,
|
|
105
|
-
version: '1.0.0',
|
|
106
|
-
status: 'prod',
|
|
107
|
-
category: 'internal',
|
|
108
|
-
},
|
|
109
|
-
contract: {
|
|
110
|
-
inputSchema: ActionWorkflowInputSchema,
|
|
111
|
-
outputSchema: ActionWorkflowOutputSchema
|
|
112
|
-
},
|
|
113
|
-
steps: {
|
|
114
|
-
transition: {
|
|
115
|
-
id: 'transition',
|
|
116
|
-
name: 'Transition to Proposal',
|
|
117
|
-
description: 'Call transitionDeal to move the deal to the proposal stage',
|
|
118
|
-
inputSchema: ActionWorkflowInputSchema,
|
|
119
|
-
outputSchema: ActionWorkflowOutputSchema,
|
|
120
|
-
handler: async (rawInput, context) => {
|
|
121
|
-
const { dealId } = rawInput as { dealId: string; organizationId: string }
|
|
122
|
-
context.logger.info(`[transition] Moving deal ${dealId} to proposal`)
|
|
123
|
-
await acqDb.transitionDeal({ dealId, toStage: 'proposal' })
|
|
124
|
-
return { dealId, sent: false }
|
|
125
|
-
},
|
|
126
|
-
next: null
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
entryPoint: 'transition'
|
|
75
|
+
availableFrom: {
|
|
76
|
+
stageKeys: ['interested'],
|
|
77
|
+
stateKeys: ['discovery_replied', 'discovery_link_sent', 'discovery_nudging'],
|
|
78
|
+
nextActionKeys: ['send_reply']
|
|
130
79
|
}
|
|
131
80
|
```
|
|
132
81
|
|
|
133
|
-
|
|
82
|
+
Every `stageKeys` entry must be a stage in the CRM pipeline catalog, and every `stateKeys` entry must be a valid state for one of the declared stages. `compileCrmActionCatalog` checks both and reports every violation at once as a `SystemInterfaceReadinessError` carrying `SYSTEM_INTERFACE_INVALID` issues -- which reaches an API caller as a 503 with `readinessCode`, not a silent missing button.
|
|
134
83
|
|
|
135
|
-
|
|
84
|
+
## Compile the Catalog
|
|
136
85
|
|
|
137
|
-
<!-- doc-snippet:skip: illustrative excerpt
|
|
86
|
+
<!-- doc-snippet:skip: illustrative excerpt -- `@core/config/organization-model` is the project's own OM alias -->
|
|
138
87
|
|
|
139
88
|
```ts
|
|
140
|
-
//
|
|
141
|
-
import {
|
|
89
|
+
// ui/src/config/crm-actions.ts
|
|
90
|
+
import { compileCrmActionCatalog, compileCrmApiOntologyValidationIndex, type CompiledCrmAction } from '@elevasis/sdk'
|
|
91
|
+
import { organizationModel } from '@core/config/organization-model'
|
|
142
92
|
|
|
143
|
-
export const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
93
|
+
export const crmActions: CompiledCrmAction[] = compileCrmActionCatalog(
|
|
94
|
+
compileCrmApiOntologyValidationIndex(organizationModel)
|
|
95
|
+
)
|
|
147
96
|
```
|
|
148
97
|
|
|
149
|
-
|
|
98
|
+
`compileCrmApiOntologyValidationIndex` proves the `sales.crm` System Interface is ready -- active `apiInterface` marker, resolvable scoped resources, a non-empty pipeline catalog -- before the action catalog compiles against it. CRM action dispatch is an API-backed surface: if that marker is missing or disabled, compilation throws rather than producing a catalog of buttons that would 503 on click.
|
|
150
99
|
|
|
151
|
-
|
|
152
|
-
pnpm exec elevasis-sdk deploy
|
|
153
|
-
```
|
|
100
|
+
If a lead-gen workflow hands off into CRM, model that crossing through lead-gen resources that bind the CRM pipeline plus a scoped topology grant to `sales.crm.apiInterface` -- not by making base lead-gen readiness depend on CRM catalogs.
|
|
154
101
|
|
|
155
|
-
|
|
102
|
+
The compiled shape:
|
|
156
103
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
<!-- doc-snippet:skip: illustrative excerpt with a relative import to the project's own platform-crm-actions module, not a standalone compilable file -->
|
|
104
|
+
<!-- doc-snippet:skip: shape excerpt from compile-action-catalog.ts -->
|
|
160
105
|
|
|
161
106
|
```ts
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
107
|
+
export interface CompiledCrmAction {
|
|
108
|
+
key: string
|
|
109
|
+
label: string
|
|
110
|
+
availableFrom: CompiledCrmActionAvailability
|
|
111
|
+
resourceId: string
|
|
112
|
+
payloadSchema?: z.ZodTypeAny
|
|
113
|
+
}
|
|
167
114
|
```
|
|
168
115
|
|
|
169
|
-
|
|
116
|
+
### Wire It Into the Provider
|
|
170
117
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
```ts
|
|
174
|
-
// ui/src/config/crm-actions.ts
|
|
175
|
-
import type { ActionDef } from '@elevasis/sdk'
|
|
176
|
-
import { platformCrmActions } from './platform-crm-actions'
|
|
177
|
-
|
|
178
|
-
export const crmActions: ActionDef[] = [
|
|
179
|
-
...platformCrmActions,
|
|
180
|
-
{
|
|
181
|
-
key: 'send_quote',
|
|
182
|
-
label: 'Send Quote',
|
|
183
|
-
isAvailableFor: (deal) => deal.stage_key === 'proposal',
|
|
184
|
-
workflowId: 'send-quote-workflow'
|
|
185
|
-
}
|
|
186
|
-
]
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
Wire it into the app provider. If the template uses `createElevasisApp`, pass it in the app config:
|
|
118
|
+
If the template uses `createElevasisApp`, pass it in the app config:
|
|
190
119
|
|
|
191
120
|
```tsx
|
|
192
121
|
// ui/src/main.tsx
|
|
@@ -214,48 +143,47 @@ If the project hand-wires providers, pass the same array to `ElevasisUIProvider`
|
|
|
214
143
|
</ElevasisUIProvider>
|
|
215
144
|
```
|
|
216
145
|
|
|
217
|
-
This
|
|
146
|
+
This drives the shared `DealDetailPage` and `DealDrawer` action rows.
|
|
218
147
|
|
|
219
|
-
##
|
|
148
|
+
## How Dispatch Works
|
|
220
149
|
|
|
221
|
-
|
|
150
|
+
When the shared UI calls `POST /deals/:dealId/actions/:actionKey`, the platform:
|
|
222
151
|
|
|
223
|
-
|
|
152
|
+
1. Loads the deal and finds the compiled action by key.
|
|
153
|
+
2. Re-evaluates `availableFrom` against the deal server-side. This is the security gate and cannot be skipped -- a client that fabricates an action key or calls one out of stage gets a 400.
|
|
154
|
+
3. Validates the payload against the action's `payloadSchema`, and rejects any payload sent to an action that declares none.
|
|
155
|
+
4. Appends an `action_taken` entry to the deal's activity log, before the handler runs -- operator intent is recorded separately from effect.
|
|
156
|
+
5. Resolves `resourceId` to the deployed resource and dispatches through `SingleExecutionCoordinator`, the same execution engine as every other workflow.
|
|
157
|
+
6. Returns the refetched deal.
|
|
224
158
|
|
|
225
|
-
###
|
|
159
|
+
### Payload Schemas Are Platform-Registered
|
|
226
160
|
|
|
227
|
-
|
|
228
|
-
// core/types/index.ts
|
|
229
|
-
import { z } from 'zod'
|
|
161
|
+
A Zod schema is not serializable model data, so `payloadSchema` cannot be authored in the OM. `compileCrmActionCatalog` resolves it from a small registry in published core, keyed by action key. Today only `send_reply` has one (`replyBody`). An action with no registered schema is a one-click button and rejects any payload.
|
|
230
162
|
|
|
231
|
-
|
|
232
|
-
dealId: z.string().uuid(),
|
|
233
|
-
organizationId: z.string().uuid()
|
|
234
|
-
})
|
|
163
|
+
Adding a payload schema for a new action key is platform work, not scaffold work. Until then, take input through a custom UI path that calls the workflow directly (see [Custom Deal Page](#build-a-fully-custom-deal-page) below).
|
|
235
164
|
|
|
236
|
-
|
|
237
|
-
dealId: z.string().uuid(),
|
|
238
|
-
sent: z.boolean(),
|
|
239
|
-
messageId: z.string().optional()
|
|
240
|
-
})
|
|
165
|
+
## Add an Action
|
|
241
166
|
|
|
242
|
-
|
|
243
|
-
export type SendQuoteOutput = z.infer<typeof sendQuoteOutputSchema>
|
|
244
|
-
```
|
|
167
|
+
Three steps, in order.
|
|
245
168
|
|
|
246
|
-
### Define the Workflow
|
|
169
|
+
### 1. Define and Deploy the Workflow
|
|
247
170
|
|
|
248
|
-
|
|
171
|
+
Workflows backing CRM actions use worker adapters instead of raw Supabase calls. The acquisition adapter is imported as `acqDb` from `@elevasis/sdk/worker`:
|
|
172
|
+
|
|
173
|
+
- `acqDb.transitionDeal({ dealId, toStage, toState? })` -- moves a deal to a new stage, optionally updating `state_key`.
|
|
174
|
+
- `acqDb.recordDealActivity({ dealId, type, title, description?, payload? })` -- appends an entry to the deal's `activity_log` JSONB column. Use this for outbound/inbound audit and lifecycle events.
|
|
175
|
+
- `acqDb.loadDeal({ dealId })` -- returns the deal row joined with contact and company.
|
|
176
|
+
|
|
177
|
+
The separate `crm` adapter exposes focused CRM methods such as `crm.recordActivity(...)`; use `acqDb` for action workflows that need deal transitions and the broader acquisition substrate.
|
|
178
|
+
|
|
179
|
+
<!-- doc-snippet:skip: illustrative excerpt -- `resourceDescriptors` names the project's own resource-descriptors export, and the schemas are project-local -->
|
|
249
180
|
|
|
250
181
|
```ts
|
|
251
182
|
// operations/src/sales/send-quote.ts
|
|
252
183
|
import type { WorkflowDefinition } from '@elevasis/sdk'
|
|
253
184
|
import { acqDb, createResendAdapter } from '@elevasis/sdk/worker'
|
|
254
185
|
import { resourceDescriptors } from '@core/config/organization-model'
|
|
255
|
-
import {
|
|
256
|
-
sendQuoteInputSchema,
|
|
257
|
-
sendQuoteOutputSchema
|
|
258
|
-
} from '@core/types'
|
|
186
|
+
import { sendQuoteInputSchema, sendQuoteOutputSchema } from '@core/types'
|
|
259
187
|
|
|
260
188
|
export const sendQuoteWorkflow: WorkflowDefinition = {
|
|
261
189
|
config: {
|
|
@@ -265,8 +193,8 @@ export const sendQuoteWorkflow: WorkflowDefinition = {
|
|
|
265
193
|
description: 'Sends a quote email to the deal contact.',
|
|
266
194
|
type: resourceDescriptors.sendQuote.kind,
|
|
267
195
|
version: '1.0.0',
|
|
268
|
-
status: '
|
|
269
|
-
category: 'internal'
|
|
196
|
+
status: 'prod',
|
|
197
|
+
category: 'internal'
|
|
270
198
|
},
|
|
271
199
|
contract: {
|
|
272
200
|
inputSchema: sendQuoteInputSchema,
|
|
@@ -299,11 +227,7 @@ export const sendQuoteWorkflow: WorkflowDefinition = {
|
|
|
299
227
|
payload: { triggered_by_action: 'send_quote', channel: 'email' }
|
|
300
228
|
})
|
|
301
229
|
|
|
302
|
-
return {
|
|
303
|
-
dealId,
|
|
304
|
-
sent: true,
|
|
305
|
-
messageId: String(sent.id ?? '')
|
|
306
|
-
}
|
|
230
|
+
return { dealId, sent: true, messageId: String(sent.id ?? '') }
|
|
307
231
|
},
|
|
308
232
|
next: null
|
|
309
233
|
}
|
|
@@ -312,9 +236,11 @@ export const sendQuoteWorkflow: WorkflowDefinition = {
|
|
|
312
236
|
}
|
|
313
237
|
```
|
|
314
238
|
|
|
315
|
-
|
|
239
|
+
If the action sends email, writes to another channel, or otherwise touches a customer, use `acqDb.recordDealActivity` inside the handler so the audit trail reflects it. For an Instantly-thread-aware variant that prefers in-thread replies and falls back to fresh outbound, see the canonical CRM action examples in `packages/elevasis/operations/src/sales/crm/actions/`.
|
|
240
|
+
|
|
241
|
+
Register and deploy:
|
|
316
242
|
|
|
317
|
-
<!-- doc-snippet:skip: illustrative excerpt with a relative import to the project's own workflow file
|
|
243
|
+
<!-- doc-snippet:skip: illustrative excerpt with a relative import to the project's own workflow file -->
|
|
318
244
|
|
|
319
245
|
```ts
|
|
320
246
|
// operations/src/index.ts
|
|
@@ -330,71 +256,118 @@ export const deploymentSpec = {
|
|
|
330
256
|
pnpm exec elevasis-sdk deploy
|
|
331
257
|
```
|
|
332
258
|
|
|
333
|
-
###
|
|
259
|
+
### 2. Author the Action Record
|
|
334
260
|
|
|
335
|
-
|
|
261
|
+
Add one entry to the `sales.crm` System's `ontology.actionTypes`, with `resourceId` matching the deployed resource id exactly:
|
|
336
262
|
|
|
337
|
-
|
|
338
|
-
// ui/src/features/crm/components/SendQuoteButton.tsx
|
|
339
|
-
import { Button } from '@mantine/core'
|
|
340
|
-
import { useMutation } from '@tanstack/react-query'
|
|
341
|
-
import { useElevasisServices } from '@elevasis/ui/provider'
|
|
342
|
-
import { resourceDescriptors } from '@core/config/organization-model'
|
|
263
|
+
<!-- doc-snippet:skip: illustrative OM excerpt -->
|
|
343
264
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
265
|
+
```ts
|
|
266
|
+
'sales.crm:action/send_quote': {
|
|
267
|
+
id: 'sales.crm:action/send_quote',
|
|
268
|
+
label: 'Send Quote',
|
|
269
|
+
description: 'Email the current quote to the deal contact.',
|
|
270
|
+
ownerSystemId: 'sales.crm',
|
|
271
|
+
order: 25,
|
|
272
|
+
actsOn: ['sales.crm:object/deal'],
|
|
273
|
+
resourceId: 'send-quote-workflow',
|
|
274
|
+
availableFrom: { stageKeys: ['proposal'] }
|
|
275
|
+
}
|
|
276
|
+
```
|
|
347
277
|
|
|
348
|
-
|
|
349
|
-
mutationFn: async () => {
|
|
350
|
-
if (!organizationId) throw new Error('Organization context is not ready')
|
|
278
|
+
The local id (`send_quote`) becomes the action key. Underscores are legal in a local id; the format is `system.path:kind/local-id`, parsed and formatted through the core ontology helpers rather than by splitting strings.
|
|
351
279
|
|
|
352
|
-
|
|
353
|
-
method: 'POST',
|
|
354
|
-
body: JSON.stringify({
|
|
355
|
-
resourceType: 'workflow',
|
|
356
|
-
resourceId,
|
|
357
|
-
input: { dealId, organizationId }
|
|
358
|
-
})
|
|
359
|
-
})
|
|
360
|
-
}
|
|
361
|
-
})
|
|
280
|
+
### 3. Deploy the Organization Model
|
|
362
281
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
282
|
+
The compiled catalog is derived from the OM, so the button appears once the model carrying it is deployed. Run the model deployment the same way you deploy any OM change in your project; `om:doctor` validates the ontology before it lands.
|
|
283
|
+
|
|
284
|
+
## Hide, Relabel, or Reorder an Action
|
|
285
|
+
|
|
286
|
+
All three are OM edits on the existing record -- no code change, no filtering array.
|
|
287
|
+
|
|
288
|
+
| Want | Do |
|
|
289
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------- |
|
|
290
|
+
| Hide the button entirely | Remove the record, or remove its `availableFrom` to keep it in the ontology without surfacing it as a button. |
|
|
291
|
+
| Change when it appears | Edit `availableFrom.stageKeys` / `stateKeys` / `nextActionKeys`. |
|
|
292
|
+
| Change the button text | Edit `label`. |
|
|
293
|
+
| Change its position | Edit `order`. |
|
|
294
|
+
|
|
295
|
+
Removing a record removes it from both the UI and the server-side dispatch set at once, because both read the same catalog. Note that removing an ontology id from a model whose snapshot is already deployed leaves that snapshot stale until the model is deployed again.
|
|
296
|
+
|
|
297
|
+
## Change What an Action Does
|
|
298
|
+
|
|
299
|
+
Edit the workflow the record's `resourceId` points at, or point `resourceId` at a different deployed resource. There is no same-name shadowing mechanism and no platform default to fall back to -- whatever id the record names is what runs.
|
|
300
|
+
|
|
301
|
+
The canonical transition workflow, for reference (see `packages/elevasis/operations/src/sales/crm/actions/move-to-proposal.ts`):
|
|
302
|
+
|
|
303
|
+
<!-- doc-snippet:skip: illustrative excerpt -- `resourceDescriptors` names the project's own resource-descriptors export, and `../shared/action-workflow-schemas.js` is a project-local sibling file -->
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
// operations/src/sales/crm/actions/move-to-proposal.ts
|
|
307
|
+
import type { WorkflowDefinition } from '@elevasis/sdk'
|
|
308
|
+
import { acqDb } from '@elevasis/sdk/worker'
|
|
309
|
+
import { resourceDescriptors } from '@core/config/organization-model'
|
|
310
|
+
import { ActionWorkflowInputSchema, ActionWorkflowOutputSchema } from '../shared/action-workflow-schemas.js'
|
|
311
|
+
|
|
312
|
+
export const moveToProposalWorkflow: WorkflowDefinition = {
|
|
313
|
+
config: {
|
|
314
|
+
resource: resourceDescriptors.moveToProposal,
|
|
315
|
+
resourceId: resourceDescriptors.moveToProposal.id,
|
|
316
|
+
name: 'Move to Proposal',
|
|
317
|
+
description: 'Transition a deal to the proposal stage.',
|
|
318
|
+
type: resourceDescriptors.moveToProposal.kind,
|
|
319
|
+
version: '1.0.0',
|
|
320
|
+
status: 'prod',
|
|
321
|
+
category: 'internal'
|
|
322
|
+
},
|
|
323
|
+
contract: {
|
|
324
|
+
inputSchema: ActionWorkflowInputSchema,
|
|
325
|
+
outputSchema: ActionWorkflowOutputSchema
|
|
326
|
+
},
|
|
327
|
+
steps: {
|
|
328
|
+
transition: {
|
|
329
|
+
id: 'transition',
|
|
330
|
+
name: 'Transition to Proposal',
|
|
331
|
+
description: 'Call transitionDeal to move the deal to the proposal stage',
|
|
332
|
+
inputSchema: ActionWorkflowInputSchema,
|
|
333
|
+
outputSchema: ActionWorkflowOutputSchema,
|
|
334
|
+
handler: async (rawInput, context) => {
|
|
335
|
+
const { dealId } = rawInput as { dealId: string; organizationId: string }
|
|
336
|
+
context.logger.info(`[transition] Moving deal ${dealId} to proposal`)
|
|
337
|
+
await acqDb.transitionDeal({ dealId, toStage: 'proposal' })
|
|
338
|
+
return { dealId, sent: false }
|
|
339
|
+
},
|
|
340
|
+
next: null
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
entryPoint: 'transition'
|
|
368
344
|
}
|
|
369
345
|
```
|
|
370
346
|
|
|
371
|
-
|
|
347
|
+
To add side effects (create a task, send a Slack message, log a deal activity), extend the handler before the `return`. Keep the record's `effects` in step with what the handler actually changes -- `effects` is what the graph and knowledge nodes read.
|
|
372
348
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
## 3. Build a Fully Custom Deal Page
|
|
349
|
+
## Build a Fully Custom Deal Page
|
|
376
350
|
|
|
377
351
|
When you own the full page, use the primitives directly:
|
|
378
352
|
|
|
379
353
|
- `useDealDetail(dealId)` loads the deal.
|
|
380
|
-
- `
|
|
381
|
-
- `useExecuteAction({ dealId })` dispatches
|
|
382
|
-
-
|
|
354
|
+
- `deriveCompiledCrmActions(deal, crmActions)` returns the subset the deal can currently invoke, in authored order.
|
|
355
|
+
- `useExecuteAction({ dealId })` dispatches by action key.
|
|
356
|
+
- A button for something that is not a CRM deal action calls `/execute` or `/execute-async` directly.
|
|
383
357
|
|
|
384
358
|
```tsx
|
|
385
|
-
import {
|
|
359
|
+
import { deriveCompiledCrmActions } from '@elevasis/sdk'
|
|
386
360
|
import { Group, Stack } from '@mantine/core'
|
|
387
361
|
import { useMemo } from 'react'
|
|
388
362
|
import { useDealDetail, useExecuteAction } from '@elevasis/ui/hooks'
|
|
389
363
|
import { crmActions } from '../config/crm-actions'
|
|
390
|
-
import { SendQuoteButton } from './SendQuoteButton'
|
|
391
364
|
|
|
392
365
|
export function CustomDealPage({ dealId }: { dealId: string }) {
|
|
393
366
|
const { data: deal } = useDealDetail(dealId)
|
|
394
367
|
const executeAction = useExecuteAction({ dealId })
|
|
395
368
|
|
|
396
|
-
const
|
|
397
|
-
return deal ?
|
|
369
|
+
const actions = useMemo(() => {
|
|
370
|
+
return deal ? deriveCompiledCrmActions(deal, crmActions) : []
|
|
398
371
|
}, [deal])
|
|
399
372
|
|
|
400
373
|
if (!deal) return null
|
|
@@ -402,22 +375,55 @@ export function CustomDealPage({ dealId }: { dealId: string }) {
|
|
|
402
375
|
return (
|
|
403
376
|
<Stack>
|
|
404
377
|
<Group>
|
|
405
|
-
{
|
|
406
|
-
<button
|
|
407
|
-
key={action.key}
|
|
408
|
-
type="button"
|
|
409
|
-
onClick={() => executeAction.mutate({ key: action.key })}
|
|
410
|
-
>
|
|
378
|
+
{actions.map((action) => (
|
|
379
|
+
<button key={action.key} type="button" onClick={() => executeAction.mutate({ key: action.key })}>
|
|
411
380
|
{action.label}
|
|
412
381
|
</button>
|
|
413
382
|
))}
|
|
414
|
-
<SendQuoteButton dealId={deal.id} />
|
|
415
383
|
</Group>
|
|
416
384
|
</Stack>
|
|
417
385
|
)
|
|
418
386
|
}
|
|
419
387
|
```
|
|
420
388
|
|
|
389
|
+
For a direct-workflow button, call `/execute-async` with the resource id:
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
// ui/src/features/crm/components/RunAuditButton.tsx
|
|
393
|
+
import { Button } from '@mantine/core'
|
|
394
|
+
import { useMutation } from '@tanstack/react-query'
|
|
395
|
+
import { useElevasisServices } from '@elevasis/ui/provider'
|
|
396
|
+
import { resourceDescriptors } from '@core/config/organization-model'
|
|
397
|
+
|
|
398
|
+
export function RunAuditButton({ dealId }: { dealId: string }) {
|
|
399
|
+
const { apiRequest, organizationId } = useElevasisServices()
|
|
400
|
+
const resourceId = resourceDescriptors.runAudit.id
|
|
401
|
+
|
|
402
|
+
const runAudit = useMutation({
|
|
403
|
+
mutationFn: async () => {
|
|
404
|
+
if (!organizationId) throw new Error('Organization context is not ready')
|
|
405
|
+
|
|
406
|
+
return apiRequest('/execute-async', {
|
|
407
|
+
method: 'POST',
|
|
408
|
+
body: JSON.stringify({
|
|
409
|
+
resourceType: 'workflow',
|
|
410
|
+
resourceId,
|
|
411
|
+
input: { dealId, organizationId }
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<Button loading={runAudit.isPending} onClick={() => runAudit.mutate()}>
|
|
419
|
+
Run Audit
|
|
420
|
+
</Button>
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Keep the same interface boundary for direct workflow buttons: custom UI may render read-only deal context without API readiness, but buttons that call `/execute`, `/execute-async`, or CRM action routes should be hidden or disabled until the relevant System Interface and any scoped topology grant are ready.
|
|
426
|
+
|
|
421
427
|
## CRM State-Key Source of Truth
|
|
422
428
|
|
|
423
429
|
CRM `stage_key` and `state_key` values are tenant/runtime data. In the Elevasis workspace, the canonical CRM pipeline definition lives in `@repo/elevasis-core/sales` and is authored into the `sales.crm:catalog/crm.pipeline` ontology catalog on the canonical organization model. Published `@elevasis/core` keeps only generic `StatefulPipelineDefinition` types/helpers and transport schemas.
|
|
@@ -432,7 +438,7 @@ const validStates = getValidStatesForStage(CRM_PIPELINE_DEFINITION, 'interested'
|
|
|
432
438
|
// [{ stateKey: 'discovery_replied', label: '...' }, ...]
|
|
433
439
|
```
|
|
434
440
|
|
|
435
|
-
Template-derived projects
|
|
441
|
+
Template-derived projects author their own CRM pipeline catalog in their own OM rather than importing Elevasis runtime constants from published core. The stage and state keys an action's `availableFrom` may name are exactly the ones that catalog defines.
|
|
436
442
|
|
|
437
443
|
## Activity Log Conventions
|
|
438
444
|
|
|
@@ -458,3 +464,5 @@ For a workflow-backed action, deploy or run the workflow smoke before wiring it
|
|
|
458
464
|
pnpm -C operations exec elevasis-sdk check
|
|
459
465
|
pnpm -C operations exec elevasis-sdk deploy
|
|
460
466
|
```
|
|
467
|
+
|
|
468
|
+
A compiled catalog that silently comes back shorter than expected means a record was skipped for want of `availableFrom`; a `SYSTEM_INTERFACE_INVALID` throw means a `stageKeys`/`stateKeys` entry names something the pipeline catalog does not define, or a record declares `availableFrom` with no `resourceId`. Both are reported with the offending ontology id.
|
|
@@ -103,7 +103,9 @@ System field reference:
|
|
|
103
103
|
- `actions` -- references to the cross-cutting actions domain.
|
|
104
104
|
- `ontology` -- System-owned object, link, action, catalog, event, surface, interface, value-type, property, or group records.
|
|
105
105
|
- `config` -- JSON-serializable settings local to this System.
|
|
106
|
-
- `systems` -- nested child Systems. Use this for new recursive authoring; `subsystems` is a
|
|
106
|
+
- `systems` -- nested child Systems. Use this for new recursive authoring; `subsystems` is a retired spelling, accepted on input only.
|
|
107
|
+
|
|
108
|
+
**A parsed model no longer carries children under both keys.** Parsing used to mirror `systems` into `subsystems`, so either spelling could be read off any parsed System. That mirror is gone: children now live under whichever key the author wrote. Anything walking the tree must read `system.systems ?? system.subsystems` -- reading `subsystems` alone looked correct only because the mirror was filling it in.
|
|
107
109
|
|
|
108
110
|
## Ontology
|
|
109
111
|
|