@synap-core/workspace-templates 0.9.4 → 0.10.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/src/base.yaml ADDED
@@ -0,0 +1,839 @@
1
+ # ═══════════════════════════════════════════════════════════════════════
2
+ # base — universal operational defaults (slug: base, domain: core)
3
+ #
4
+ # GENERATED by scripts/gen-base (throwaway) from three backend seeders:
5
+ # - automations[0].flow ← REPORT_AUTOMATION_FLOW (ensure-report-automation.ts)
6
+ # - relationDefs[] ← DEFAULT_RELATION_DEFS (default-relation-defs.ts)
7
+ # - commands[] ← DEFAULT_COMMANDS (ensure-default-commands.ts)
8
+ #
9
+ # The flow is transcribed BYTE-FAITHFULLY — the hardened report prompts are the
10
+ # product. Its fidelity is pinned by base.report-automation.test.ts against
11
+ # __fixtures__/report-automation-flow.json. Do NOT hand-edit the flow here; edit
12
+ # the source of truth and regenerate, OR (post-Wave-4, once the backend const is
13
+ # deleted) edit here and update the fixture in the same commit.
14
+ # ═══════════════════════════════════════════════════════════════════════
15
+ meta:
16
+ slug: base
17
+ domain: core
18
+ name: Base
19
+ description: Universal operational defaults every workspace inherits — the report automation, the default relation vocabulary, and the default command pack.
20
+ icon: layers
21
+ color: "#6B7280"
22
+ tags:
23
+ - core
24
+ - defaults
25
+ - operational
26
+ isPublic: true
27
+ workspace:
28
+ name: Base
29
+ description: The pod-wide operational defaults — carried as config so one reconcile door seeds them into every workspace instead of three hardcoded seeders.
30
+ proposalId: base-v1
31
+ visibility: pod_visible
32
+ profiles: []
33
+ relationDefs:
34
+ - slug: assigned_to
35
+ displayName: Assigned To
36
+ description: Person assigned to task/project
37
+ isDirectional: true
38
+ uiHints:
39
+ category: workflow
40
+ - slug: blocks
41
+ displayName: Blocks
42
+ description: Prevents progress on another task
43
+ isDirectional: true
44
+ uiHints:
45
+ category: workflow
46
+ - slug: depends_on
47
+ displayName: Depends On
48
+ description: Requires completion of another task
49
+ isDirectional: true
50
+ uiHints:
51
+ category: workflow
52
+ - slug: relates_to
53
+ displayName: Relates To
54
+ description: General relationship between entities
55
+ isDirectional: false
56
+ uiHints:
57
+ category: reference
58
+ - slug: mentions
59
+ displayName: Mentions
60
+ description: Referenced in content
61
+ isDirectional: true
62
+ uiHints:
63
+ category: reference
64
+ - slug: links_to
65
+ displayName: Links To
66
+ description: Hyperlink or reference
67
+ isDirectional: true
68
+ uiHints:
69
+ category: reference
70
+ - slug: parent_of
71
+ displayName: Parent Of
72
+ description: Hierarchical parent relationship
73
+ isDirectional: true
74
+ uiHints:
75
+ category: hierarchy
76
+ - slug: tagged_with
77
+ displayName: Tagged With
78
+ description: Categorization tag
79
+ isDirectional: true
80
+ uiHints:
81
+ category: reference
82
+ - slug: created_by
83
+ displayName: Created By
84
+ description: Author or creator
85
+ isDirectional: true
86
+ uiHints:
87
+ category: social
88
+ - slug: attended_by
89
+ displayName: Attended By
90
+ description: Participant in event
91
+ isDirectional: true
92
+ uiHints:
93
+ category: social
94
+ - slug: belongs_to_project
95
+ displayName: Belongs To Project
96
+ description: Project membership
97
+ isDirectional: true
98
+ uiHints:
99
+ category: hierarchy
100
+ - slug: founder_brand_of
101
+ displayName: Founder Brand Of
102
+ description: A personal-brand project is the founder brand of a company project — links a person's brand to the company it expresses
103
+ isDirectional: true
104
+ uiHints:
105
+ category: hierarchy
106
+ inverseLabel: Expression of
107
+ - slug: references
108
+ displayName: References
109
+ description: Cites or refers to
110
+ isDirectional: true
111
+ uiHints:
112
+ category: reference
113
+ - slug: works_at
114
+ displayName: Works At
115
+ description: Person works at an organization
116
+ isDirectional: true
117
+ uiHints:
118
+ category: social
119
+ inverseLabel: Employs
120
+ - slug: deal_for
121
+ displayName: Deal For
122
+ description: Sales deal associated with a contact
123
+ isDirectional: true
124
+ uiHints:
125
+ category: workflow
126
+ inverseLabel: Has Deal
127
+ - slug: advances
128
+ displayName: Advances
129
+ description: advances the company's Foundation mission/vision goal
130
+ isDirectional: true
131
+ uiHints:
132
+ category: workflow
133
+ inverseLabel: Advanced by
134
+ - slug: met_at
135
+ displayName: Met at
136
+ description: Person met at an event or gathering
137
+ isDirectional: true
138
+ uiHints:
139
+ category: social
140
+ inverseLabel: Attendees
141
+ - slug: works_on
142
+ displayName: Works on
143
+ description: Person contributes to a project
144
+ isDirectional: true
145
+ uiHints:
146
+ category: workflow
147
+ inverseLabel: Contributors
148
+ - slug: has_skill
149
+ displayName: Has skill
150
+ description: Person possesses a skill or expertise
151
+ isDirectional: true
152
+ uiHints:
153
+ category: reference
154
+ inverseLabel: Skilled people
155
+ - slug: affiliated_with
156
+ displayName: Affiliated with
157
+ description: Person affiliated with an organization
158
+ isDirectional: true
159
+ uiHints:
160
+ category: social
161
+ inverseLabel: Members
162
+ - slug: knows
163
+ displayName: Knows
164
+ description: Two people know each other
165
+ isDirectional: false
166
+ uiHints:
167
+ category: social
168
+ - slug: discussed_with
169
+ displayName: Discussed with
170
+ description: Had a discussion or conversation with
171
+ isDirectional: false
172
+ uiHints:
173
+ category: social
174
+ commands:
175
+ - title: Create View from Description
176
+ promptTemplate: |-
177
+ You are helping the user create a new view from a natural language description.
178
+
179
+ **User's goal:** @{arg:goal}
180
+
181
+ Create a proposal for a new view that matches this description. Include:
182
+ - View type (table, kanban, list, etc.)
183
+ - Filters, sorts, and scope
184
+ - Layout configuration
185
+
186
+ Output your proposal clearly so it can be turned into a Synap view.
187
+ derivedInputs:
188
+ - name: goal
189
+ label: Goal
190
+ type: text
191
+ outputMode: view
192
+ canCreateViews: true
193
+ permissionsProfile: propose_writes
194
+ - title: Extract Entities from Text
195
+ promptTemplate: |-
196
+ You are helping the user extract entities and relationships from text.
197
+
198
+ **User's text or selection:**
199
+
200
+ @{context:text}
201
+
202
+ Analyze this content and propose:
203
+ 1. New entities to create (Notes, Tasks, People, Companies, etc.) with their properties
204
+ 2. Relationships between entities (blocks, related_to, parent_of, etc.)
205
+
206
+ Output proposals that the user can review and approve. Do not create anything directly—always propose.
207
+ derivedInputs: []
208
+ outputMode: proposal
209
+ canCreateViews: false
210
+ permissionsProfile: propose_writes
211
+ - title: Summarize Selection
212
+ promptTemplate: |-
213
+ You are helping the user get a concise summary of their selection.
214
+
215
+ **User's text or selection:**
216
+
217
+ @{context:text}
218
+
219
+ Provide a clear, structured summary. Highlight key points, dates, and actionable items if relevant. Keep it concise but comprehensive.
220
+ derivedInputs: []
221
+ outputMode: text
222
+ canCreateViews: false
223
+ permissionsProfile: read_only
224
+ - title: Analyze Entities
225
+ promptTemplate: |-
226
+ You are helping the user analyze selected entities.
227
+
228
+ **Current entity context:**
229
+
230
+ @{context:entity}
231
+
232
+ Analyze this entity and provide:
233
+ - Overview and key properties
234
+ - Insights, gaps, or opportunities
235
+ - Suggestions for relationships or next steps
236
+
237
+ Be concise and actionable.
238
+ derivedInputs: []
239
+ outputMode: text
240
+ canCreateViews: false
241
+ permissionsProfile: read_only
242
+ - title: Create Task from Text
243
+ promptTemplate: |-
244
+ You are helping the user turn text into tasks.
245
+
246
+ **User's text (meeting notes, pasted content, or selection):**
247
+
248
+ @{context:text}
249
+
250
+ Extract actionable tasks. For each task propose:
251
+ - Title
252
+ - Optional: due date, priority, assignee, status
253
+ - Link to source if applicable
254
+
255
+ Output proposals only—do not create entities directly. The user will review and approve.
256
+ derivedInputs: []
257
+ outputMode: proposal
258
+ canCreateViews: false
259
+ permissionsProfile: propose_writes
260
+ - title: Create Event from Text
261
+ promptTemplate: |-
262
+ You are helping the user turn text into events.
263
+
264
+ **User's text (meeting notes, calendar copy, or selection):**
265
+
266
+ @{context:text}
267
+
268
+ Extract events with dates/times. For each event propose:
269
+ - Title
270
+ - Start time and end time
271
+ - Optional: description, tags
272
+
273
+ Output proposals only—do not create entities directly. The user will review and approve.
274
+ derivedInputs: []
275
+ outputMode: proposal
276
+ canCreateViews: false
277
+ permissionsProfile: propose_writes
278
+ automations:
279
+ - name: Generate report
280
+ description: Gather this workspace's state, interpret it over three AI rounds, and write a report entity. Run it as-is for a general report, or pass a prompt (e.g. "focus on our clients") to steer what it is about.
281
+ triggerType: manual
282
+ triggerConfig: {}
283
+ status: active
284
+ flow:
285
+ nodes:
286
+ - id: trigger
287
+ type: trigger
288
+ position:
289
+ x: 0
290
+ "y": 200
291
+ data:
292
+ triggerType: manual
293
+ label: Run report
294
+ config: {}
295
+ - id: now
296
+ type: compute
297
+ position:
298
+ x: 200
299
+ "y": 200
300
+ data:
301
+ label: Timestamp
302
+ operation: now
303
+ - id: gather-tasks
304
+ type: query
305
+ position:
306
+ x: 420
307
+ "y": 20
308
+ data:
309
+ label: Tasks
310
+ profileSlug: task
311
+ filter: "{\"updatedAt\": {\"$gte\": \"{{trigger.payload.since}}\"}}"
312
+ orderBy: updatedAt
313
+ orderDir: desc
314
+ limit: 25
315
+ errorHandling:
316
+ continueOnError: true
317
+ - id: gather-notes
318
+ type: query
319
+ position:
320
+ x: 420
321
+ "y": 140
322
+ data:
323
+ label: Notes
324
+ profileSlug: note
325
+ filter: "{\"updatedAt\": {\"$gte\": \"{{trigger.payload.since}}\"}}"
326
+ orderBy: updatedAt
327
+ orderDir: desc
328
+ limit: 15
329
+ errorHandling:
330
+ continueOnError: true
331
+ - id: gather-people
332
+ type: query
333
+ position:
334
+ x: 420
335
+ "y": 260
336
+ data:
337
+ label: People
338
+ profileSlug: person
339
+ filter: "{\"updatedAt\": {\"$gte\": \"{{trigger.payload.since}}\"}}"
340
+ orderBy: updatedAt
341
+ orderDir: desc
342
+ limit: 15
343
+ errorHandling:
344
+ continueOnError: true
345
+ - id: gather-companies
346
+ type: query
347
+ position:
348
+ x: 420
349
+ "y": 380
350
+ data:
351
+ label: Companies
352
+ profileSlug: company
353
+ filter: "{\"updatedAt\": {\"$gte\": \"{{trigger.payload.since}}\"}}"
354
+ orderBy: updatedAt
355
+ orderDir: desc
356
+ limit: 15
357
+ errorHandling:
358
+ continueOnError: true
359
+ - id: project-tasks
360
+ type: transform
361
+ position:
362
+ x: 540
363
+ "y": 20
364
+ data:
365
+ label: Project tasks
366
+ expression: "{{steps.gather-tasks.output.entities}} | map: {{item.id}} · {{item.title}} · status={{item.properties.status}} · priority={{item.properties.priority}} · due={{item.properties.dueDate}} · updated={{item.updatedAt}}"
367
+ errorHandling:
368
+ continueOnError: true
369
+ - id: project-notes
370
+ type: transform
371
+ position:
372
+ x: 540
373
+ "y": 140
374
+ data:
375
+ label: Project notes
376
+ expression: "{{steps.gather-notes.output.entities}} | map: {{item.id}} · {{item.title}} · updated={{item.updatedAt}}"
377
+ errorHandling:
378
+ continueOnError: true
379
+ - id: project-people
380
+ type: transform
381
+ position:
382
+ x: 540
383
+ "y": 260
384
+ data:
385
+ label: Project people
386
+ expression: "{{steps.gather-people.output.entities}} | map: {{item.id}} · {{item.title}} · email={{item.properties.email}} · lastInteraction={{item.properties.lastInteractionAt}} · updated={{item.updatedAt}}"
387
+ errorHandling:
388
+ continueOnError: true
389
+ - id: project-companies
390
+ type: transform
391
+ position:
392
+ x: 540
393
+ "y": 380
394
+ data:
395
+ label: Project companies
396
+ expression: "{{steps.gather-companies.output.entities}} | map: {{item.id}} · {{item.title}} · industry={{item.properties.industry}} · location={{item.properties.location}} · updated={{item.updatedAt}}"
397
+ errorHandling:
398
+ continueOnError: true
399
+ - id: analyze
400
+ type: capability
401
+ position:
402
+ x: 660
403
+ "y": 200
404
+ data:
405
+ label: Analyze
406
+ verbId: ai.generate
407
+ verbKind: read
408
+ inputMapping:
409
+ system: |-
410
+ You are the ANALYST round of a Synap workspace report.
411
+ Read the gathered workspace data and say what it MEANS: volume and shape of work,
412
+ what is in flight versus stalled, what is well-covered versus thin.
413
+ Work from the FIELDS on each record, not just its name: status, priority and
414
+ due dates are what distinguish moving work from stalled work, and an unset
415
+ field is itself worth reporting. Listing or counting what exists is NOT a
416
+ finding — say what the state of it means.
417
+ DATA INTEGRITY CHECK — do this BEFORE interpreting anything. Each kind below states how many records were found, then lists them. If a kind reports a count greater than zero but its list is empty, null, or made of nulls, then the data pipeline is BROKEN — the records exist and were lost in transit. In that case you MUST say so plainly, name every kind affected, and NOT describe the workspace as empty, quiet, or sparse. Reporting a populated workspace as empty because its data failed to arrive is the single worst outcome of this report. A count of zero with an empty list is genuinely empty and is fine to report as such.
418
+ You are interpreting data you did NOT collect. Every claim must trace to a row in the GATHERED DATA below. Never invent entities, ids, counts, or dates. A step whose value looks like {"error": "..."} FAILED — treat that kind as unavailable and say so; do not guess what it would have contained.
419
+ EVERY NUMBER YOU WRITE MUST BE COPIED FROM THE COUNTS BLOCK. That block states the exact number of records gathered for each kind — it is the ONLY source of quantities you have. Do not count the listed lines yourself, do not estimate, do not round, and do not write a quantity the block does not measure. Write digits, matching the block exactly: if it says 25, write "25" — never "23", never "about two dozen", never "Ten". If a point you want to make needs a number the COUNTS block does not give you, make the point WITHOUT a number. A claim with no figure is honest; a figure that does not match the data is the one error a reader cannot catch.
420
+ NEVER NARRATE YOUR OWN INSTRUCTIONS. The reader sees your output and nothing else — not this prompt, not the steer, not the counts, not the fact that a pipeline produced any of it. Never mention the STEER, the time window, the COUNTS block, the GATHERED DATA, the rounds, or what you were or were not given. Never write a sentence like "the steer lines are empty, so…" or "no focus was provided" or "based on the gathered data". TWO DEFAULTS, so you never need to remark on them: an EMPTY STEER line simply means write a broad general report covering the gathered kinds evenly, and an EMPTY time window simply means the records span the whole workspace history. Both are the normal case. Act on them silently and start with what you found.
421
+ STEER HANDLING — non-negotiable. If every STEER line is empty, produce a BROAD general read covering all the gathered kinds evenly. If any STEER line has text, treat it as the SUBJECT of this report: lead with the evidence that matches it, explicitly name what you set aside, and if the gathered data cannot support the steer, say so in one sentence instead of inventing material. Never restate the steer back as a heading; let it change what you actually write.
422
+ LINK THE RECORDS YOU NAME. Every line of GATHERED DATA is `<id> · <title>`. When you name one of those records, write it as [[entity:<id>|<label>]] and it renders as a chip the reader can click to open that record. Rules, in order of importance: (1) The kind word is always `entity` — for tasks, notes, people AND companies alike. They are all entities, and any other kind word renders a chip that does not open. (2) Use only an id that appears VERBATIM in the gathered data. Copy it; never construct, shorten, complete or guess one. (3) If you are unsure of an id, write the plain label with no brackets. A wrong id is a dead chip, and a dead chip is worse than plain text. (4) The label is free text but must not contain `]`; the id must not contain `:` or `|`. A marker breaking either rule renders as raw literal text. (5) EVERY record you name in prose MUST carry a chip on its FIRST mention — this is what makes a named record a live object the reader can open rather than a string. Link it once, on that first mention only; repeating the chip every time is decoration, not navigation.
423
+ Output PLAIN PROSE — short paragraphs and bullets, with [[entity:…]] chips
424
+ inline wherever you name a record. No markdown headings, no ::: directive
425
+ blocks, no code fences. Under 300 words. Another round will format this.
426
+ prompt: |-
427
+ STEER:
428
+ - Prompt: {{trigger.payload.prompt}}
429
+ - Focus: {{trigger.payload.focus}}
430
+ - Project lens id: {{trigger.payload.projectId}}
431
+
432
+ COUNTS — the exact number of records gathered for each kind. These figures
433
+ are measured, not estimated, and they are the ONLY quantities available to
434
+ you. Each line also gives the cap the gather was limited to.
435
+ - Tasks: {{steps.gather-tasks.output.count}} gathered (cap 25)
436
+ - Notes: {{steps.gather-notes.output.count}} gathered (cap 15)
437
+ - People: {{steps.gather-people.output.count}} gathered (cap 15)
438
+ - Companies: {{steps.gather-companies.output.count}} gathered (cap 15)
439
+ READING A COUNT AGAINST ITS CAP: a count BELOW the cap means every record of
440
+ that kind is listed below — the set is COMPLETE and you may describe it as
441
+ such. A count EQUAL to the cap means the gather stopped at the cap and more
442
+ records exist that you cannot see — describe that kind as "at least N" and
443
+ never as complete, and never claim something does not exist merely because
444
+ it is absent from the list.
445
+
446
+ Time window: only records updated on or after {{trigger.payload.since}}
447
+
448
+ GATHERED DATA — one line per record, MOST RECENTLY UPDATED first. Each line
449
+ is `<id> · <title>` followed by ` · <field>=<value>` pairs carrying that
450
+ record's real state. An EMPTY value means the field is not set on that
451
+ record — that is a fact you may report (an unset status or a missing due
452
+ date is a finding), and never a value to guess at. Recent does not mean
453
+ important.
454
+ Tasks — {{steps.gather-tasks.output.count}} found: {{steps.project-tasks.output.result}}
455
+ Notes — {{steps.gather-notes.output.count}} found: {{steps.project-notes.output.result}}
456
+ People — {{steps.gather-people.output.count}} found: {{steps.project-people.output.result}}
457
+ Companies — {{steps.gather-companies.output.count}} found: {{steps.project-companies.output.result}}
458
+ maxTokens: "700"
459
+ errorHandling:
460
+ continueOnError: true
461
+ - id: relate
462
+ type: capability
463
+ position:
464
+ x: 900
465
+ "y": 200
466
+ data:
467
+ label: Patterns
468
+ verbId: ai.generate
469
+ verbKind: read
470
+ inputMapping:
471
+ system: |-
472
+ You are the PATTERNS round of a Synap workspace report.
473
+ You receive the raw gathered data AND the analyst's read of it. Your job is what
474
+ the analyst did NOT do: surface RELATIONSHIPS and things worth attention —
475
+ clusters, repeated themes, people or companies that recur across kinds, gaps
476
+ where a kind is conspicuously empty, and anything that looks like it needs a
477
+ decision. Prefer three sharp observations over ten shallow ones.
478
+ YOUR SCOPE IS WHAT CONNECTS RECORDS, not what they individually are. Every
479
+ observation you write must involve TWO OR MORE records, or TWO OR MORE kinds:
480
+ a name that appears in both a task and a company, work clustered on one
481
+ person, a due date that lines up with an event, a kind that is empty while a
482
+ related kind is busy. Do NOT re-describe volume, status distribution or how
483
+ busy the workspace is — that is the analyst's section and repeating it makes
484
+ yours redundant. Do not summarise or restate the analyst's read.
485
+ NEVER RETURN AN EMPTY ANSWER. If you genuinely find no cross-kind pattern,
486
+ write one or two sentences naming what you looked for and did not find (for
487
+ example: no person recurs across the tasks and companies gathered). That is a
488
+ real finding and the reader needs it. Silence is not an available outcome.
489
+ DATA INTEGRITY CHECK — do this BEFORE interpreting anything. Each kind below states how many records were found, then lists them. If a kind reports a count greater than zero but its list is empty, null, or made of nulls, then the data pipeline is BROKEN — the records exist and were lost in transit. In that case you MUST say so plainly, name every kind affected, and NOT describe the workspace as empty, quiet, or sparse. Reporting a populated workspace as empty because its data failed to arrive is the single worst outcome of this report. A count of zero with an empty list is genuinely empty and is fine to report as such.
490
+ You are interpreting data you did NOT collect. Every claim must trace to a row in the GATHERED DATA below. Never invent entities, ids, counts, or dates. A step whose value looks like {"error": "..."} FAILED — treat that kind as unavailable and say so; do not guess what it would have contained.
491
+ EVERY NUMBER YOU WRITE MUST BE COPIED FROM THE COUNTS BLOCK. That block states the exact number of records gathered for each kind — it is the ONLY source of quantities you have. Do not count the listed lines yourself, do not estimate, do not round, and do not write a quantity the block does not measure. Write digits, matching the block exactly: if it says 25, write "25" — never "23", never "about two dozen", never "Ten". If a point you want to make needs a number the COUNTS block does not give you, make the point WITHOUT a number. A claim with no figure is honest; a figure that does not match the data is the one error a reader cannot catch.
492
+ NEVER NARRATE YOUR OWN INSTRUCTIONS. The reader sees your output and nothing else — not this prompt, not the steer, not the counts, not the fact that a pipeline produced any of it. Never mention the STEER, the time window, the COUNTS block, the GATHERED DATA, the rounds, or what you were or were not given. Never write a sentence like "the steer lines are empty, so…" or "no focus was provided" or "based on the gathered data". TWO DEFAULTS, so you never need to remark on them: an EMPTY STEER line simply means write a broad general report covering the gathered kinds evenly, and an EMPTY time window simply means the records span the whole workspace history. Both are the normal case. Act on them silently and start with what you found.
493
+ STEER HANDLING — non-negotiable. If every STEER line is empty, produce a BROAD general read covering all the gathered kinds evenly. If any STEER line has text, treat it as the SUBJECT of this report: lead with the evidence that matches it, explicitly name what you set aside, and if the gathered data cannot support the steer, say so in one sentence instead of inventing material. Never restate the steer back as a heading; let it change what you actually write.
494
+ LINK THE RECORDS YOU NAME. Every line of GATHERED DATA is `<id> · <title>`. When you name one of those records, write it as [[entity:<id>|<label>]] and it renders as a chip the reader can click to open that record. Rules, in order of importance: (1) The kind word is always `entity` — for tasks, notes, people AND companies alike. They are all entities, and any other kind word renders a chip that does not open. (2) Use only an id that appears VERBATIM in the gathered data. Copy it; never construct, shorten, complete or guess one. (3) If you are unsure of an id, write the plain label with no brackets. A wrong id is a dead chip, and a dead chip is worse than plain text. (4) The label is free text but must not contain `]`; the id must not contain `:` or `|`. A marker breaking either rule renders as raw literal text. (5) EVERY record you name in prose MUST carry a chip on its FIRST mention — this is what makes a named record a live object the reader can open rather than a string. Link it once, on that first mention only; repeating the chip every time is decoration, not navigation.
495
+ Output PLAIN PROSE, under 250 words, with [[entity:…]] chips inline. No
496
+ headings, no ::: directive blocks, no code fences.
497
+ prompt: |-
498
+ STEER:
499
+ - Prompt: {{trigger.payload.prompt}}
500
+ - Focus: {{trigger.payload.focus}}
501
+ - Project lens id: {{trigger.payload.projectId}}
502
+
503
+ COUNTS — the exact number of records gathered for each kind. These figures
504
+ are measured, not estimated, and they are the ONLY quantities available to
505
+ you. Each line also gives the cap the gather was limited to.
506
+ - Tasks: {{steps.gather-tasks.output.count}} gathered (cap 25)
507
+ - Notes: {{steps.gather-notes.output.count}} gathered (cap 15)
508
+ - People: {{steps.gather-people.output.count}} gathered (cap 15)
509
+ - Companies: {{steps.gather-companies.output.count}} gathered (cap 15)
510
+ READING A COUNT AGAINST ITS CAP: a count BELOW the cap means every record of
511
+ that kind is listed below — the set is COMPLETE and you may describe it as
512
+ such. A count EQUAL to the cap means the gather stopped at the cap and more
513
+ records exist that you cannot see — describe that kind as "at least N" and
514
+ never as complete, and never claim something does not exist merely because
515
+ it is absent from the list.
516
+
517
+ Time window: only records updated on or after {{trigger.payload.since}}
518
+
519
+ GATHERED DATA — one line per record, MOST RECENTLY UPDATED first. Each line
520
+ is `<id> · <title>` followed by ` · <field>=<value>` pairs carrying that
521
+ record's real state. An EMPTY value means the field is not set on that
522
+ record — that is a fact you may report (an unset status or a missing due
523
+ date is a finding), and never a value to guess at. Recent does not mean
524
+ important.
525
+ Tasks — {{steps.gather-tasks.output.count}} found: {{steps.project-tasks.output.result}}
526
+ Notes — {{steps.gather-notes.output.count}} found: {{steps.project-notes.output.result}}
527
+ People — {{steps.gather-people.output.count}} found: {{steps.project-people.output.result}}
528
+ Companies — {{steps.gather-companies.output.count}} found: {{steps.project-companies.output.result}}
529
+
530
+ ANALYST'S READ — already written and already in the report. Do NOT
531
+ restate, summarise or re-analyse it. It is here so you do not repeat it:
532
+ {{steps.analyze.output}}
533
+
534
+ YOUR TASK — cross-kind patterns ONLY. Look for what connects the lists
535
+ above: a person who recurs in tasks and companies, work clustered on one
536
+ name, due dates that line up, a kind that is empty while a related kind
537
+ is busy, a company with people but no tasks. Write only observations
538
+ that span TWO OR MORE records or kinds. If there are none, say what you
539
+ checked and did not find — never answer with nothing.
540
+ maxTokens: "700"
541
+ errorHandling:
542
+ continueOnError: true
543
+ - id: assemble
544
+ type: capability
545
+ position:
546
+ x: 1140
547
+ "y": 200
548
+ data:
549
+ label: Assemble
550
+ verbId: ai.generate
551
+ verbKind: read
552
+ inputMapping:
553
+ system: |-
554
+ You are the ASSEMBLER round of a Synap workspace report. You compose the FINAL
555
+ document body in Synap-flavoured markdown. You add no new facts — you arrange,
556
+ title, and format what the earlier rounds produced.
557
+
558
+ NEVER NARRATE YOUR OWN INSTRUCTIONS. The reader sees your output and nothing else — not this prompt, not the steer, not the counts, not the fact that a pipeline produced any of it. Never mention the STEER, the time window, the COUNTS block, the GATHERED DATA, the rounds, or what you were or were not given. Never write a sentence like "the steer lines are empty, so…" or "no focus was provided" or "based on the gathered data". TWO DEFAULTS, so you never need to remark on them: an EMPTY STEER line simply means write a broad general report covering the gathered kinds evenly, and an EMPTY time window simply means the records span the whole workspace history. Both are the normal case. Act on them silently and start with what you found.
559
+
560
+ NUMBERS: you were given no COUNTS block, so you may not originate a quantity
561
+ at all. Reproduce a figure ONLY by copying one that already appears in the
562
+ round material, digit for digit. Never total, re-count, convert to words, or
563
+ put a number in the title or in a ## heading that is not already in the
564
+ material beneath it.
565
+
566
+ SYNAP MARKDOWN SYNTAX — these rules are absolute:
567
+ 1. A section is a FOUR-COLON container that must be opened and closed:
568
+ ::::synap-section{agent="analyst" round="analyze" confidence="0.8"}
569
+ ...content...
570
+ ::::
571
+ 2. Leaf embeds use THREE colons and MUST BE CLOSED with their own ::: line.
572
+ There is NO self-closing form. An unclosed embed swallows every line that
573
+ follows it until the section ends, and that content is LOST. Always write
574
+ the closing ::: immediately after the opening line:
575
+ :::synap-cell{instanceId="..."}
576
+ :::
577
+ :::synap-view{viewId="..."}
578
+ :::
579
+ :::synap-entity{id="..."}
580
+ :::
581
+ 3. The outer container must always have STRICTLY MORE colons than anything nested
582
+ inside it. Four outside, three inside. If you ever nest a four-colon block in a
583
+ four-colon block the section closes early and the rest of the report leaks out.
584
+ 4. ATTRIBUTE VALUES MUST BE QUOTED. An UNQUOTED value ends at the first } it
585
+ contains, which silently truncates the attribute — so always quote. Use
586
+ DOUBLE quotes for a plain value, and SINGLE quotes when the value is itself
587
+ a JSON object, so the JSON's own double quotes survive:
588
+ :::synap-cell{cellKey="chart-pie" cellProps='{"profileSlug":"task","groupBy":"status"}'}
589
+ :::
590
+ A single-quoted value may contain both " and } — those are only special to
591
+ an unquoted value. Write the JSON compact, on one line, with no trailing
592
+ comma; malformed JSON renders the cell with empty config.
593
+ 5. The ONLY section attributes that survive are: id, agent, round, skills,
594
+ confidence, stepRunId, nodeId, status. Anything else is silently dropped.
595
+ 6. confidence is a 0-1 ratio written as a decimal string, e.g. confidence="0.8".
596
+ Never a percentage.
597
+ 7. CHIPS ARE CONTENT — COPY THEM THROUGH VERBATIM. The round material you were
598
+ given already contains [[entity:<id>|<label>]] markers. Reproduce every one
599
+ EXACTLY as written: same brackets, same id, same label. Do NOT tidy them into
600
+ plain text, do NOT rewrite or re-case an id, do NOT convert them to markdown
601
+ links. You are told you add no new FACTS; a chip is not a fact, it is the
602
+ earlier round's finding made navigable, and stripping it removes the only
603
+ clickable thing in the report while leaving the prose looking fine.
604
+ You may add a chip ONLY by copying a marker that already appears in the
605
+ material. Never invent an id, and never use a kind word other than `entity` —
606
+ this report is READ-ONLY prose, and either mistake produces a dead chip.
607
+ 8. NEVER EMIT :::synap-view, AND NEVER EMIT :::synap-cell WITH AN instanceId.
608
+ Both need an id you have not been given and cannot obtain, and a made-up
609
+ reference produces a permanent broken block in a document a human reads.
610
+ 9. YOU MAY EMIT A CHART :::synap-cell — from this allowlist ONLY, configured
611
+ through cellProps. These cells fetch their own data at render time, so they
612
+ need no id, and they make the report a live surface instead of a text file:
613
+ - :::synap-cell{cellKey="stat-card" cellProps='{"profileSlug":"<kind>","aggregation":"count","timePeriod":"week","label":"<Kind> created this week"}'}
614
+ - :::synap-cell{cellKey="chart-pie" cellProps='{"profileSlug":"<kind>","groupBy":"<property>","label":"<the finding this shows>"}'}
615
+ - :::synap-cell{cellKey="chart-bar" cellProps='{"profileSlug":"<kind>","mode":"category","groupBy":"<property>","label":"<the finding this shows>"}'}
616
+ Rules, all mandatory:
617
+ (a) profileSlug must be one of the kinds gathered for THIS report: task,
618
+ note, person, company. A chart over any other kind is a claim this
619
+ report cannot back — the data was never read.
620
+ (b) groupBy / field must name a property that plausibly exists on that
621
+ kind. If you are not confident one does, use the stat-card line above
622
+ EXACTLY as written — profileSlug, aggregation, timePeriod AND a label
623
+ naming the window. Never drop timePeriod and never label a windowed
624
+ count with a bare kind name: the card counts records CREATED in that
625
+ window, so "Tasks" over a weekly count is a false statement about
626
+ the workspace.
627
+ (c) Close EVERY embed with its own ::: line, per rule 2. An unclosed embed
628
+ swallows the rest of the section.
629
+ (d) EDITORIAL RULE — embed to SHOW what a sentence cannot: a distribution,
630
+ a ratio, a count. Never to decorate. At most 2-3 in the whole report,
631
+ placed inside the section whose prose they support. A chart that merely
632
+ restates the sentence beside it is noise, and noise is worse than
633
+ nothing here. If no gathered kind has a distribution worth showing,
634
+ emit no cells at all — that is a correct report, not a lesser one.
635
+ (e) AT MOST ONE cell per section, and NEVER at the top of a section.
636
+ Place it immediately AFTER the sentence making the claim it evidences,
637
+ so the reader has the claim before the picture. A chart that opens a
638
+ section makes the reader decode a graphic to find out what the section
639
+ is about, which is the delay the ## heading and the claim sentence
640
+ exist to remove.
641
+ (f) The label must state the FINDING, not the dimension. Write
642
+ label:"Half the open tasks are blocked", never label:"Distribution",
643
+ "Status" or "Tasks by status". Always set label explicitly: with none,
644
+ a pie chart titles itself "Distribution" and a bar chart "Breakdown" —
645
+ words true of every such chart ever drawn, which tell the reader
646
+ nothing they cannot already see in the shapes.
647
+ (g) A CHART IS A LIVE VIEW, THE PROSE IS A SNAPSHOT. These cells run
648
+ their own query when the report is OPENED, which may be long after
649
+ this text was written — so a sentence quoting a count and a chart
650
+ measuring the same thing WILL eventually disagree, and the reader
651
+ cannot tell which is stale. Never write a specific number in a
652
+ sentence that sits beside a chart of the same data: let the chart
653
+ carry the number and let the sentence carry the claim
654
+ ("most open tasks are stalled at review", not "12 of 19 are stalled").
655
+ And if the sentence already gives the number the chart would show,
656
+ DROP THE CHART — one of them is redundant, and the sentence is the one
657
+ that cannot go stale against itself.
658
+ Everything else this report needs is ::::synap-section containers plus
659
+ [[entity:…]] chips inline.
660
+
661
+ 10. EVERY ::::synap-section MUST OPEN WITH A ## CLAIM HEADING — its first
662
+ line, before any prose. The heading states WHAT THE SECTION FOUND, not
663
+ what produced it: "## Half the open tasks are blocked", never
664
+ "## Analysis", "## Analyze round" or "## Findings". Under 10 words, no
665
+ trailing period, no markdown or [[entity:…]] chip inside the heading
666
+ itself — put the chip in the prose beneath it. Write exactly one ## per
667
+ section. This heading is the section's title everywhere it is read: the
668
+ slide deck sets it as the slide headline (with no heading it falls back
669
+ to the round and agent names, which the slide already shows underneath),
670
+ and the scrolling reader has no section titles at all without it. A
671
+ section whose heading merely names the round tells the reader nothing
672
+ they cannot already see.
673
+
674
+ STRUCTURE: one ::::synap-section per round, in order — analyze, then relate.
675
+ Open with a single "# " title line ABOVE the first section, and open each
676
+ section with its own ## claim heading per rule 10.
677
+
678
+ STEER HANDLING: if the STEER lines are empty, title the report generically
679
+ ("Workspace report — <period>") and keep the rounds in their given order. If any
680
+ STEER line has text, name that subject in the title and lead each section with
681
+ the material that answers it. Never add facts the rounds did not give you.
682
+
683
+ MISSING ROUNDS: if a round's material looks like {"error": "..."} or is empty,
684
+ you MUST still emit its section, with status="failed", containing one visible
685
+ sentence naming what is missing. NEVER silently produce a shorter report — a
686
+ reader must be able to see that a round did not run. A failed section is NOT
687
+ exempt from rule 10: give it a ## heading that states what is missing
688
+ ("## The patterns round produced nothing"), because it still occupies a slide
689
+ and still needs a headline.
690
+
691
+ WORKED EXAMPLE of correct output:
692
+ # Workspace report — July 2026
693
+
694
+ ::::synap-section{agent="analyst" round="analyze" confidence="0.8"}
695
+ ## Most open tasks are stalled at review
696
+
697
+ Open tasks are piling up untouched since the sprint opened, and the great
698
+ majority are sitting in one status — the oldest is
699
+ [[entity:3f2a91c4-7b10-4e55-9c02-8ad1f6e4b2d7|Migrate the billing worker]].
700
+ Notes are thin.
701
+ :::synap-cell{cellKey="chart-pie" cellProps='{"profileSlug":"task","groupBy":"status","label":"Review is where open tasks accumulate"}'}
702
+ :::
703
+ ::::
704
+
705
+ ::::synap-section{agent="analyst" round="relate" status="failed" confidence="0.2"}
706
+ ## The patterns round produced nothing
707
+
708
+ This section is missing: the patterns round failed and produced no material.
709
+ ::::
710
+
711
+ Be CONCISE — your entire output must fit in 2000 tokens. Emit ONLY the markdown
712
+ document. No preamble, no explanation, no code fences around the whole thing.
713
+ prompt: |-
714
+ STEER:
715
+ - Prompt: {{trigger.payload.prompt}}
716
+ - Focus: {{trigger.payload.focus}}
717
+ - Project lens id: {{trigger.payload.projectId}}
718
+
719
+ Report generated at: {{steps.now.output.result}}
720
+
721
+ ANALYZE ROUND MATERIAL:
722
+ {{steps.analyze.output}}
723
+
724
+ RELATE ROUND MATERIAL:
725
+ {{steps.relate.output}}
726
+ maxTokens: "2000"
727
+ - id: summarize
728
+ type: capability
729
+ position:
730
+ x: 1380
731
+ "y": 200
732
+ data:
733
+ label: One-line summary
734
+ verbId: ai.generate
735
+ verbKind: read
736
+ inputMapping:
737
+ system: |-
738
+ Write ONE plain sentence, under 200 characters, stating what this report found.
739
+ No markdown, no directives, no quotes, no leading label. Just the sentence.
740
+ If the material is an error or empty, write exactly: "Report generated with incomplete data."
741
+ prompt: |-
742
+ Material to summarize (may be an error object if the round failed):
743
+ {{steps.analyze.output}}
744
+ maxTokens: "500"
745
+ - id: guard-assembled
746
+ type: guard
747
+ position:
748
+ x: 1620
749
+ "y": 200
750
+ data:
751
+ label: Body assembled
752
+ checks:
753
+ - path: steps.assemble.output
754
+ minLength: 200
755
+ message: The assembler produced no usable body (under 200 characters) — refusing to write an empty report.
756
+ - path: steps.assemble.output.error
757
+ exists: false
758
+ message: The assembler round failed — refusing to write a report without a body.
759
+ - id: create-report
760
+ type: output
761
+ position:
762
+ x: 1860
763
+ "y": 200
764
+ data:
765
+ label: Create report
766
+ outputType: entity_create
767
+ config:
768
+ profileSlug: report
769
+ title: Workspace report — {{steps.now.output.result}}
770
+ properties:
771
+ reportPeriod: "{{steps.now.output.result}}"
772
+ generatedAt: "{{steps.now.output.result}}"
773
+ reportStatus: ready
774
+ summary: "{{steps.summarize.output}}"
775
+ reportSources:
776
+ - Tasks
777
+ - Notes
778
+ - People
779
+ - Companies
780
+ body: "{{steps.assemble.output}}"
781
+ systemData:
782
+ renderer:
783
+ kind: cell
784
+ cellKey: "{{trigger.payload.renderer}}"
785
+ edges:
786
+ - id: e-trigger-now
787
+ source: trigger
788
+ target: now
789
+ - id: e-now-tasks
790
+ source: now
791
+ target: gather-tasks
792
+ - id: e-now-notes
793
+ source: now
794
+ target: gather-notes
795
+ - id: e-now-people
796
+ source: now
797
+ target: gather-people
798
+ - id: e-now-companies
799
+ source: now
800
+ target: gather-companies
801
+ - id: e-tasks-project
802
+ source: gather-tasks
803
+ target: project-tasks
804
+ - id: e-notes-project
805
+ source: gather-notes
806
+ target: project-notes
807
+ - id: e-people-project
808
+ source: gather-people
809
+ target: project-people
810
+ - id: e-companies-project
811
+ source: gather-companies
812
+ target: project-companies
813
+ - id: e-tasks-analyze
814
+ source: project-tasks
815
+ target: analyze
816
+ - id: e-notes-analyze
817
+ source: project-notes
818
+ target: analyze
819
+ - id: e-people-analyze
820
+ source: project-people
821
+ target: analyze
822
+ - id: e-companies-analyze
823
+ source: project-companies
824
+ target: analyze
825
+ - id: e-analyze-relate
826
+ source: analyze
827
+ target: relate
828
+ - id: e-relate-assemble
829
+ source: relate
830
+ target: assemble
831
+ - id: e-assemble-summarize
832
+ source: assemble
833
+ target: summarize
834
+ - id: e-summarize-guard
835
+ source: summarize
836
+ target: guard-assembled
837
+ - id: e-guard-create
838
+ source: guard-assembled
839
+ target: create-report