@synap-core/workspace-templates 0.6.4 → 0.7.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/index.d.ts +216 -1
- package/dist/index.js +3501 -1622
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/business-developer.template.test.ts +280 -0
- package/src/business-developer.yaml +644 -0
- package/src/composition-graph.test.ts +135 -0
- package/src/composition-graph.ts +298 -0
- package/src/crm.yaml +38 -58
- package/src/define.flow-automations.test.ts +65 -0
- package/src/define.ts +95 -7
- package/src/ecosystem.yaml +24 -17
- package/src/index.ts +15 -0
- package/src/meetings-knowledge.yaml +204 -0
- package/src/outreach-comms.yaml +282 -0
- package/src/templates.ts +2999 -1288
- package/src/types.ts +106 -3
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
# Business Developer — a composable, lead-first CRM operating model.
|
|
2
|
+
#
|
|
3
|
+
# This is an ADDITIVE overlay on the CRM workspace, not a second CRM and not a
|
|
4
|
+
# rewrite of shared identity profiles. It deliberately uses workspace-scoped
|
|
5
|
+
# `crm-lead` and `crm-client` role facets instead of the pod-wide `lead` and
|
|
6
|
+
# `client` vocabulary from Foundation. A person or company can therefore be a
|
|
7
|
+
# lead in one CRM workspace without becoming a lead everywhere in the pod.
|
|
8
|
+
#
|
|
9
|
+
# Lifecycle:
|
|
10
|
+
# person / company + crm-lead → optional deal(s) → won deal → crm-client
|
|
11
|
+
#
|
|
12
|
+
# Deals are commercial proposals attached through CRM's existing
|
|
13
|
+
# `linked_to_deal` relation. The CRM requires one counterparty before a deal
|
|
14
|
+
# can be won, but a counterparty can have more than one proposal over time. A
|
|
15
|
+
# client facet is only added when a deal is won; post-sale delivery belongs to
|
|
16
|
+
# another application.
|
|
17
|
+
|
|
18
|
+
meta:
|
|
19
|
+
slug: business-developer
|
|
20
|
+
name: Business Developer
|
|
21
|
+
description: "A lead-first sales motion for a CRM workspace: qualify leads, work commercial proposals, and hand won clients to the next system."
|
|
22
|
+
icon: target
|
|
23
|
+
color: "#D97706"
|
|
24
|
+
tags: [crm, sales, lead-management, business-development]
|
|
25
|
+
isPublic: true
|
|
26
|
+
|
|
27
|
+
# Compose onto an existing CRM workspace. The resolver installs CRM first, then
|
|
28
|
+
# applies these profiles and relations to that same workspace.
|
|
29
|
+
dependencies:
|
|
30
|
+
- slug: crm
|
|
31
|
+
kind: workspace
|
|
32
|
+
relation: compose
|
|
33
|
+
reason: "Adds a lead-first business-development workflow to your CRM workspace."
|
|
34
|
+
|
|
35
|
+
workspace:
|
|
36
|
+
name: CRM
|
|
37
|
+
description: A focused pre-sale workspace for lead qualification and commercial proposals.
|
|
38
|
+
subtype: crm
|
|
39
|
+
capabilities:
|
|
40
|
+
- crm.business-development
|
|
41
|
+
|
|
42
|
+
profiles:
|
|
43
|
+
# Workspace-local operational status. This is intentionally separate from
|
|
44
|
+
# Foundation's shared `lead` role: lead ownership, next actions, and CRM
|
|
45
|
+
# lifecycle status must be visible to this CRM workspace's members only.
|
|
46
|
+
- slug: crm-lead
|
|
47
|
+
displayName: CRM Lead
|
|
48
|
+
icon: user-round-search
|
|
49
|
+
color: amber
|
|
50
|
+
description: A person or company actively being qualified or contacted by this CRM workspace.
|
|
51
|
+
scope: WORKSPACE
|
|
52
|
+
profileKind: role
|
|
53
|
+
applicableKinds: [person, company]
|
|
54
|
+
properties:
|
|
55
|
+
- slug: lifecycleStage
|
|
56
|
+
label: Lifecycle stage
|
|
57
|
+
valueType: string
|
|
58
|
+
inputType: select
|
|
59
|
+
enumValues: [new, researching, outreach, engaged, qualified, nurturing, disqualified, converted]
|
|
60
|
+
constraints: { defaultValue: new }
|
|
61
|
+
- slug: assignedUserId
|
|
62
|
+
label: Assigned user ID
|
|
63
|
+
valueType: string
|
|
64
|
+
inputType: text
|
|
65
|
+
placeholder: Workspace member ID
|
|
66
|
+
- slug: source
|
|
67
|
+
label: Source
|
|
68
|
+
valueType: string
|
|
69
|
+
inputType: select
|
|
70
|
+
enumValues: [manual, referral, linkedin, email, event, inbound, partner, import, other]
|
|
71
|
+
- slug: icpScore
|
|
72
|
+
label: ICP score
|
|
73
|
+
valueType: number
|
|
74
|
+
inputType: number
|
|
75
|
+
- slug: outreachChannels
|
|
76
|
+
label: Outreach channels
|
|
77
|
+
valueType: array
|
|
78
|
+
inputType: text
|
|
79
|
+
placeholder: linkedin, email, phone
|
|
80
|
+
- slug: lastContactedAt
|
|
81
|
+
label: Last contacted at
|
|
82
|
+
valueType: date
|
|
83
|
+
inputType: date
|
|
84
|
+
- slug: nextActionAt
|
|
85
|
+
label: Next action at
|
|
86
|
+
valueType: date
|
|
87
|
+
inputType: date
|
|
88
|
+
- slug: nextAction
|
|
89
|
+
label: Next action
|
|
90
|
+
valueType: string
|
|
91
|
+
inputType: textarea
|
|
92
|
+
- slug: qualificationNotes
|
|
93
|
+
label: Qualification notes
|
|
94
|
+
valueType: string
|
|
95
|
+
inputType: textarea
|
|
96
|
+
- slug: disqualificationReason
|
|
97
|
+
label: Disqualification reason
|
|
98
|
+
valueType: string
|
|
99
|
+
inputType: textarea
|
|
100
|
+
- slug: convertedAt
|
|
101
|
+
label: Converted at
|
|
102
|
+
valueType: date
|
|
103
|
+
inputType: date
|
|
104
|
+
|
|
105
|
+
# The same counterparty receives this facet when one of its commercial
|
|
106
|
+
# proposals is won. It intentionally carries only handoff context; client
|
|
107
|
+
# delivery, renewal, and service health remain outside this pre-sale addon.
|
|
108
|
+
- slug: crm-client
|
|
109
|
+
displayName: CRM Client
|
|
110
|
+
icon: badge-check
|
|
111
|
+
color: emerald
|
|
112
|
+
description: A CRM lead converted by a won commercial proposal and ready for handoff.
|
|
113
|
+
scope: WORKSPACE
|
|
114
|
+
profileKind: role
|
|
115
|
+
applicableKinds: [person, company]
|
|
116
|
+
properties:
|
|
117
|
+
- slug: becameClientAt
|
|
118
|
+
label: Became client at
|
|
119
|
+
valueType: date
|
|
120
|
+
inputType: date
|
|
121
|
+
- slug: handoffStatus
|
|
122
|
+
label: Handoff status
|
|
123
|
+
valueType: string
|
|
124
|
+
inputType: select
|
|
125
|
+
enumValues: [pending, ready, handed_off]
|
|
126
|
+
constraints: { defaultValue: pending }
|
|
127
|
+
- slug: handoffDestination
|
|
128
|
+
label: Handoff destination
|
|
129
|
+
valueType: string
|
|
130
|
+
inputType: text
|
|
131
|
+
- slug: handoffNotes
|
|
132
|
+
label: Handoff notes
|
|
133
|
+
valueType: string
|
|
134
|
+
inputType: textarea
|
|
135
|
+
|
|
136
|
+
# CRM's original deal profile remains the generic compatibility layer. These
|
|
137
|
+
# fields are the additive commercial contract used by this addon; applications
|
|
138
|
+
# that opt into Business Developer read them rather than treating a deal board
|
|
139
|
+
# as the lead pipeline.
|
|
140
|
+
- slug: deal
|
|
141
|
+
displayName: Deal
|
|
142
|
+
icon: git-branch
|
|
143
|
+
color: violet
|
|
144
|
+
properties:
|
|
145
|
+
- slug: commercialStage
|
|
146
|
+
label: Commercial stage
|
|
147
|
+
valueType: string
|
|
148
|
+
inputType: select
|
|
149
|
+
enumValues: [draft, proposed, negotiating, won, lost]
|
|
150
|
+
constraints: { defaultValue: draft }
|
|
151
|
+
- slug: synapFeeModel
|
|
152
|
+
label: Synap fee model
|
|
153
|
+
valueType: string
|
|
154
|
+
inputType: select
|
|
155
|
+
enumValues: [fixed, success_fee]
|
|
156
|
+
- slug: synapFixedFee
|
|
157
|
+
label: Synap fixed fee
|
|
158
|
+
valueType: number
|
|
159
|
+
inputType: number
|
|
160
|
+
- slug: expectedClientGain
|
|
161
|
+
label: Expected client gain
|
|
162
|
+
valueType: number
|
|
163
|
+
inputType: number
|
|
164
|
+
- slug: synapSuccessFeeRate
|
|
165
|
+
label: Synap success fee rate
|
|
166
|
+
valueType: number
|
|
167
|
+
inputType: number
|
|
168
|
+
- slug: sourcedByUserId
|
|
169
|
+
label: Sourced by user ID
|
|
170
|
+
valueType: string
|
|
171
|
+
inputType: text
|
|
172
|
+
- slug: synapFeeAmount
|
|
173
|
+
label: Synap fee amount
|
|
174
|
+
valueType: number
|
|
175
|
+
inputType: number
|
|
176
|
+
- slug: commissionEntitlementId
|
|
177
|
+
label: Commission entitlement
|
|
178
|
+
valueType: entity_id
|
|
179
|
+
targetProfileSlug: crm-commission-entitlement
|
|
180
|
+
inputType: entity_select
|
|
181
|
+
- slug: commissionStatus
|
|
182
|
+
label: Commission status
|
|
183
|
+
valueType: string
|
|
184
|
+
inputType: select
|
|
185
|
+
enumValues: [estimated, earned, paid, requires_review, void]
|
|
186
|
+
- slug: wonAt
|
|
187
|
+
label: Won at
|
|
188
|
+
valueType: date
|
|
189
|
+
inputType: date
|
|
190
|
+
- slug: lostReason
|
|
191
|
+
label: Lost reason
|
|
192
|
+
valueType: string
|
|
193
|
+
inputType: textarea
|
|
194
|
+
|
|
195
|
+
# Immutable-at-creation commission records make first- and repeat-deal
|
|
196
|
+
# commissions auditable. The CRM creates one on a win: 20% of Synap's fee for
|
|
197
|
+
# the counterparty's first won deal, then 10% for later won deals.
|
|
198
|
+
- slug: crm-commission-entitlement
|
|
199
|
+
displayName: Commission Entitlement
|
|
200
|
+
icon: receipt-text
|
|
201
|
+
color: green
|
|
202
|
+
description: An auditable business-developer commission created from a won deal.
|
|
203
|
+
scope: WORKSPACE
|
|
204
|
+
properties:
|
|
205
|
+
# Correlates this record with its originating deal. The configured
|
|
206
|
+
# handoff automation uses it as its idempotency key, so a resumed run
|
|
207
|
+
# cannot create a second entitlement after a partial write.
|
|
208
|
+
- slug: sourceDealId
|
|
209
|
+
label: Source deal ID
|
|
210
|
+
valueType: entity_id
|
|
211
|
+
targetProfileSlug: deal
|
|
212
|
+
inputType: entity_select
|
|
213
|
+
- slug: sourcedByUserId
|
|
214
|
+
label: Sourced by user ID
|
|
215
|
+
valueType: string
|
|
216
|
+
inputType: text
|
|
217
|
+
- slug: sequence
|
|
218
|
+
label: Deal sequence
|
|
219
|
+
valueType: string
|
|
220
|
+
inputType: select
|
|
221
|
+
enumValues: [first, subsequent]
|
|
222
|
+
- slug: rateBps
|
|
223
|
+
label: Commission rate (basis points)
|
|
224
|
+
valueType: number
|
|
225
|
+
inputType: number
|
|
226
|
+
- slug: feeBasisAmount
|
|
227
|
+
label: Synap fee basis amount
|
|
228
|
+
valueType: number
|
|
229
|
+
inputType: number
|
|
230
|
+
- slug: estimatedAmount
|
|
231
|
+
label: Estimated commission amount
|
|
232
|
+
valueType: number
|
|
233
|
+
inputType: number
|
|
234
|
+
- slug: status
|
|
235
|
+
label: Status
|
|
236
|
+
valueType: string
|
|
237
|
+
inputType: select
|
|
238
|
+
enumValues: [estimated, earned, paid, void]
|
|
239
|
+
constraints: { defaultValue: estimated }
|
|
240
|
+
- slug: earnedAt
|
|
241
|
+
label: Earned at
|
|
242
|
+
valueType: date
|
|
243
|
+
inputType: date
|
|
244
|
+
- slug: paidAt
|
|
245
|
+
label: Paid at
|
|
246
|
+
valueType: date
|
|
247
|
+
inputType: date
|
|
248
|
+
- slug: voidReason
|
|
249
|
+
label: Void reason
|
|
250
|
+
valueType: string
|
|
251
|
+
inputType: textarea
|
|
252
|
+
|
|
253
|
+
entityLinks:
|
|
254
|
+
# The existing generic `linked_to_deal` relation remains the deal ↔
|
|
255
|
+
# counterparty connection. This overlay adds only the new auditable
|
|
256
|
+
# commission relation, avoiding a parallel relationship model.
|
|
257
|
+
- sourceProfileSlug: deal
|
|
258
|
+
targetProfileSlug: crm-commission-entitlement
|
|
259
|
+
type: crm_commission_for_deal
|
|
260
|
+
label: Commission entitlement
|
|
261
|
+
|
|
262
|
+
# These are guided, proposal-first sessions — not background automations. They
|
|
263
|
+
# never create a deal merely because a lead exists, and never convert a lead to
|
|
264
|
+
# a client until a human-confirmed deal win is proposed and applied.
|
|
265
|
+
playbooks:
|
|
266
|
+
- name: Qualify a CRM lead
|
|
267
|
+
description: Research and qualify one CRM lead, then make the next action explicit without creating a commercial proposal automatically.
|
|
268
|
+
goalTemplate: >-
|
|
269
|
+
Work on CRM lead ({lead}). Verify and enrich only factual information that
|
|
270
|
+
can be supported, assess fit, and update its CRM lifecycle stage, ICP score,
|
|
271
|
+
qualification notes, outreach channels, and next action. Do not create a
|
|
272
|
+
deal simply because the lead is qualified. If a commercial proposal is
|
|
273
|
+
warranted, explain why and ask for or propose that next step separately.
|
|
274
|
+
subjectProfile:
|
|
275
|
+
profileSlug: crm-lead
|
|
276
|
+
params:
|
|
277
|
+
- { name: lead, type: entity, label: CRM lead, required: true }
|
|
278
|
+
executor: is-agent
|
|
279
|
+
status: active
|
|
280
|
+
|
|
281
|
+
- name: Advance a commercial proposal
|
|
282
|
+
description: Help progress a linked deal without confusing commercial state with the lead pipeline or client conversion.
|
|
283
|
+
goalTemplate: >-
|
|
284
|
+
Work on commercial proposal ({deal}). Confirm its primary counterparty,
|
|
285
|
+
current commercial stage, fee model, expected fee, and next blocker. Propose
|
|
286
|
+
the single next action needed to advance it. Never mark the counterparty as
|
|
287
|
+
a CRM client unless the deal is genuinely won; a win must preserve the
|
|
288
|
+
sourced business developer and create the appropriate commission entitlement
|
|
289
|
+
(20% of Synap's fee for the first won deal with that counterparty, 10% later).
|
|
290
|
+
subjectProfile:
|
|
291
|
+
profileSlug: deal
|
|
292
|
+
params:
|
|
293
|
+
- { name: deal, type: entity, label: Commercial proposal, required: true }
|
|
294
|
+
executor: is-agent
|
|
295
|
+
status: active
|
|
296
|
+
|
|
297
|
+
# The deterministic counterpart to the guided playbooks above. This runs only
|
|
298
|
+
# after a human explicitly confirms the commercial win. It remains entirely
|
|
299
|
+
# domain-configured: generic workflow nodes read, validate, calculate, and
|
|
300
|
+
# write through the Pod's governed facet/relation/entity doors.
|
|
301
|
+
automations:
|
|
302
|
+
- key: crm-win-and-handoff
|
|
303
|
+
name: Win and hand off
|
|
304
|
+
description: Convert one qualified CRM lead after its proposal is won, record its commission entitlement, and leave a resumable handoff trail.
|
|
305
|
+
triggerType: manual
|
|
306
|
+
triggerConfig: {}
|
|
307
|
+
status: active
|
|
308
|
+
flow:
|
|
309
|
+
nodes:
|
|
310
|
+
- id: trigger
|
|
311
|
+
type: trigger
|
|
312
|
+
position: { x: 0, y: 0 }
|
|
313
|
+
data:
|
|
314
|
+
label: Mark won and hand off
|
|
315
|
+
triggerType: manual
|
|
316
|
+
config: {}
|
|
317
|
+
|
|
318
|
+
- id: read-deal
|
|
319
|
+
type: entity_read
|
|
320
|
+
position: { x: 240, y: 0 }
|
|
321
|
+
data:
|
|
322
|
+
label: Read proposal
|
|
323
|
+
entityId: "{{trigger.payload.entityId}}"
|
|
324
|
+
|
|
325
|
+
- id: guard-deal
|
|
326
|
+
type: guard
|
|
327
|
+
position: { x: 480, y: 0 }
|
|
328
|
+
data:
|
|
329
|
+
label: Confirm proposal is ready
|
|
330
|
+
checks:
|
|
331
|
+
- path: steps.read-deal.output.entity.type
|
|
332
|
+
equals: deal
|
|
333
|
+
message: This action is only available for a commercial proposal.
|
|
334
|
+
- path: steps.read-deal.output.entity.properties.commercialStage
|
|
335
|
+
anyOf:
|
|
336
|
+
- { path: steps.read-deal.output.entity.properties.commercialStage, equals: won }
|
|
337
|
+
- { path: steps.read-deal.output.entity.properties.dealStage, equals: won }
|
|
338
|
+
message: Mark the proposal as won before handing it off.
|
|
339
|
+
- path: steps.read-deal.output.entity.properties.handoffStatus
|
|
340
|
+
notEquals: handed_off
|
|
341
|
+
message: This proposal has already been handed off.
|
|
342
|
+
- path: steps.read-deal.output.entity.properties.sourcedByUserId
|
|
343
|
+
exists: true
|
|
344
|
+
message: Set the business developer who sourced this proposal before handing it off.
|
|
345
|
+
- path: steps.read-deal.output.entity.properties.synapFeeAmount
|
|
346
|
+
numberGte: 0
|
|
347
|
+
message: Set the confirmed Synap fee before handing this proposal off.
|
|
348
|
+
|
|
349
|
+
- id: find-party
|
|
350
|
+
type: related_entities
|
|
351
|
+
position: { x: 720, y: 0 }
|
|
352
|
+
data:
|
|
353
|
+
label: Find proposal counterparty
|
|
354
|
+
entityId: "{{trigger.payload.entityId}}"
|
|
355
|
+
direction: inbound
|
|
356
|
+
relationTypes: [linked_to_deal]
|
|
357
|
+
limit: 2
|
|
358
|
+
|
|
359
|
+
- id: guard-party
|
|
360
|
+
type: guard
|
|
361
|
+
position: { x: 960, y: 0 }
|
|
362
|
+
data:
|
|
363
|
+
label: Confirm one qualified counterparty
|
|
364
|
+
checks:
|
|
365
|
+
- path: steps.find-party.output.entities
|
|
366
|
+
lengthEquals: 1
|
|
367
|
+
message: A proposal must have exactly one commercial counterparty before it can be handed off.
|
|
368
|
+
|
|
369
|
+
- id: read-party
|
|
370
|
+
type: entity_read
|
|
371
|
+
position: { x: 1200, y: 0 }
|
|
372
|
+
data:
|
|
373
|
+
label: Read CRM lead
|
|
374
|
+
entityId: "{{steps.find-party.output.entities.0.id}}"
|
|
375
|
+
|
|
376
|
+
- id: guard-lead
|
|
377
|
+
type: guard
|
|
378
|
+
position: { x: 1440, y: 0 }
|
|
379
|
+
data:
|
|
380
|
+
label: Confirm qualified CRM lead
|
|
381
|
+
checks:
|
|
382
|
+
- path: steps.read-party.output.entity.facetSlugs
|
|
383
|
+
arrayIncludes: crm-lead
|
|
384
|
+
message: The commercial counterparty must still be a CRM lead before it becomes a CRM client.
|
|
385
|
+
|
|
386
|
+
# Source-deal claim is the durable idempotency boundary for the whole
|
|
387
|
+
# financial handoff. It must happen before the first-deal decision so
|
|
388
|
+
# duplicate clicks cannot reserve different commission policies.
|
|
389
|
+
- id: claim-commission-entitlement
|
|
390
|
+
type: claim
|
|
391
|
+
position: { x: 1680, y: 0 }
|
|
392
|
+
data:
|
|
393
|
+
label: Reserve commission entitlement
|
|
394
|
+
namespace: crm-commission-entitlement
|
|
395
|
+
key: "{{trigger.payload.entityId}}"
|
|
396
|
+
|
|
397
|
+
- id: guard-commission-claim
|
|
398
|
+
type: guard
|
|
399
|
+
position: { x: 1920, y: 0 }
|
|
400
|
+
data:
|
|
401
|
+
label: Confirm this run owns the commission write
|
|
402
|
+
checks:
|
|
403
|
+
- path: steps.claim-commission-entitlement.output.claimed
|
|
404
|
+
equals: true
|
|
405
|
+
message: This proposal handoff is already being handled by another workflow run.
|
|
406
|
+
|
|
407
|
+
# A durable generic claim serializes the one-time policy decision. It
|
|
408
|
+
# prevents two concurrent wins for the same counterparty from both
|
|
409
|
+
# receiving the first-deal rate, while retries of the winning run keep
|
|
410
|
+
# their original decision.
|
|
411
|
+
- id: claim-first-client-deal
|
|
412
|
+
type: claim
|
|
413
|
+
position: { x: 2160, y: 0 }
|
|
414
|
+
data:
|
|
415
|
+
label: Reserve first-client commission rate
|
|
416
|
+
namespace: crm-commission-first-deal
|
|
417
|
+
key: "{{steps.read-party.output.entity.id}}"
|
|
418
|
+
|
|
419
|
+
# Existing pods may already have won work from before this add-on. Read
|
|
420
|
+
# that history before applying the claim policy, including the legacy
|
|
421
|
+
# `dealStage` field until old CRM records are migrated.
|
|
422
|
+
- id: prior-won-deals
|
|
423
|
+
type: related_entities
|
|
424
|
+
position: { x: 2400, y: 0 }
|
|
425
|
+
data:
|
|
426
|
+
label: Find earlier won proposals
|
|
427
|
+
entityId: "{{steps.read-party.output.entity.id}}"
|
|
428
|
+
direction: outbound
|
|
429
|
+
relationTypes: [linked_to_deal]
|
|
430
|
+
excludeEntityId: "{{trigger.payload.entityId}}"
|
|
431
|
+
propertyAnyEquals:
|
|
432
|
+
commercialStage: [won]
|
|
433
|
+
dealStage: [won]
|
|
434
|
+
limit: 100
|
|
435
|
+
|
|
436
|
+
- id: has-no-prior-won-deals
|
|
437
|
+
type: condition
|
|
438
|
+
position: { x: 2640, y: 0 }
|
|
439
|
+
data:
|
|
440
|
+
label: Is this the first historical win?
|
|
441
|
+
expression: steps.prior-won-deals.output.count === 0
|
|
442
|
+
|
|
443
|
+
- id: is-first-client-deal
|
|
444
|
+
type: compute
|
|
445
|
+
position: { x: 2880, y: 120 }
|
|
446
|
+
data:
|
|
447
|
+
label: Confirm first-client commission
|
|
448
|
+
operation: multiply
|
|
449
|
+
left: "{{steps.claim-first-client-deal.output.claimed}}"
|
|
450
|
+
right: "{{steps.has-no-prior-won-deals.output.result}}"
|
|
451
|
+
|
|
452
|
+
# 1,000 bps for every deal, plus 1,000 bps for the winning first-deal claim.
|
|
453
|
+
# Number(true) is deliberately supported by the generic compute node,
|
|
454
|
+
# keeping the first/subsequent policy in config rather than application code.
|
|
455
|
+
- id: first-deal-bonus-bps
|
|
456
|
+
type: compute
|
|
457
|
+
position: { x: 2880, y: 0 }
|
|
458
|
+
data:
|
|
459
|
+
label: Calculate first-deal bonus
|
|
460
|
+
operation: multiply
|
|
461
|
+
left: 1000
|
|
462
|
+
right: "{{steps.is-first-client-deal.output.result}}"
|
|
463
|
+
|
|
464
|
+
- id: commission-sequence
|
|
465
|
+
type: select
|
|
466
|
+
position: { x: 3120, y: 0 }
|
|
467
|
+
data:
|
|
468
|
+
label: Label commission sequence
|
|
469
|
+
when: "{{steps.is-first-client-deal.output.result}}"
|
|
470
|
+
ifTrue: first
|
|
471
|
+
ifFalse: subsequent
|
|
472
|
+
|
|
473
|
+
- id: commission-rate-bps
|
|
474
|
+
type: compute
|
|
475
|
+
position: { x: 3360, y: 0 }
|
|
476
|
+
data:
|
|
477
|
+
label: Calculate commission rate
|
|
478
|
+
operation: add
|
|
479
|
+
left: 1000
|
|
480
|
+
right: "{{steps.first-deal-bonus-bps.output.result}}"
|
|
481
|
+
|
|
482
|
+
- id: commission-product
|
|
483
|
+
type: compute
|
|
484
|
+
position: { x: 3600, y: 0 }
|
|
485
|
+
data:
|
|
486
|
+
label: Calculate commission basis points
|
|
487
|
+
operation: multiply
|
|
488
|
+
left: "{{steps.read-deal.output.entity.properties.synapFeeAmount}}"
|
|
489
|
+
right: "{{steps.commission-rate-bps.output.result}}"
|
|
490
|
+
|
|
491
|
+
- id: commission-amount
|
|
492
|
+
type: compute
|
|
493
|
+
position: { x: 3840, y: 0 }
|
|
494
|
+
data:
|
|
495
|
+
label: Calculate commission amount
|
|
496
|
+
operation: divide
|
|
497
|
+
left: "{{steps.commission-product.output.result}}"
|
|
498
|
+
right: 10000
|
|
499
|
+
|
|
500
|
+
- id: handoff-time
|
|
501
|
+
type: compute
|
|
502
|
+
position: { x: 4080, y: 0 }
|
|
503
|
+
data:
|
|
504
|
+
label: Record handoff time
|
|
505
|
+
operation: now
|
|
506
|
+
|
|
507
|
+
- id: create-commission
|
|
508
|
+
type: output
|
|
509
|
+
position: { x: 4320, y: 0 }
|
|
510
|
+
data:
|
|
511
|
+
label: Record commission entitlement
|
|
512
|
+
outputType: entity_create
|
|
513
|
+
config:
|
|
514
|
+
profileSlug: crm-commission-entitlement
|
|
515
|
+
title: "Commission · {{steps.read-deal.output.entity.title}}"
|
|
516
|
+
# The durable claim above serializes creators; this lookup heals a
|
|
517
|
+
# crash after the entity write but before its step ledger update.
|
|
518
|
+
dedupeBy: sourceDealId
|
|
519
|
+
properties:
|
|
520
|
+
sourceDealId: "{{trigger.payload.entityId}}"
|
|
521
|
+
sourcedByUserId: "{{steps.read-deal.output.entity.properties.sourcedByUserId}}"
|
|
522
|
+
sequence: "{{steps.commission-sequence.output.value}}"
|
|
523
|
+
rateBps: "{{steps.commission-rate-bps.output.result}}"
|
|
524
|
+
feeBasisAmount: "{{steps.read-deal.output.entity.properties.synapFeeAmount}}"
|
|
525
|
+
estimatedAmount: "{{steps.commission-amount.output.result}}"
|
|
526
|
+
status: estimated
|
|
527
|
+
earnedAt: "{{steps.handoff-time.output.result}}"
|
|
528
|
+
|
|
529
|
+
- id: link-commission
|
|
530
|
+
type: output
|
|
531
|
+
position: { x: 4560, y: 0 }
|
|
532
|
+
data:
|
|
533
|
+
label: Link proposal and commission
|
|
534
|
+
outputType: relation_create
|
|
535
|
+
config:
|
|
536
|
+
fromEntityId: "{{trigger.payload.entityId}}"
|
|
537
|
+
toEntityId: "{{steps.create-commission.output.entityId}}"
|
|
538
|
+
relationType: crm_commission_for_deal
|
|
539
|
+
dedupe: true
|
|
540
|
+
|
|
541
|
+
- id: mark-deal-won
|
|
542
|
+
type: output
|
|
543
|
+
position: { x: 4800, y: 0 }
|
|
544
|
+
data:
|
|
545
|
+
label: Mark proposal won
|
|
546
|
+
outputType: entity_update
|
|
547
|
+
config:
|
|
548
|
+
entityId: "{{trigger.payload.entityId}}"
|
|
549
|
+
properties:
|
|
550
|
+
commercialStage: won
|
|
551
|
+
wonAt: "{{steps.handoff-time.output.result}}"
|
|
552
|
+
handoffStatus: handed_off
|
|
553
|
+
commissionEntitlementId: "{{steps.create-commission.output.entityId}}"
|
|
554
|
+
commissionStatus: estimated
|
|
555
|
+
|
|
556
|
+
- id: attach-client
|
|
557
|
+
type: output
|
|
558
|
+
position: { x: 5040, y: 0 }
|
|
559
|
+
data:
|
|
560
|
+
label: Attach CRM client role
|
|
561
|
+
outputType: facet_attach
|
|
562
|
+
config:
|
|
563
|
+
entityId: "{{steps.read-party.output.entity.id}}"
|
|
564
|
+
facetSlug: crm-client
|
|
565
|
+
properties:
|
|
566
|
+
becameClientAt: "{{steps.handoff-time.output.result}}"
|
|
567
|
+
handoffStatus: handed_off
|
|
568
|
+
|
|
569
|
+
- id: convert-lead
|
|
570
|
+
type: output
|
|
571
|
+
position: { x: 5280, y: 0 }
|
|
572
|
+
data:
|
|
573
|
+
label: Complete CRM lead lifecycle
|
|
574
|
+
outputType: facet_update
|
|
575
|
+
config:
|
|
576
|
+
entityId: "{{steps.read-party.output.entity.id}}"
|
|
577
|
+
facetSlug: crm-lead
|
|
578
|
+
properties:
|
|
579
|
+
lifecycleStage: converted
|
|
580
|
+
convertedAt: "{{steps.handoff-time.output.result}}"
|
|
581
|
+
|
|
582
|
+
edges:
|
|
583
|
+
- { id: trigger-read, source: trigger, target: read-deal }
|
|
584
|
+
- { id: read-guard, source: read-deal, target: guard-deal }
|
|
585
|
+
- { id: guard-party-search, source: guard-deal, target: find-party }
|
|
586
|
+
- { id: party-search-guard, source: find-party, target: guard-party }
|
|
587
|
+
- { id: party-guard-read, source: guard-party, target: read-party }
|
|
588
|
+
- { id: party-read-guard, source: read-party, target: guard-lead }
|
|
589
|
+
- { id: lead-guard-commission-claim, source: guard-lead, target: claim-commission-entitlement }
|
|
590
|
+
- { id: commission-claim-guard, source: claim-commission-entitlement, target: guard-commission-claim }
|
|
591
|
+
- { id: commission-guard-first-claim, source: guard-commission-claim, target: claim-first-client-deal }
|
|
592
|
+
- { id: first-claim-history, source: claim-first-client-deal, target: prior-won-deals }
|
|
593
|
+
- { id: history-condition, source: prior-won-deals, target: has-no-prior-won-deals }
|
|
594
|
+
- { id: first-confirm-bonus, source: is-first-client-deal, target: first-deal-bonus-bps }
|
|
595
|
+
- { id: first-history-confirm, source: has-no-prior-won-deals, target: is-first-client-deal }
|
|
596
|
+
- { id: first-confirm-sequence, source: is-first-client-deal, target: commission-sequence }
|
|
597
|
+
- { id: bonus-rate, source: first-deal-bonus-bps, target: commission-rate-bps }
|
|
598
|
+
- { id: rate-product, source: commission-rate-bps, target: commission-product }
|
|
599
|
+
- { id: product-amount, source: commission-product, target: commission-amount }
|
|
600
|
+
- { id: amount-time, source: commission-amount, target: handoff-time }
|
|
601
|
+
- { id: time-commission, source: handoff-time, target: create-commission }
|
|
602
|
+
- { id: sequence-commission, source: commission-sequence, target: create-commission }
|
|
603
|
+
- { id: commission-link, source: create-commission, target: link-commission }
|
|
604
|
+
- { id: link-deal, source: link-commission, target: mark-deal-won }
|
|
605
|
+
- { id: deal-client, source: mark-deal-won, target: attach-client }
|
|
606
|
+
- { id: client-lead, source: attach-client, target: convert-lead }
|
|
607
|
+
|
|
608
|
+
# Rendered by both CRM and Browser through the shared entity-action runner.
|
|
609
|
+
# The lead facet requirement keeps the qualification playbook out of general
|
|
610
|
+
# person/company records; the commercial action is explicitly confirmed because
|
|
611
|
+
# it writes a client handoff and commission record.
|
|
612
|
+
actionPlacements:
|
|
613
|
+
- profileSlug: person
|
|
614
|
+
surface: entity-detail
|
|
615
|
+
kind: playbook
|
|
616
|
+
ref: Qualify a CRM lead
|
|
617
|
+
label: Qualify lead
|
|
618
|
+
when: { requiredFacetSlugs: [crm-lead] }
|
|
619
|
+
|
|
620
|
+
- profileSlug: company
|
|
621
|
+
surface: entity-detail
|
|
622
|
+
kind: playbook
|
|
623
|
+
ref: Qualify a CRM lead
|
|
624
|
+
label: Qualify lead
|
|
625
|
+
when: { requiredFacetSlugs: [crm-lead] }
|
|
626
|
+
|
|
627
|
+
- profileSlug: deal
|
|
628
|
+
surface: entity-detail
|
|
629
|
+
kind: playbook
|
|
630
|
+
ref: Advance a commercial proposal
|
|
631
|
+
label: Advance proposal
|
|
632
|
+
|
|
633
|
+
- profileSlug: deal
|
|
634
|
+
surface: entity-detail
|
|
635
|
+
kind: automation
|
|
636
|
+
ref: Win and hand off
|
|
637
|
+
label: Hand off won proposal
|
|
638
|
+
when:
|
|
639
|
+
propertyAnyEquals: { commercialStage: [won], dealStage: [won] }
|
|
640
|
+
propertyNotEquals: { handoffStatus: handed_off }
|
|
641
|
+
confirmation:
|
|
642
|
+
title: Hand off this won proposal?
|
|
643
|
+
description: This converts its qualified CRM lead counterparty and records the business developer commission.
|
|
644
|
+
confirmLabel: Hand off proposal
|