@synap-core/workspace-templates 0.9.4 → 0.11.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/dist/index.d.ts +154 -45
- package/dist/index.js +2462 -543
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__fixtures__/default-relation-defs.json +208 -0
- package/src/__fixtures__/report-automation-flow.json +393 -0
- package/src/agent-fleet.yaml +6 -1
- package/src/base.template.test.ts +160 -0
- package/src/base.yaml +839 -0
- package/src/blockchain-ecosystem.yaml +106 -0
- package/src/business-developer.template.test.ts +79 -10
- package/src/business-developer.yaml +61 -39
- package/src/catalog.ts +36 -0
- package/src/concept-guard.test.ts +47 -31
- package/src/concept-guard.ts +23 -19
- package/src/content-os.yaml +22 -0
- package/src/corpus-flow-validity.test.ts +130 -0
- package/src/crm.yaml +59 -8
- package/src/define.primary-surface.test.ts +2 -4
- package/src/define.ts +28 -0
- package/src/ecosystem.yaml +14 -0
- package/src/enterprise-os.yaml +1 -1
- package/src/foundation.yaml +8 -0
- package/src/grants.yaml +256 -1
- package/src/life-os.yaml +6 -1
- package/src/networking.yaml +14 -3
- package/src/operations.yaml +270 -0
- package/src/outreach-comms.yaml +196 -0
- package/src/project-management.yaml +6 -1
- package/src/templates.ts +1770 -129
- package/src/the-arch.yaml +4 -0
- package/src/types.ts +98 -34
- package/src/validate-flow.ts +186 -7
- package/src/validate.ts +26 -2
package/src/outreach-comms.yaml
CHANGED
|
@@ -203,6 +203,202 @@ playbooks:
|
|
|
203
203
|
- entity.create
|
|
204
204
|
status: active
|
|
205
205
|
|
|
206
|
+
# ── LinkedIn conversation ingest (PURE-CONFIG automation — PROPOSAL) ─────────
|
|
207
|
+
# Pulls LinkedIn threads via Unipile and records each as an EXTERNAL channel +
|
|
208
|
+
# messages through the GENERIC `channel.ingest` verb. No provider code in the
|
|
209
|
+
# pod — this composes existing verbs (unipile_list_accounts /
|
|
210
|
+
# unipile_list_conversations / unipile_list_messages + channel.ingest).
|
|
211
|
+
#
|
|
212
|
+
# TRIGGER MODEL — ONE automation, both jobs (no high-freq polling):
|
|
213
|
+
# • RECONCILIATION: `triggerType: cron` at a LOW frequency (daily 06:00).
|
|
214
|
+
# • BACKFILL / on-demand: fire the SAME automation via
|
|
215
|
+
# `POST /automations/{id}/trigger` (MCP `synap_trigger_automation`), which
|
|
216
|
+
# "bypasses the automation's normal trigger config" and runs the flow once.
|
|
217
|
+
# channel.ingest is idempotent, so cron sweeps and manual backfills are safe to
|
|
218
|
+
# overlap and repeat.
|
|
219
|
+
#
|
|
220
|
+
# ⚠️ NEEDS-REVIEW — TWO verified engine constraints shape this flow; see the
|
|
221
|
+
# handoff report. (1) The automation engine has NO nested loops
|
|
222
|
+
# (LOOP_BODY_NODE_TYPES excludes `loop`/`sub_automation`), so the requested
|
|
223
|
+
# `loop conversations → loop messages → channel.ingest per message` graph is
|
|
224
|
+
# not expressible. This does ONE loop over conversations and hands each thread's
|
|
225
|
+
# whole `messages` array to channel.ingest in a single call. (2) A `transform`
|
|
226
|
+
# `map:` pipe can only project a single field/string per item, so it cannot
|
|
227
|
+
# normalize messages into `{text, sentAt, participant, idempotencySeed}` objects
|
|
228
|
+
# in-flow. => channel.ingest must accept a BATCH `messages: [...]` of raw
|
|
229
|
+
# provider rows and do the per-message normalization + idempotent dedup
|
|
230
|
+
# SERVER-SIDE. That is a contract change vs the single-message signature.
|
|
231
|
+
# Kept `status: draft` until that batch shape ships; then activate.
|
|
232
|
+
automations:
|
|
233
|
+
- key: linkedin-conversation-ingest
|
|
234
|
+
name: LinkedIn conversation ingest
|
|
235
|
+
description: >-
|
|
236
|
+
Pull LinkedIn conversations via Unipile and record each thread as an
|
|
237
|
+
external channel + its messages through channel.ingest (idempotent, so
|
|
238
|
+
re-runs are safe). Cron = low-frequency reconciliation; manual trigger =
|
|
239
|
+
on-demand backfill.
|
|
240
|
+
triggerType: cron
|
|
241
|
+
triggerConfig:
|
|
242
|
+
expression: "0 6 * * *"
|
|
243
|
+
status: draft
|
|
244
|
+
flow:
|
|
245
|
+
nodes:
|
|
246
|
+
- id: trigger
|
|
247
|
+
type: trigger
|
|
248
|
+
position: { x: 0, y: 0 }
|
|
249
|
+
data:
|
|
250
|
+
label: Daily 06:00 reconciliation (also fired manually for backfill)
|
|
251
|
+
triggerType: cron
|
|
252
|
+
config:
|
|
253
|
+
expression: "0 6 * * *"
|
|
254
|
+
|
|
255
|
+
# Resolve the connected account AT RUNTIME so the cron sweep needs no
|
|
256
|
+
# payload. A multi-account / multi-provider pod would still need a
|
|
257
|
+
# run-param accountId to sweep MORE than the first LinkedIn account — a
|
|
258
|
+
# per-account sweep would itself need the nested loop this engine lacks
|
|
259
|
+
# (secondary follow-up, noted in the report).
|
|
260
|
+
- id: list-accounts
|
|
261
|
+
type: capability
|
|
262
|
+
position: { x: 0, y: 120 }
|
|
263
|
+
data:
|
|
264
|
+
label: Resolve the connected Unipile account
|
|
265
|
+
verbId: unipile_list_accounts
|
|
266
|
+
|
|
267
|
+
# F5 — PROVIDER FILTER. `unipile_list_accounts` returns RAW Unipile v2
|
|
268
|
+
# account rows and a pod can connect several providers, so a non-LinkedIn
|
|
269
|
+
# account (WhatsApp / Telegram / Gmail) can sit at index 0. Taking a blind
|
|
270
|
+
# `accounts.0` would ingest that provider's threads AS linkedin. This
|
|
271
|
+
# `transform` node's array `filter:` pipe keeps ONLY LinkedIn accounts;
|
|
272
|
+
# every step below takes `.0` of the FILTERED list.
|
|
273
|
+
# Field: the predicate matches the RAW Unipile v2 account shape
|
|
274
|
+
# `type: 'LINKEDIN' | 'GMAIL' | 'WHATSAPP' | …` (verified in
|
|
275
|
+
# UnipileConnector's UnipileAccountSchema, NOT the capability's loose
|
|
276
|
+
# "provider" prose). evaluateCondition does an EXACT, case-sensitive
|
|
277
|
+
# `==`, hence the uppercase literal.
|
|
278
|
+
# Failure mode: if no LinkedIn account is connected (or the raw field
|
|
279
|
+
# shape ever drifts) the filtered list is empty → the accountId below
|
|
280
|
+
# resolves to "" → the run FAILS VISIBLY at list-conversations. It never
|
|
281
|
+
# silently ingests the wrong provider — that is the point of F5.
|
|
282
|
+
# Multi-LinkedIn-account: still takes the first; a run-param accountId
|
|
283
|
+
# is the multi-account follow-up (noted in the report).
|
|
284
|
+
- id: filter-linkedin-account
|
|
285
|
+
type: transform
|
|
286
|
+
position: { x: 0, y: 180 }
|
|
287
|
+
data:
|
|
288
|
+
label: Keep only LinkedIn Unipile accounts
|
|
289
|
+
expression: "{{steps.list-accounts.output.accounts}} | filter:item.type == 'LINKEDIN'"
|
|
290
|
+
|
|
291
|
+
- id: list-conversations
|
|
292
|
+
type: capability
|
|
293
|
+
position: { x: 0, y: 240 }
|
|
294
|
+
data:
|
|
295
|
+
label: List LinkedIn conversations for the account
|
|
296
|
+
verbId: unipile_list_conversations
|
|
297
|
+
inputMapping:
|
|
298
|
+
accountId: "{{steps.filter-linkedin-account.output.result.0.id}}"
|
|
299
|
+
|
|
300
|
+
# NOTE: the automation engine caps loops at MAX_LOOP_ITERATIONS=100 per run
|
|
301
|
+
# (logs a warning, then truncates). So one cron pass reconciles the 100
|
|
302
|
+
# most-recent conversations; accounts with >100 threads need either a
|
|
303
|
+
# newest-first list (so the active threads are always covered) or a paged
|
|
304
|
+
# sweep (blocked today — would need the nested loops the engine lacks).
|
|
305
|
+
# Fine for the current dogfood scale; revisit if a user exceeds ~100 threads.
|
|
306
|
+
- id: conversations-loop
|
|
307
|
+
type: loop
|
|
308
|
+
position: { x: 0, y: 360 }
|
|
309
|
+
data:
|
|
310
|
+
label: For each conversation
|
|
311
|
+
iteratorExpression: steps.list-conversations.output.conversations
|
|
312
|
+
itemVariable: conversation
|
|
313
|
+
|
|
314
|
+
# Loop body (owned by conversations-loop): list this thread's messages,
|
|
315
|
+
# then ingest. `{{loop.item.id}}` is the chat/thread id for THIS
|
|
316
|
+
# iteration; `steps.list-messages` is re-bound per iteration.
|
|
317
|
+
- id: list-messages
|
|
318
|
+
type: capability
|
|
319
|
+
position: { x: 0, y: 480 }
|
|
320
|
+
data:
|
|
321
|
+
label: List this thread's messages
|
|
322
|
+
verbId: unipile_list_messages
|
|
323
|
+
inputMapping:
|
|
324
|
+
threadId: "{{loop.item.id}}"
|
|
325
|
+
accountId: "{{steps.filter-linkedin-account.output.result.0.id}}"
|
|
326
|
+
|
|
327
|
+
# GENERIC ingest — records the external channel for this thread and its
|
|
328
|
+
# messages, idempotently (dedup on a per-message idempotency seed derived
|
|
329
|
+
# SERVER-SIDE, e.g. `${externalId}:${sentAt}:${body}`). See the ⚠️ note:
|
|
330
|
+
# `messages` is a BATCH of raw provider rows (pending channel.ingest's
|
|
331
|
+
# batch contract).
|
|
332
|
+
- id: ingest
|
|
333
|
+
type: capability
|
|
334
|
+
position: { x: 0, y: 600 }
|
|
335
|
+
data:
|
|
336
|
+
label: Ingest thread + messages (idempotent, generic)
|
|
337
|
+
verbId: channel.ingest
|
|
338
|
+
inputMapping:
|
|
339
|
+
provider: linkedin
|
|
340
|
+
externalId: "{{loop.item.id}}"
|
|
341
|
+
title: "{{loop.item.name}}"
|
|
342
|
+
accountExternalId: "{{steps.filter-linkedin-account.output.result.0.id}}"
|
|
343
|
+
# F1 — TARGET THE CRM WORKSPACE. Without this, channel.ingest lands
|
|
344
|
+
# in the automation's OWN workspace, but the dashboard inbox reads
|
|
345
|
+
# "The arch CRM". Single-tenant target = the inbox/CRM workspace.
|
|
346
|
+
# TODO(multi-tenant): make this an install/run param instead of a
|
|
347
|
+
# literal (the flow schema has no template-var/param mechanism for a
|
|
348
|
+
# capability inputMapping today, so a documented literal is the
|
|
349
|
+
# single-tenant path — see the report).
|
|
350
|
+
workspaceId: "499328b7-fd35-4143-b1bc-67b9e45d9b4b"
|
|
351
|
+
# F2 — SUPPRESS THE BACKFILL STORM. This is a poll / reconciliation
|
|
352
|
+
# of HISTORICAL messages, so it must NOT emit
|
|
353
|
+
# `external_message.received` per message (channel resolve + dedup
|
|
354
|
+
# insert still run). Without this, one backfill of N threads fans
|
|
355
|
+
# out through the webhook + automation-trigger reactors N×messages.
|
|
356
|
+
suppressSideEffects: true
|
|
357
|
+
# BATCH mode: hand the whole thread's messages[] to the generic verb
|
|
358
|
+
# in ONE call (the engine has no nested loops). channel.ingest stays
|
|
359
|
+
# provider-agnostic — messageMap tells it (in CONFIG) where each field
|
|
360
|
+
# lives inside a raw Unipile v2 message row. Verified live 2026-07-27:
|
|
361
|
+
# a message = { id, text, timestamp (ISO), sender_id, is_sender, ... }.
|
|
362
|
+
messages: "{{steps.list-messages.output.messages}}"
|
|
363
|
+
messageMap:
|
|
364
|
+
text: text
|
|
365
|
+
id: id
|
|
366
|
+
sentAt: timestamp
|
|
367
|
+
# F3 — DIRECTION. `is_sender` (dot-path to a boolean per row) marks
|
|
368
|
+
# the operator's OWN sent messages as OUTBOUND so the inbox renders
|
|
369
|
+
# them right-aligned, instead of everything looking inbound.
|
|
370
|
+
isOutbound: is_sender
|
|
371
|
+
# F6 — participantExternalId DROPPED on purpose. link-at-birth
|
|
372
|
+
# binds the channel to the participant of the first-processed
|
|
373
|
+
# (newest) message, which — when that message is the account
|
|
374
|
+
# OWNER's own reply — mis-binds the whole thread to yourself. The
|
|
375
|
+
# channel `title` already carries the conversation (contact) name,
|
|
376
|
+
# so dropping the per-message sender id lets link-at-birth avoid
|
|
377
|
+
# the mis-bind.
|
|
378
|
+
edges:
|
|
379
|
+
- { id: e-trigger-accounts, source: trigger, target: list-accounts }
|
|
380
|
+
- {
|
|
381
|
+
id: e-accounts-filter,
|
|
382
|
+
source: list-accounts,
|
|
383
|
+
target: filter-linkedin-account,
|
|
384
|
+
}
|
|
385
|
+
- {
|
|
386
|
+
id: e-filter-convos,
|
|
387
|
+
source: filter-linkedin-account,
|
|
388
|
+
target: list-conversations,
|
|
389
|
+
}
|
|
390
|
+
- {
|
|
391
|
+
id: e-convos-loop,
|
|
392
|
+
source: list-conversations,
|
|
393
|
+
target: conversations-loop,
|
|
394
|
+
}
|
|
395
|
+
- {
|
|
396
|
+
id: e-loop-messages,
|
|
397
|
+
source: conversations-loop,
|
|
398
|
+
target: list-messages,
|
|
399
|
+
}
|
|
400
|
+
- { id: e-messages-ingest, source: list-messages, target: ingest }
|
|
401
|
+
|
|
206
402
|
# LinkedIn send/search via Unipile, email send via Gmail (nango-google). The
|
|
207
403
|
# user connects each once on install; a missing key fails gracefully per-skill.
|
|
208
404
|
integrations:
|
|
@@ -268,7 +268,12 @@ profiles:
|
|
|
268
268
|
icon: user
|
|
269
269
|
color: "#0891B2"
|
|
270
270
|
description: "Team member or stakeholder"
|
|
271
|
-
|
|
271
|
+
# Pod-wide identity: `person` is ONE pod-wide SYSTEM kind. `entityScope: pod`
|
|
272
|
+
# lands the ROWS with `workspace_id = NULL` (visible from every workspace);
|
|
273
|
+
# `scope` omitted (no claim) so the applier reuses the system row and these
|
|
274
|
+
# project-management props land as this workspace's OVERLAY.
|
|
275
|
+
# (Was `scope: WORKSPACE`, which contradicted the pod-wide model.)
|
|
276
|
+
entityScope: pod
|
|
272
277
|
properties:
|
|
273
278
|
- slug: person-role
|
|
274
279
|
label: Role
|