@synap-core/workspace-templates 0.12.0 → 0.23.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/README.md +483 -0
- package/automations/bookmark-enrichment.automation.json +321 -0
- package/automations/github-pr-events.automation.json +505 -0
- package/automations/mail-feed.automation.json +86 -0
- package/automations/ship-a-change-checks-green.automation.json +106 -0
- package/automations/ship-a-change-checks-red.automation.json +106 -0
- package/automations/ship-a-change-deployed.automation.json +56 -0
- package/automations/ship-a-change-pr-merged.automation.json +110 -0
- package/automations/ship-a-change-pr-opened.automation.json +104 -0
- package/automations/url-bookmark-capture.automation.json +132 -0
- package/capabilities/agency-skills.capability.json +157 -0
- package/capabilities/apify.capability.json +265 -0
- package/capabilities/apollo.capability.json +120 -0
- package/capabilities/arch-backend.capability.json +97 -0
- package/capabilities/arch-client-intelligence.capability.json +401 -0
- package/capabilities/backblaze-b2.capability.json +205 -0
- package/capabilities/cal-com.capability.json +266 -0
- package/capabilities/canva.capability.json +274 -0
- package/capabilities/claude-code-routines.capability.json +172 -0
- package/capabilities/claude-managed-agents.capability.json +215 -0
- package/capabilities/client-comms-report.capability.json +246 -0
- package/capabilities/content-idea-to-production.capability.json +29 -0
- package/capabilities/datagouv.capability.json +315 -0
- package/capabilities/discord-bot.capability.json +157 -0
- package/capabilities/dropbox.capability.json +281 -0
- package/capabilities/exa.capability.json +109 -0
- package/capabilities/fal-ai.capability.json +83 -0
- package/capabilities/fal-media-lab.capability.json +158 -0
- package/capabilities/fireflies.capability.json +191 -0
- package/capabilities/freellmapi.capability.json +114 -0
- package/capabilities/generic-apikey.capability.json +92 -0
- package/capabilities/github-merge.capability.json +99 -0
- package/capabilities/github-read.capability.json +325 -0
- package/capabilities/governance-tighten-recommender.capability.json +81 -0
- package/capabilities/jev.capability.json +226 -0
- package/capabilities/lead-conversion-enrollment.capability.json +246 -0
- package/capabilities/mail-feed.capability.json +277 -0
- package/capabilities/mailgun.capability.json +52 -0
- package/capabilities/nango-google.capability.json +639 -0
- package/capabilities/notion.capability.json +417 -0
- package/capabilities/omniroute.capability.json +137 -0
- package/capabilities/posthog-analytics.capability.json +258 -0
- package/capabilities/postiz.capability.json +467 -0
- package/capabilities/proactive-digest.capability.json +190 -0
- package/capabilities/proton.capability.json +288 -0
- package/capabilities/replicate-rvm.capability.json +97 -0
- package/capabilities/research-methods.capability.json +57 -0
- package/capabilities/shopify.capability.json +395 -0
- package/capabilities/slack.capability.json +369 -0
- package/capabilities/stripe.capability.json +420 -0
- package/capabilities/telegram-bridge.capability.json +56 -0
- package/capabilities/treg.capability.json +102 -0
- package/capabilities/unipile-linkedin.capability.json +471 -0
- package/capabilities/vercel.capability.json +424 -0
- package/dist/define-CPS0n8BV.d.ts +1331 -0
- package/dist/index.d.ts +174 -900
- package/dist/index.js +26148 -23931
- package/dist/index.js.map +1 -1
- package/dist/wire.d.ts +222 -0
- package/dist/wire.js +77 -0
- package/dist/wire.js.map +1 -0
- package/package.json +27 -4
- package/packs/bookmark-views-pack.json +33 -0
- package/packs/crm-pack.json +76 -0
- package/packs/customer-profile.json +28 -0
- package/packs/deep-research.playbook.json +30 -0
- package/packs/event-views-pack.json +33 -0
- package/packs/habit-profile.json +27 -0
- package/packs/hr-pack.json +54 -0
- package/packs/invoice-profile.json +28 -0
- package/packs/knowledge-base-pack.json +56 -0
- package/packs/meeting-profile.json +29 -0
- package/packs/product-profile.json +30 -0
- package/packs/project-management-pack.json +49 -0
- package/packs/recipe-profile.json +29 -0
- package/packs/task-views-pack.json +39 -0
- package/retired.yaml +136 -0
- package/skills/app-factory-build-slice/SKILL.md +70 -0
- package/skills/app-factory-build-slice.catalog.json +17 -0
- package/skills/app-factory-design-canvas/SKILL.md +96 -0
- package/skills/app-factory-design-canvas.catalog.json +17 -0
- package/skills/app-factory-design-review/SKILL.md +99 -0
- package/skills/app-factory-design-review.catalog.json +18 -0
- package/skills/app-factory-research/SKILL.md +123 -0
- package/skills/app-factory-research.catalog.json +17 -0
- package/skills/bookmark-enrichment/SKILL.md +40 -0
- package/skills/carousel-format/carousel-format.skill.json +18 -0
- package/skills/collage-broll/collage-broll.skill.json +20 -0
- package/skills/content-taste/content-taste.skill.json +18 -0
- package/skills/creative-director/creative-director.skill.json +18 -0
- package/skills/design-critique/SKILL.md +135 -0
- package/skills/design-critique.catalog.json +12 -0
- package/skills/designing-with-real-components/SKILL.md +140 -0
- package/skills/designing-with-real-components.catalog.json +12 -0
- package/skills/edit-loop/edit-loop.skill.json +25 -0
- package/skills/facecam-edit/facecam-edit.skill.json +24 -0
- package/skills/mapping-the-server/SKILL.md +54 -0
- package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
- package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
- package/skills/open-design-edit/open-design-edit.skill.json +24 -0
- package/skills/reading-client-progress/SKILL.md +65 -0
- package/skills/routing-to-channels/SKILL.md +54 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
- package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
- package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
- package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
- package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
- package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
- package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
- package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
- package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
- package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
- package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
- package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
- package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
- package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
- package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
- package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
- package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
- package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
- package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
- package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
- package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
- package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
- package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
- package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
- package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
- package/skills/video-direction/video-direction.skill.json +20 -0
- package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
- package/skills/video-toolbox/video-toolbox.skill.json +20 -0
- package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
- package/src/agency-os.yaml +122 -0
- package/src/agent-os-skill-slugs.test.ts +65 -0
- package/src/apify-social-reader.test.ts +317 -0
- package/src/base.yaml +85 -39
- package/src/blockchain-ecosystem.yaml +1 -0
- package/src/brand-library.yaml +525 -14
- package/src/builder-dev-fold.template.test.ts +113 -0
- package/src/builder.yaml +779 -91
- package/src/business-model-catalog-description.test.ts +47 -0
- package/src/business-model-grp.template.test.ts +214 -0
- package/src/business-model.yaml +296 -49
- package/src/catalog-safety.test.ts +198 -0
- package/src/catalog.template-items.test.ts +75 -0
- package/src/catalog.ts +66 -2
- package/src/concept-guard.test.ts +28 -8
- package/src/concept-guard.ts +2 -2
- package/src/content-creation.template.test.ts +52 -0
- package/src/content-creation.yaml +192 -0
- package/src/content-os.edit-loop.template.test.ts +210 -0
- package/src/content-os.publish-due-posts.template.test.ts +261 -0
- package/src/content-os.template.test.ts +322 -0
- package/src/content-os.video-takes.template.test.ts +338 -0
- package/src/content-os.work-a-take.template.test.ts +125 -0
- package/src/content-os.yaml +1830 -39
- package/src/content-studio-retired.template.test.ts +82 -0
- package/src/corpus-flow-validity.test.ts +4 -5
- package/src/crm-absorbs-outreach.template.test.ts +117 -0
- package/src/crm-build-plan.template.test.ts +128 -0
- package/src/crm-lists.template.test.ts +96 -0
- package/src/crm-messages-cadence.template.test.ts +164 -0
- package/src/crm-one-send-door.template.test.ts +65 -0
- package/src/crm.yaml +1456 -181
- package/src/decisions-wave.template.test.ts +186 -0
- package/src/define.ts +130 -17
- package/src/design-to-implementation.template.test.ts +94 -0
- package/src/domain-template-type.test.ts +75 -0
- package/src/ecosystem.yaml +27 -66
- package/src/enterprise-os.yaml +10 -134
- package/src/facecam-edit.audio-studio.test.ts +188 -0
- package/src/fal-generate-image.test.ts +200 -0
- package/src/fal-media-lab-pod-files.test.ts +180 -0
- package/src/finance.yaml +254 -1
- package/src/foundation.yaml +78 -38
- package/src/frame-host-modules.d.mts +6 -0
- package/src/frame-host-modules.mjs +58 -0
- package/src/generate.mjs +9 -3
- package/src/github-merge.test.ts +210 -0
- package/src/grants.yaml +46 -17
- package/src/hr.yaml +39 -6
- package/src/index.ts +36 -1
- package/src/intent.test.ts +161 -0
- package/src/internal-runbook.yaml +19 -1
- package/src/jev.test.ts +356 -0
- package/src/legal.yaml +33 -1
- package/src/life-os.yaml +51 -240
- package/src/lifecycle-coverage.test.ts +124 -0
- package/src/lifecycle-declarations.test.ts +252 -0
- package/src/lifecycle-presets.test.ts +468 -0
- package/src/lifecycle-presets.ts +494 -0
- package/src/lifecycle-stuck.test.ts +46 -0
- package/src/lifecycle-waits.parity.test.ts +319 -0
- package/src/marketing.yaml +108 -4
- package/src/meetings-knowledge.yaml +18 -0
- package/src/networking.yaml +23 -2
- package/src/operations.yaml +304 -4
- package/src/pipeline-views.template.test.ts +88 -0
- package/src/playbook-goal-placeholders.test.ts +57 -0
- package/src/postiz-contract.test.ts +134 -0
- package/src/presentation-declarations.test.ts +174 -0
- package/src/process-contract.validate.test.ts +352 -0
- package/src/project-management.yaml +39 -740
- package/src/publish-body.d.mts +29 -0
- package/src/publish-body.mjs +74 -0
- package/src/publish-body.test.ts +74 -0
- package/src/publish-official-lib.test.ts +176 -0
- package/src/replicate-rvm.test.ts +504 -0
- package/src/research-base.yaml +119 -20
- package/src/research-decision.yaml +60 -3
- package/src/reserved-profile-slugs.test.ts +83 -0
- package/src/retired-goal-kinds.test.ts +58 -0
- package/src/role-reference-and-step-domain.test.ts +192 -0
- package/src/seed-obligation.test.ts +1 -6
- package/src/send-message-maps.test.ts +227 -0
- package/src/shared-role-ssot.test.ts +40 -2
- package/src/ship-a-change.template.test.ts +177 -0
- package/src/skills.test.ts +170 -0
- package/src/social.yaml +85 -27
- package/src/space-packages.freshness.test.ts +97 -0
- package/src/space-packages.generated.ts +40 -0
- package/src/space-packages.test.ts +125 -0
- package/src/space-packages.ts +73 -0
- package/src/start.test.ts +188 -0
- package/src/task-intents.test.ts +293 -0
- package/src/templates.ts +20851 -21496
- package/src/types.ts +443 -60
- package/src/unipile-publish-post.test.ts +162 -0
- package/src/validate-flow.drift.test.ts +2 -3
- package/src/validate-flow.ts +22 -2
- package/src/validate-onboarding.test.ts +59 -0
- package/src/validate.test.ts +71 -4
- package/src/validate.ts +1335 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -0
- package/src/agent-fleet.yaml +0 -1645
- package/src/business-developer.template.test.ts +0 -351
- package/src/business-developer.yaml +0 -860
- package/src/content-studio.yaml +0 -361
- package/src/dev-dashboard.yaml +0 -990
- package/src/outreach-comms.yaml +0 -486
- package/src/the-arch.yaml +0 -201
package/src/content-os.yaml
CHANGED
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
# The editorial engine — turn thinking into a published content system.
|
|
3
3
|
# View-driven workspace (app: stored / entity-detail), modeled on marketing.yaml.
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# posts, pillars, platforms,
|
|
5
|
+
# Content OS is THE Content space — one space per domain (root CLAUDE.md, "One
|
|
6
|
+
# space per domain"), so this ONE template carries every format: the editorial
|
|
7
|
+
# model (posts, pillars, platforms, newsletters) AND the video/visual pipeline
|
|
8
|
+
# (`content-composition`, `content-asset`, `render-job`, plus the shared
|
|
9
|
+
# `brand-template` grant). The browser Content app (`CONTENT_KINDS` in
|
|
10
|
+
# browser/.../apps/content/content-model.ts) reads all of them from this one
|
|
11
|
+
# space — before this template declared them, "New video" failed with
|
|
12
|
+
# `Profile not found: content-composition` on a pod with only Content OS.
|
|
13
|
+
# content-studio.yaml was SUPERSEDED (its kinds were re-declared here verbatim)
|
|
14
|
+
# and is retired into this template (retired.yaml, 2026-11-03).
|
|
8
15
|
|
|
9
16
|
meta:
|
|
10
17
|
slug: content-os
|
|
@@ -15,6 +22,34 @@ meta:
|
|
|
15
22
|
color: "#6366F1"
|
|
16
23
|
tags: [content, editorial, publishing, creator, newsletter]
|
|
17
24
|
isPublic: true
|
|
25
|
+
# Onboarding start (meta.start): the "content" card.
|
|
26
|
+
start:
|
|
27
|
+
order: 2
|
|
28
|
+
label: Content
|
|
29
|
+
promise: Every post from idea to published, on one calendar.
|
|
30
|
+
suggestFor: [writer, creator, marketer, musician, photographer]
|
|
31
|
+
profile: post
|
|
32
|
+
views: [content-posts, content-calendar]
|
|
33
|
+
example:
|
|
34
|
+
- { title: "Why we left spreadsheets", values: { post-status: Idea, post-format: Article } }
|
|
35
|
+
- { title: "Three mistakes in our first launch", values: { post-status: Briefed, post-format: Thread } }
|
|
36
|
+
- { title: "Behind the scenes: build week", values: { post-status: "In Draft", post-format: "Short Video" } }
|
|
37
|
+
- { title: "Customer story: the Lyon bakery", values: { post-status: Review, post-format: Carousel } }
|
|
38
|
+
- { title: "Our pricing, explained", values: { post-status: Published, post-format: Article } }
|
|
39
|
+
firstSession:
|
|
40
|
+
title: Your content pipeline
|
|
41
|
+
goal: "Add the 3 to 5 posts you are working on now, each with a format and a status, and set a publish date on the next one."
|
|
42
|
+
starters:
|
|
43
|
+
- "I publish a weekly newsletter and a few social posts"
|
|
44
|
+
- "I make short videos for Instagram and YouTube"
|
|
45
|
+
- "I write articles to attract clients"
|
|
46
|
+
# What this space PRODUCES (homes): the purpose word, then its kinds/roles.
|
|
47
|
+
intent:
|
|
48
|
+
label: Content
|
|
49
|
+
produces:
|
|
50
|
+
- profile: post
|
|
51
|
+
- profile: newsletter
|
|
52
|
+
- profile: campaign
|
|
18
53
|
|
|
19
54
|
# The editorial `post`/`newsletter` kinds REFERENCE profiles owned elsewhere:
|
|
20
55
|
# • `campaign` — the marketing-campaign template's kind. It ships pod-wide as a
|
|
@@ -53,6 +88,91 @@ dependencies:
|
|
|
53
88
|
kind: workspace
|
|
54
89
|
relation: require
|
|
55
90
|
reason: "Provides the campaign kind (and its SSOT property body) this workspace's posts/newsletters link to."
|
|
91
|
+
# Same reason as foundation above, for the video pipeline: `brand-template` is
|
|
92
|
+
# Brand Library's pod-wide `scope: shared` kind and is re-declared below with
|
|
93
|
+
# only the properties used here. Requiring brand-library installs it FIRST, so
|
|
94
|
+
# its FULL body is the SSOT base and our subset never takes the pod-wide seat.
|
|
95
|
+
# It is also what the Produce Content playbook already reads (the brand kit,
|
|
96
|
+
# GET /api/hub/brand/kit). Brand is a separate domain, so this is a second
|
|
97
|
+
# space by design, not a second Content space.
|
|
98
|
+
- slug: brand-library
|
|
99
|
+
kind: workspace
|
|
100
|
+
relation: require
|
|
101
|
+
reason: "Provides the brand kit and the shared brand-template kind compositions start from."
|
|
102
|
+
# Bottom-up: the skill is its OWN standalone package (skills/creative-director.skill.json),
|
|
103
|
+
# published before this template, and installed here by slug. Resolved in Step 0 of
|
|
104
|
+
# materialization — BEFORE the skills linker — so the `skills:` link below finds a
|
|
105
|
+
# pod-wide approved row and reports `linked` rather than `unresolved`.
|
|
106
|
+
- slug: creative-director
|
|
107
|
+
kind: skill
|
|
108
|
+
relation: require
|
|
109
|
+
reason: "The creative director routes every content ask to the brand's approved brand-template, references and assets."
|
|
110
|
+
# Same bottom-up shape for the taste skill (skills/content-taste/): linked
|
|
111
|
+
# on-demand below, for the readiness gaps the director routes to it.
|
|
112
|
+
- slug: content-taste
|
|
113
|
+
kind: skill
|
|
114
|
+
relation: require
|
|
115
|
+
reason: "Turns references into approved guidelines and the asset kit a post is made from, when the readiness check finds them missing."
|
|
116
|
+
- slug: carousel-format
|
|
117
|
+
kind: skill
|
|
118
|
+
relation: require
|
|
119
|
+
reason: "The carousel module: hook slide, one idea per slide, text density, last-slide CTA, 4:5, slot use, built as a layered composition on the post."
|
|
120
|
+
# The edit loop (founder D0-D2, 2026-11-02): one contract every engine
|
|
121
|
+
# shares (the layer-<pass> slots, studio, needs-tool-<tool>, the three
|
|
122
|
+
# answers), and the Open Design engine for decks and PDF documents.
|
|
123
|
+
- slug: edit-loop
|
|
124
|
+
kind: skill
|
|
125
|
+
relation: require
|
|
126
|
+
reason: "The one edit-run contract every engine shares: pass slots, the three answers, a version before every pass, work from the diff."
|
|
127
|
+
- slug: open-design-edit
|
|
128
|
+
kind: skill
|
|
129
|
+
relation: require
|
|
130
|
+
reason: "Decks, PDF documents and presentations built in Open Design on the person's Mac, filed back to the pod."
|
|
131
|
+
# The video toolbox (founder decision 2026-10-13, pod decision "Video toolbox:
|
|
132
|
+
# one tool per job"): one routing skill plus one skill per job. Same bottom-up
|
|
133
|
+
# shape as the skills above: each is its own package under skills/, installed
|
|
134
|
+
# here and linked on-demand in `skills:` below.
|
|
135
|
+
- slug: video-toolbox
|
|
136
|
+
kind: skill
|
|
137
|
+
relation: require
|
|
138
|
+
reason: "Routes every video ask to the one tool for its job: HyperFrames to create, Cap + OpenChatCut to edit footage, ManimGX for math, fal.ai to generate."
|
|
139
|
+
- slug: video-direction
|
|
140
|
+
kind: skill
|
|
141
|
+
relation: require
|
|
142
|
+
reason: "Directs a short film from brief to delivery: story, treatment, look, sound, camera, delivery."
|
|
143
|
+
- slug: video-style-vocabulary
|
|
144
|
+
kind: skill
|
|
145
|
+
relation: require
|
|
146
|
+
reason: "43 visual styles and the per-film style sheet the build follows, under the brand's palette and fonts."
|
|
147
|
+
- slug: motion-quality-gates
|
|
148
|
+
kind: skill
|
|
149
|
+
relation: require
|
|
150
|
+
reason: "The checks a HyperFrames cut passes before anyone sees it: determinism, reading time, seams, loudness, rhythm."
|
|
151
|
+
- slug: math-explainer-manimgx
|
|
152
|
+
kind: skill
|
|
153
|
+
relation: require
|
|
154
|
+
reason: "Math, equation, graph and algorithm explainer clips with ManimGX, handed to HyperFrames as media."
|
|
155
|
+
- slug: facecam-edit
|
|
156
|
+
kind: skill
|
|
157
|
+
relation: require
|
|
158
|
+
reason: "Face-camera footage to a finished cut: transcript-based cuts, captions and graphics timed to speech, QA."
|
|
159
|
+
- slug: collage-broll
|
|
160
|
+
kind: skill
|
|
161
|
+
relation: require
|
|
162
|
+
reason: "Generated b-roll through fal.ai in three approval gates (idea, still, video), saved to the pod for reuse."
|
|
163
|
+
|
|
164
|
+
# ── taskIntents — WHAT THIS WORKSPACE NEEDS TO FUNCTION ─────────────────────
|
|
165
|
+
# Carried over from content-studio with the video pipeline it describes:
|
|
166
|
+
# generate_media (the render), manage_file (zip / mp4 / stills land as pod
|
|
167
|
+
# files), capture_into_pod (a finished render becomes pod entities),
|
|
168
|
+
# search_external (asset gathering upstream of composition). A declaration, not
|
|
169
|
+
# a snapshot of what is installed — validated by `validateTemplate`
|
|
170
|
+
# (`task-intent-unknown`) and src/task-intents.test.ts.
|
|
171
|
+
taskIntents:
|
|
172
|
+
- generate_media
|
|
173
|
+
- manage_file
|
|
174
|
+
- capture_into_pod
|
|
175
|
+
- search_external
|
|
56
176
|
|
|
57
177
|
workspace:
|
|
58
178
|
name: Content OS
|
|
@@ -69,18 +189,48 @@ workspace:
|
|
|
69
189
|
- content.pillars
|
|
70
190
|
- content.calendar
|
|
71
191
|
- content.newsletter
|
|
192
|
+
# The video pipeline's family (`CONTENT_CAPABILITIES`, @synap-core/workspace-directory),
|
|
193
|
+
# carried over from content-studio. `content.pipeline` is the anchor the
|
|
194
|
+
# browser resolves a `workspaceType: content` scope against
|
|
195
|
+
# (useSurfaceReadiness `capabilityForKind`). `content.templates` is not
|
|
196
|
+
# claimed: templates are Brand Library's `brand-template` (brand.templates).
|
|
197
|
+
- content.pipeline
|
|
198
|
+
- content.compose
|
|
199
|
+
- content.render
|
|
200
|
+
- content.assets
|
|
72
201
|
sourceRoles:
|
|
73
202
|
brand: consumer
|
|
74
203
|
marketing: consumer
|
|
75
204
|
content: provider
|
|
76
205
|
|
|
77
206
|
profiles:
|
|
207
|
+
# `post` is the ONE pod-wide post kind (`scope: shared`, the `platform`
|
|
208
|
+
# pattern below): Social re-declares it to get the grant instead of owning a
|
|
209
|
+
# `social-post` twin (folded in by the w11.post.* manifest ops, plan D2).
|
|
210
|
+
# Shares the DEFINITION, not the entities — no `entityScope: pod`.
|
|
78
211
|
- slug: post
|
|
79
212
|
displayName: Post
|
|
80
213
|
icon: file-edit
|
|
81
214
|
color: "#6366F1"
|
|
82
215
|
description: "Your atomic content unit. One idea, multiple platform formats — tracked from spark to publication."
|
|
83
|
-
scope:
|
|
216
|
+
scope: shared
|
|
217
|
+
# The ONE lifecycle (work model L1): every option's category.
|
|
218
|
+
lifecycle:
|
|
219
|
+
property: post-status
|
|
220
|
+
options:
|
|
221
|
+
- { key: Idea, category: open }
|
|
222
|
+
- { key: Briefed, category: active }
|
|
223
|
+
# Added 2026-10-12 (D4): the written version (script / outline) exists
|
|
224
|
+
# and waits for a person's approval before anything is generated.
|
|
225
|
+
- { key: Scripted, category: active }
|
|
226
|
+
- { key: "In Draft", category: active }
|
|
227
|
+
- { key: Review, category: active }
|
|
228
|
+
- { key: Scheduled, category: active }
|
|
229
|
+
- { key: Published, category: done }
|
|
230
|
+
- { key: Archived, category: lost, humanOnly: true }
|
|
231
|
+
# How it renders (entity rendering, decision 8): @synap-core/types/presentation.
|
|
232
|
+
presentation:
|
|
233
|
+
family: work
|
|
84
234
|
properties:
|
|
85
235
|
- slug: post-status
|
|
86
236
|
label: Status
|
|
@@ -90,6 +240,7 @@ profiles:
|
|
|
90
240
|
[
|
|
91
241
|
"Idea",
|
|
92
242
|
"Briefed",
|
|
243
|
+
"Scripted",
|
|
93
244
|
"In Draft",
|
|
94
245
|
"Review",
|
|
95
246
|
"Scheduled",
|
|
@@ -113,6 +264,24 @@ profiles:
|
|
|
113
264
|
"Podcast Episode",
|
|
114
265
|
"Other",
|
|
115
266
|
]
|
|
267
|
+
# What the post is FOR — the axis that picks its narrative frame (an
|
|
268
|
+
# approved `content-guideline` of type=frame for this goal, Brand
|
|
269
|
+
# Library). Same enum as `guideline-goal`; never rename a value.
|
|
270
|
+
- slug: post-goal
|
|
271
|
+
label: Goal
|
|
272
|
+
valueType: string
|
|
273
|
+
inputType: select
|
|
274
|
+
enumValues:
|
|
275
|
+
[
|
|
276
|
+
"sell",
|
|
277
|
+
"hot-take",
|
|
278
|
+
"tutorial",
|
|
279
|
+
"product-demo",
|
|
280
|
+
"announcement",
|
|
281
|
+
"proof",
|
|
282
|
+
"behind-the-scenes",
|
|
283
|
+
"community",
|
|
284
|
+
]
|
|
116
285
|
- slug: post-platform
|
|
117
286
|
label: Platform
|
|
118
287
|
valueType: string
|
|
@@ -225,12 +394,177 @@ profiles:
|
|
|
225
394
|
valueType: entity_id
|
|
226
395
|
targetProfileSlug: post
|
|
227
396
|
inputType: entity_select
|
|
397
|
+
# The person's own words, verbatim (B7). Stored by capture before it was
|
|
398
|
+
# declared; declared so it can be queried and validated.
|
|
399
|
+
- slug: content
|
|
400
|
+
label: Original words
|
|
401
|
+
description: "The person's own words, verbatim, in whatever language they were written."
|
|
402
|
+
valueType: string
|
|
403
|
+
inputType: textarea
|
|
228
404
|
- slug: post-url
|
|
229
405
|
label: Published URL
|
|
230
406
|
valueType: string
|
|
231
407
|
inputType: text
|
|
232
408
|
placeholder: "https://..."
|
|
233
409
|
constraints: { format: url }
|
|
410
|
+
# The publishing ACCOUNT (a `platform` entity) the post goes out on —
|
|
411
|
+
# distinct from `post-platform`, the network label above.
|
|
412
|
+
- slug: post-account
|
|
413
|
+
label: Account
|
|
414
|
+
valueType: entity_id
|
|
415
|
+
targetProfileSlug: platform
|
|
416
|
+
inputType: entity_select
|
|
417
|
+
# ── Publish outcome (plan D12) ─────────────────────────────────────────
|
|
418
|
+
# `failed` is what happened to a publish ATTEMPT, not a lifecycle stage:
|
|
419
|
+
# a post whose publish failed is still Scheduled (or wherever it was).
|
|
420
|
+
# `queued`: approved and armed, Publish due posts sends it on its day;
|
|
421
|
+
# `sending`: claimed by the send (the automation, or Content Studio's
|
|
422
|
+
# "Post now"), so it is never sent twice (decision A, 3 Nov);
|
|
423
|
+
# `waiting`: a repost armed at approval that waits for its original to
|
|
424
|
+
# go out (Publish due posts queues it then, with the original's id).
|
|
425
|
+
- slug: publish-outcome
|
|
426
|
+
label: Publish outcome
|
|
427
|
+
valueType: string
|
|
428
|
+
inputType: select
|
|
429
|
+
enumValues: [succeeded, failed, queued, sending, waiting]
|
|
430
|
+
- slug: publish-error
|
|
431
|
+
label: Publish error
|
|
432
|
+
valueType: string
|
|
433
|
+
inputType: textarea
|
|
434
|
+
- slug: published-at
|
|
435
|
+
label: Published At
|
|
436
|
+
valueType: date
|
|
437
|
+
inputType: date
|
|
438
|
+
# ── What goes out, frozen when the post is approved (decision A, 3 Nov) ──
|
|
439
|
+
# Approving a post (Review → Scheduled) freezes what "Publish due posts"
|
|
440
|
+
# sends on its day, so the automation never reads documents: the words,
|
|
441
|
+
# the Unipile account, the pictures, and the moment it may go.
|
|
442
|
+
- slug: publish-text
|
|
443
|
+
label: Words to send
|
|
444
|
+
description: "The approved caption or text, frozen when the post was approved. Publish due posts sends exactly this."
|
|
445
|
+
valueType: string
|
|
446
|
+
inputType: textarea
|
|
447
|
+
- slug: publish-account-id
|
|
448
|
+
label: Unipile account ID
|
|
449
|
+
description: "The Unipile account it goes out on, copied from its platform's unipile-account-id when it was approved."
|
|
450
|
+
valueType: string
|
|
451
|
+
inputType: text
|
|
452
|
+
- slug: publish-media
|
|
453
|
+
label: Files to send
|
|
454
|
+
description: "The pictures or video sent with it, as pod file ids in order (at most 10), frozen when it was approved."
|
|
455
|
+
valueType: array
|
|
456
|
+
# Epoch milliseconds: the automation's query compares properties
|
|
457
|
+
# numerically only, so the moment is a number (09:00 Europe/Paris on the
|
|
458
|
+
# publish date when Content Studio arms it).
|
|
459
|
+
- slug: publish-at-ms
|
|
460
|
+
label: Sends after (ms)
|
|
461
|
+
description: "The moment Publish due posts may send it, in milliseconds since 1970 (UTC)."
|
|
462
|
+
valueType: number
|
|
463
|
+
inputType: number
|
|
464
|
+
# ── Where it goes (accounts and destinations, founder 4 Nov, L9) ──────
|
|
465
|
+
# A post goes out on ONE account (`post-account`) and may also go out on
|
|
466
|
+
# others: the same post sent again from each account in `post-copy-to`,
|
|
467
|
+
# or a native repost of it from each account in `post-reshare-on`
|
|
468
|
+
# (LinkedIn only: Unipile's quoted_post_id; Instagram has no repost).
|
|
469
|
+
# Approving the post writes one post per extra account (`post-copy-of`
|
|
470
|
+
# pointing back here, `post-send-mode` copy | reshare), each with its own
|
|
471
|
+
# frozen send and its own outcome, so Publish due posts still sends one
|
|
472
|
+
# post per run and a failed repost never hides a sent original.
|
|
473
|
+
- slug: post-copy-to
|
|
474
|
+
label: Also posted on
|
|
475
|
+
description: "The other accounts (platform ids) that get the same post, sent again from each."
|
|
476
|
+
valueType: array
|
|
477
|
+
- slug: post-reshare-on
|
|
478
|
+
label: Reposted by
|
|
479
|
+
description: "The other accounts (platform ids) that repost it once it is out (LinkedIn)."
|
|
480
|
+
valueType: array
|
|
481
|
+
- slug: post-copy-of
|
|
482
|
+
label: Sent for
|
|
483
|
+
description: "The post this one sends again or reposts on another account."
|
|
484
|
+
valueType: entity_id
|
|
485
|
+
targetProfileSlug: post
|
|
486
|
+
inputType: entity_select
|
|
487
|
+
- slug: post-send-mode
|
|
488
|
+
label: Sent as
|
|
489
|
+
valueType: string
|
|
490
|
+
inputType: select
|
|
491
|
+
enumValues: [copy, reshare]
|
|
492
|
+
# Frozen with the rest at approval, for a LinkedIn company page: the
|
|
493
|
+
# page's id (its platform's linkedin-organization-id), sent as post_as.
|
|
494
|
+
- slug: publish-as-organization
|
|
495
|
+
label: Posts as page
|
|
496
|
+
description: "The LinkedIn company page it is posted as (Unipile post_as), frozen when it was approved."
|
|
497
|
+
valueType: string
|
|
498
|
+
inputType: text
|
|
499
|
+
# A repost's original, written when the original went out: Unipile's id
|
|
500
|
+
# of that post, sent as quoted_post_id.
|
|
501
|
+
- slug: publish-quote-id
|
|
502
|
+
label: Reposts post
|
|
503
|
+
description: "Unipile's id of the post this repost shares, written when the original went out."
|
|
504
|
+
valueType: string
|
|
505
|
+
inputType: text
|
|
506
|
+
# Unipile's id of this post once it is out (what a repost of it quotes).
|
|
507
|
+
- slug: publish-post-id
|
|
508
|
+
label: Unipile post ID
|
|
509
|
+
description: "Unipile's id of this post, written when it went out."
|
|
510
|
+
valueType: string
|
|
511
|
+
inputType: text
|
|
512
|
+
# ── Postiz (the `postiz` capability) ─────────────────────────────────
|
|
513
|
+
# The post's id in the person's Postiz, recorded when it is queued there
|
|
514
|
+
# (postiz_schedule_post returns it). Postiz's webhook finds the post by it.
|
|
515
|
+
- slug: postiz-post-id
|
|
516
|
+
label: Postiz post ID
|
|
517
|
+
valueType: string
|
|
518
|
+
inputType: text
|
|
519
|
+
|
|
520
|
+
# An idea is its OWN kind, not a post at Idea (founder decision B, 3 Nov
|
|
521
|
+
# 2026, review 22fec5ee): it has no format and no date, and "one idea feeds
|
|
522
|
+
# several posts, several ideas feed one post" is a relation, `idea --feeds-->
|
|
523
|
+
# post` (entityLinks below). Packaging ideas into posts is the "Package ideas"
|
|
524
|
+
# playbook. The posts at Idea that predate this kind convert through the
|
|
525
|
+
# backend conversions manifest, never by hand.
|
|
526
|
+
- slug: idea
|
|
527
|
+
displayName: Idea
|
|
528
|
+
icon: lightbulb
|
|
529
|
+
color: "#F59E0B"
|
|
530
|
+
description: "A spark worth keeping: a claim, a story or a question, in your own words. It feeds one or more posts."
|
|
531
|
+
# The ONE lifecycle (work model L1). Used = it feeds a post. No playbook
|
|
532
|
+
# binds this lifecycle, so no option is humanOnly (an agent's move into
|
|
533
|
+
# done or lost proposes anyway).
|
|
534
|
+
lifecycle:
|
|
535
|
+
property: idea-status
|
|
536
|
+
options:
|
|
537
|
+
- { key: New, category: open }
|
|
538
|
+
- { key: Shaped, category: active }
|
|
539
|
+
- { key: Used, category: done }
|
|
540
|
+
- { key: Dropped, category: lost, tone: textMuted }
|
|
541
|
+
properties:
|
|
542
|
+
- slug: idea-status
|
|
543
|
+
label: Status
|
|
544
|
+
valueType: string
|
|
545
|
+
inputType: select
|
|
546
|
+
enumValues: ["New", "Shaped", "Used", "Dropped"]
|
|
547
|
+
constraints: { defaultValue: New }
|
|
548
|
+
# The person's own words, verbatim (same field as on `post`).
|
|
549
|
+
- slug: content
|
|
550
|
+
label: Original words
|
|
551
|
+
description: "The person's own words, verbatim, in whatever language they were written."
|
|
552
|
+
valueType: string
|
|
553
|
+
inputType: textarea
|
|
554
|
+
- slug: idea-pillar
|
|
555
|
+
label: Pillar
|
|
556
|
+
valueType: entity_id
|
|
557
|
+
targetProfileSlug: pillar
|
|
558
|
+
inputType: entity_select
|
|
559
|
+
- slug: idea-stage
|
|
560
|
+
label: Journey Stage
|
|
561
|
+
valueType: entity_id
|
|
562
|
+
targetProfileSlug: journey-stage
|
|
563
|
+
inputType: entity_select
|
|
564
|
+
# Where it came from (`source:dump`, `source:interview`, …), as on `post-tags`.
|
|
565
|
+
- slug: idea-tags
|
|
566
|
+
label: Tags
|
|
567
|
+
valueType: array
|
|
234
568
|
|
|
235
569
|
- slug: pillar
|
|
236
570
|
displayName: Pillar
|
|
@@ -495,6 +829,42 @@ profiles:
|
|
|
495
829
|
- slug: platform-tags
|
|
496
830
|
label: Tags
|
|
497
831
|
valueType: array
|
|
832
|
+
# ── Postiz channel (the `postiz` capability) ─────────────────────────
|
|
833
|
+
# Which Postiz channel this account is, both read from
|
|
834
|
+
# postiz_list_integrations: its id, and its provider (the `identifier`
|
|
835
|
+
# a post's `settings.__type` names: x, linkedin, instagram…). A post
|
|
836
|
+
# queued on this account passes both to postiz_schedule_post.
|
|
837
|
+
- slug: postiz-integration-id
|
|
838
|
+
label: Postiz channel ID
|
|
839
|
+
valueType: string
|
|
840
|
+
inputType: text
|
|
841
|
+
- slug: postiz-provider
|
|
842
|
+
label: Postiz provider
|
|
843
|
+
valueType: string
|
|
844
|
+
inputType: text
|
|
845
|
+
# The Unipile account this platform posts from (unipile_list_accounts
|
|
846
|
+
# `id`, e.g. acc_…): copied onto a post as publish-account-id when it is
|
|
847
|
+
# approved, so Publish due posts sends it from this account.
|
|
848
|
+
- slug: unipile-account-id
|
|
849
|
+
label: Unipile account ID
|
|
850
|
+
valueType: string
|
|
851
|
+
inputType: text
|
|
852
|
+
# Whose account it is (founder, 4 Nov): one row per account, so a
|
|
853
|
+
# network can hold a personal and a company account side by side. A new
|
|
854
|
+
# post goes to the personal account of its network by default.
|
|
855
|
+
- slug: account-owner
|
|
856
|
+
label: Owner
|
|
857
|
+
valueType: string
|
|
858
|
+
inputType: select
|
|
859
|
+
enumValues: [Personal, Company]
|
|
860
|
+
# A LinkedIn company page posts through the personal account that
|
|
861
|
+
# administers it: same unipile-account-id, plus the page's id here
|
|
862
|
+
# (linkedin_list_company_pages), copied onto a post as
|
|
863
|
+
# publish-as-organization and sent as Unipile's post_as.
|
|
864
|
+
- slug: linkedin-organization-id
|
|
865
|
+
label: LinkedIn page ID
|
|
866
|
+
valueType: string
|
|
867
|
+
inputType: text
|
|
498
868
|
|
|
499
869
|
- slug: newsletter
|
|
500
870
|
displayName: Newsletter
|
|
@@ -502,6 +872,16 @@ profiles:
|
|
|
502
872
|
color: "#10B981"
|
|
503
873
|
description: "A newsletter issue — your direct channel to an owned audience. No algorithm, no platform fee, no intermediary."
|
|
504
874
|
scope: WORKSPACE
|
|
875
|
+
# The ONE lifecycle (work model L1): every option's category.
|
|
876
|
+
lifecycle:
|
|
877
|
+
property: newsletter-status
|
|
878
|
+
options:
|
|
879
|
+
- { key: Idea, category: open }
|
|
880
|
+
- { key: Drafting, category: active }
|
|
881
|
+
- { key: Editing, category: active }
|
|
882
|
+
- { key: Scheduled, category: active }
|
|
883
|
+
- { key: Sent, category: done }
|
|
884
|
+
- { key: Archived, category: lost, humanOnly: true }
|
|
505
885
|
properties:
|
|
506
886
|
- slug: newsletter-status
|
|
507
887
|
label: Status
|
|
@@ -578,6 +958,15 @@ profiles:
|
|
|
578
958
|
color: pink
|
|
579
959
|
description: "A marketing campaign — email, social, event, content, or partnership"
|
|
580
960
|
semanticSlug: marketing.campaign
|
|
961
|
+
# The ONE lifecycle (work model L1): every option's category.
|
|
962
|
+
lifecycle:
|
|
963
|
+
property: campaign-status
|
|
964
|
+
options:
|
|
965
|
+
- { key: planning, category: open }
|
|
966
|
+
- { key: active, category: active }
|
|
967
|
+
- { key: paused, category: paused, humanOnly: true }
|
|
968
|
+
- { key: completed, category: done }
|
|
969
|
+
- { key: cancelled, category: lost, humanOnly: true }
|
|
581
970
|
properties:
|
|
582
971
|
- slug: campaign-status
|
|
583
972
|
label: Status
|
|
@@ -627,7 +1016,7 @@ profiles:
|
|
|
627
1016
|
# `campaign-channel` (the delivery medium). scoping/outreach/live have no
|
|
628
1017
|
# channel equivalent, so folding them into `campaign-channel` would lose
|
|
629
1018
|
# them — they get their own def. Kept verbatim across the three campaign
|
|
630
|
-
# declarations (
|
|
1019
|
+
# declarations (crm / marketing / here) so there is no drift.
|
|
631
1020
|
- slug: campaign-type
|
|
632
1021
|
label: Type
|
|
633
1022
|
valueType: string
|
|
@@ -793,6 +1182,30 @@ profiles:
|
|
|
793
1182
|
- usage-based
|
|
794
1183
|
- free
|
|
795
1184
|
- other
|
|
1185
|
+
# "Starting at" list price. The deal/contract stays authoritative for what
|
|
1186
|
+
# a buyer actually pays; these three only answer "what do we advertise".
|
|
1187
|
+
- slug: offer-list-price
|
|
1188
|
+
label: Starting at
|
|
1189
|
+
valueType: number
|
|
1190
|
+
inputType: number
|
|
1191
|
+
placeholder: "List price, before any negotiation"
|
|
1192
|
+
- slug: offer-currency
|
|
1193
|
+
label: Currency
|
|
1194
|
+
valueType: string
|
|
1195
|
+
inputType: select
|
|
1196
|
+
enumValues:
|
|
1197
|
+
- EUR
|
|
1198
|
+
- USD
|
|
1199
|
+
- GBP
|
|
1200
|
+
- CHF
|
|
1201
|
+
constraints: { defaultValue: EUR }
|
|
1202
|
+
- slug: offer-billing
|
|
1203
|
+
label: Billing
|
|
1204
|
+
valueType: string
|
|
1205
|
+
inputType: select
|
|
1206
|
+
enumValues:
|
|
1207
|
+
- one-off
|
|
1208
|
+
- monthly
|
|
796
1209
|
- slug: offer-audience
|
|
797
1210
|
label: Audience
|
|
798
1211
|
valueType: entity_id
|
|
@@ -804,6 +1217,334 @@ profiles:
|
|
|
804
1217
|
targetProfileSlug: positioning
|
|
805
1218
|
inputType: entity_select
|
|
806
1219
|
|
|
1220
|
+
# ── The video / visual pipeline (moved from content-studio.yaml) ──────────
|
|
1221
|
+
# HyperFrames-first: a composition IS a HyperFrames project — brief +
|
|
1222
|
+
# storyboard documents, the project zip, the rendered MP4 and its key-frame
|
|
1223
|
+
# stills, and the hash of the brand kit it was made with. Remotion is RETIRED
|
|
1224
|
+
# (Content x Brand plan, 2026-10-05); live rows are retired through
|
|
1225
|
+
# conversions/manifest.ts (`w11.content.*`), never by dropping a field here
|
|
1226
|
+
# (the reconciler is ADD-ONLY).
|
|
1227
|
+
#
|
|
1228
|
+
# SCOPE: workspace (omitted), exactly as content-studio declared them. Only
|
|
1229
|
+
# the ONE Content space uses these kinds, and `shared` would bind the whole
|
|
1230
|
+
# corpus to this body forever (the shared-kind SSOT rule) — the one-way door
|
|
1231
|
+
# the `journey-stage` note above declines for the same reason. Bodies are
|
|
1232
|
+
# VERBATIM from content-studio.yaml so a pod that ever installed it converges
|
|
1233
|
+
# on one body (same-actor reuse in resolve-profile-for-apply.ts), no drift.
|
|
1234
|
+
# `recipe-ref` / `composition-ref` stay free text verbatim: a pod's live rows
|
|
1235
|
+
# carry them, and `composition-ref` is what the Content app joins renders on.
|
|
1236
|
+
# `recipe-ref` was `pipeline-ref` until 2026-10-11 ("pipeline" is now only the
|
|
1237
|
+
# view); live rows move via conversions `w12.content.pipeline-ref-to-recipe-ref`.
|
|
1238
|
+
- slug: content-composition
|
|
1239
|
+
displayName: Composition
|
|
1240
|
+
icon: clapperboard
|
|
1241
|
+
color: blue
|
|
1242
|
+
description: "A HyperFrames project: brief + storyboard, the project zip, the rendered MP4, its key-frame stills, and the brand kit it used."
|
|
1243
|
+
semanticSlug: content.composition
|
|
1244
|
+
# How it renders (entity rendering, decision 8): @synap-core/types/presentation.
|
|
1245
|
+
presentation:
|
|
1246
|
+
family: document
|
|
1247
|
+
properties:
|
|
1248
|
+
- slug: composition-id
|
|
1249
|
+
label: Composition ID
|
|
1250
|
+
valueType: string
|
|
1251
|
+
inputType: text
|
|
1252
|
+
description: Internal id for this composition record.
|
|
1253
|
+
# HyperFrames renders video; `layered` is a carousel / image built as a
|
|
1254
|
+
# layered composition in the pod, `open-design` a deck or PDF document
|
|
1255
|
+
# built in Open Design (founder D1, 2 Nov; skills carousel-format and
|
|
1256
|
+
# open-design-edit). `remotion` / `hybrid` are retired (reconcile is
|
|
1257
|
+
# add-only — live enums keep them until retired by hand).
|
|
1258
|
+
- slug: render-engine
|
|
1259
|
+
label: Render engine
|
|
1260
|
+
valueType: string
|
|
1261
|
+
inputType: select
|
|
1262
|
+
enumValues: [hyperframes, layered, open-design]
|
|
1263
|
+
constraints: { defaultValue: hyperframes }
|
|
1264
|
+
- slug: aspect-ratio
|
|
1265
|
+
label: Aspect Ratio
|
|
1266
|
+
valueType: string
|
|
1267
|
+
inputType: select
|
|
1268
|
+
enumValues: ["9:16", "16:9", "1:1", "4:5"]
|
|
1269
|
+
constraints: { defaultValue: "16:9" }
|
|
1270
|
+
- slug: duration-seconds
|
|
1271
|
+
label: Duration (seconds)
|
|
1272
|
+
valueType: number
|
|
1273
|
+
inputType: number
|
|
1274
|
+
# The HyperFrames workflow's own documents (pod document ids, the same
|
|
1275
|
+
# spelling Brand Library uses for `asset-document-id`).
|
|
1276
|
+
- slug: brief-document-id
|
|
1277
|
+
label: Brief
|
|
1278
|
+
valueType: string
|
|
1279
|
+
inputType: text
|
|
1280
|
+
description: Document id of the creative brief.
|
|
1281
|
+
- slug: storyboard-document-id
|
|
1282
|
+
label: Storyboard
|
|
1283
|
+
valueType: string
|
|
1284
|
+
inputType: text
|
|
1285
|
+
description: Document id of the storyboard / script.
|
|
1286
|
+
# Outputs are pod FILES (uploaded through the one upload door), linked
|
|
1287
|
+
# so each is a door to its own page.
|
|
1288
|
+
- slug: project-zip
|
|
1289
|
+
label: Project (zip)
|
|
1290
|
+
valueType: entity_id
|
|
1291
|
+
targetProfileSlug: file
|
|
1292
|
+
inputType: entity_select
|
|
1293
|
+
description: The HyperFrames project folder, zipped — "edit as code".
|
|
1294
|
+
- slug: render-mp4
|
|
1295
|
+
label: Render (MP4)
|
|
1296
|
+
valueType: entity_id
|
|
1297
|
+
targetProfileSlug: file
|
|
1298
|
+
inputType: entity_select
|
|
1299
|
+
description: The rendered video.
|
|
1300
|
+
- slug: key-frame-stills
|
|
1301
|
+
label: Key-frame stills
|
|
1302
|
+
valueType: array
|
|
1303
|
+
description: File entity ids of key-frame stills — what relay approves from.
|
|
1304
|
+
- slug: brand-kit-hash
|
|
1305
|
+
label: Brand kit hash
|
|
1306
|
+
valueType: string
|
|
1307
|
+
inputType: text
|
|
1308
|
+
description: Content hash of the brand kit export this was made with (GET /api/hub/brand/kit). A different current hash = stale brand.
|
|
1309
|
+
- slug: hyperframes-meta
|
|
1310
|
+
label: HyperFrames JSON
|
|
1311
|
+
valueType: string
|
|
1312
|
+
inputType: textarea
|
|
1313
|
+
- slug: composition-template
|
|
1314
|
+
label: Template
|
|
1315
|
+
valueType: entity_id
|
|
1316
|
+
targetProfileSlug: brand-template
|
|
1317
|
+
inputType: entity_select
|
|
1318
|
+
description: The reusable brand-template shell this started from.
|
|
1319
|
+
- slug: recipe-ref
|
|
1320
|
+
label: Recipe
|
|
1321
|
+
valueType: string
|
|
1322
|
+
inputType: text
|
|
1323
|
+
|
|
1324
|
+
- slug: content-asset
|
|
1325
|
+
displayName: Asset
|
|
1326
|
+
icon: image
|
|
1327
|
+
color: green
|
|
1328
|
+
description: "Media asset: image, video clip, audio track, font, or script."
|
|
1329
|
+
semanticSlug: content.asset
|
|
1330
|
+
# How it renders (entity rendering, decision 8): @synap-core/types/presentation.
|
|
1331
|
+
presentation:
|
|
1332
|
+
family: media
|
|
1333
|
+
media:
|
|
1334
|
+
source: { property: document-id }
|
|
1335
|
+
properties:
|
|
1336
|
+
# `recording` (D1, 29 Oct): a TAKE recorded in Content Studio's booth, or
|
|
1337
|
+
# a PIECE cut from one into its own new file (D2). A piece is told apart
|
|
1338
|
+
# by `source-take`; one Kind keeps one player and one transcript door.
|
|
1339
|
+
- slug: asset-kind
|
|
1340
|
+
label: Kind
|
|
1341
|
+
valueType: string
|
|
1342
|
+
inputType: select
|
|
1343
|
+
enumValues: [image, video, audio, font, script, recording, other]
|
|
1344
|
+
- slug: source-kind
|
|
1345
|
+
label: Source
|
|
1346
|
+
valueType: string
|
|
1347
|
+
inputType: select
|
|
1348
|
+
enumValues: [uploaded, brand, generated]
|
|
1349
|
+
constraints: { defaultValue: uploaded }
|
|
1350
|
+
- slug: source-url
|
|
1351
|
+
label: URL
|
|
1352
|
+
valueType: string
|
|
1353
|
+
inputType: url
|
|
1354
|
+
- slug: brand-asset-ref
|
|
1355
|
+
label: Brand Asset Ref
|
|
1356
|
+
valueType: string
|
|
1357
|
+
inputType: text
|
|
1358
|
+
- slug: file-id
|
|
1359
|
+
label: File ID
|
|
1360
|
+
valueType: string
|
|
1361
|
+
inputType: text
|
|
1362
|
+
- slug: document-id
|
|
1363
|
+
label: Document ID
|
|
1364
|
+
valueType: string
|
|
1365
|
+
inputType: text
|
|
1366
|
+
# ── A recording: a take, or a piece cut from one (D1, D2, 29 Oct) ──────
|
|
1367
|
+
# `file-id` is the raw take's file, or the piece's OWN new file. The raw
|
|
1368
|
+
# take is never overwritten. The facts come from the device that
|
|
1369
|
+
# recorded it (a browser WebM carries no duration), else from ffprobe.
|
|
1370
|
+
- slug: duration-seconds
|
|
1371
|
+
label: Duration (seconds)
|
|
1372
|
+
valueType: number
|
|
1373
|
+
inputType: number
|
|
1374
|
+
- slug: frame-width
|
|
1375
|
+
label: Width (px)
|
|
1376
|
+
valueType: number
|
|
1377
|
+
inputType: number
|
|
1378
|
+
- slug: frame-height
|
|
1379
|
+
label: Height (px)
|
|
1380
|
+
valueType: number
|
|
1381
|
+
inputType: number
|
|
1382
|
+
- slug: frame-rate
|
|
1383
|
+
label: Frame rate (fps)
|
|
1384
|
+
valueType: number
|
|
1385
|
+
inputType: number
|
|
1386
|
+
- slug: recorded-at
|
|
1387
|
+
label: Recorded at
|
|
1388
|
+
valueType: date
|
|
1389
|
+
inputType: date
|
|
1390
|
+
- slug: poster-file-id
|
|
1391
|
+
label: Poster
|
|
1392
|
+
valueType: string
|
|
1393
|
+
inputType: text
|
|
1394
|
+
description: File id of one still frame of the recording.
|
|
1395
|
+
# The take's own facts. The week is derived from `recorded-at`.
|
|
1396
|
+
- slug: take-kind
|
|
1397
|
+
label: Take
|
|
1398
|
+
valueType: string
|
|
1399
|
+
inputType: select
|
|
1400
|
+
enumValues: [answer, free]
|
|
1401
|
+
description: "answer: it answers an interview question; free: a free take."
|
|
1402
|
+
- slug: take-topic
|
|
1403
|
+
label: Question or topic
|
|
1404
|
+
valueType: string
|
|
1405
|
+
inputType: text
|
|
1406
|
+
description: The question the take answers, or the free take's topic.
|
|
1407
|
+
- slug: take-look
|
|
1408
|
+
label: Look (JSON)
|
|
1409
|
+
valueType: string
|
|
1410
|
+
inputType: textarea
|
|
1411
|
+
description: 'The look picked in the booth before recording, the JSON Produce Content takes as its look param: {"background": …, "colour": …}.'
|
|
1412
|
+
# A piece's lineage (G12). Seconds are in the TAKE's time.
|
|
1413
|
+
- slug: source-take
|
|
1414
|
+
label: Cut from take
|
|
1415
|
+
valueType: entity_id
|
|
1416
|
+
targetProfileSlug: content-asset
|
|
1417
|
+
inputType: entity_select
|
|
1418
|
+
- slug: source-start
|
|
1419
|
+
label: Starts at (s)
|
|
1420
|
+
valueType: number
|
|
1421
|
+
inputType: number
|
|
1422
|
+
- slug: source-end
|
|
1423
|
+
label: Ends at (s)
|
|
1424
|
+
valueType: number
|
|
1425
|
+
inputType: number
|
|
1426
|
+
- slug: piece-hook
|
|
1427
|
+
label: Hook
|
|
1428
|
+
valueType: string
|
|
1429
|
+
inputType: text
|
|
1430
|
+
description: The piece's opening line, in the person's exact words.
|
|
1431
|
+
- slug: piece-format
|
|
1432
|
+
label: Suggested format
|
|
1433
|
+
valueType: string
|
|
1434
|
+
inputType: select
|
|
1435
|
+
enumValues: ["Short Video", "Long Video"]
|
|
1436
|
+
# A re-cut is a NEW file: `file-id` moves to it and the old file id is
|
|
1437
|
+
# appended here, oldest first. Nothing is deleted.
|
|
1438
|
+
- slug: earlier-file-ids
|
|
1439
|
+
label: Earlier cuts
|
|
1440
|
+
valueType: array
|
|
1441
|
+
description: File ids of this piece's earlier cuts, oldest first.
|
|
1442
|
+
|
|
1443
|
+
- slug: render-job
|
|
1444
|
+
displayName: Render Job
|
|
1445
|
+
icon: play
|
|
1446
|
+
color: amber
|
|
1447
|
+
description: "One render run of a composition — on the user's own agent (HyperFrames, local) — queued, rendering, done, or failed."
|
|
1448
|
+
semanticSlug: content.render
|
|
1449
|
+
# How it renders (entity rendering, decision 8): @synap-core/types/presentation.
|
|
1450
|
+
presentation:
|
|
1451
|
+
family: media
|
|
1452
|
+
media:
|
|
1453
|
+
source: { entityRef: output-file }
|
|
1454
|
+
properties:
|
|
1455
|
+
- slug: job-status
|
|
1456
|
+
label: Status
|
|
1457
|
+
valueType: string
|
|
1458
|
+
inputType: select
|
|
1459
|
+
enumValues: [queued, rendering, done, failed]
|
|
1460
|
+
constraints: { defaultValue: queued }
|
|
1461
|
+
- slug: render-progress
|
|
1462
|
+
label: Progress (%)
|
|
1463
|
+
valueType: number
|
|
1464
|
+
inputType: number
|
|
1465
|
+
description: 0-100 while rendering, when the agent reports it.
|
|
1466
|
+
- slug: output-format
|
|
1467
|
+
label: Format
|
|
1468
|
+
valueType: string
|
|
1469
|
+
inputType: select
|
|
1470
|
+
enumValues: [mp4, gif, webm, mp3, pdf, png, pptx]
|
|
1471
|
+
constraints: { defaultValue: mp4 }
|
|
1472
|
+
- slug: output-file
|
|
1473
|
+
label: Output
|
|
1474
|
+
valueType: entity_id
|
|
1475
|
+
targetProfileSlug: file
|
|
1476
|
+
inputType: entity_select
|
|
1477
|
+
- slug: output-url
|
|
1478
|
+
label: Output URL
|
|
1479
|
+
valueType: string
|
|
1480
|
+
inputType: url
|
|
1481
|
+
- slug: render-duration-ms
|
|
1482
|
+
label: Render Duration (ms)
|
|
1483
|
+
valueType: number
|
|
1484
|
+
inputType: number
|
|
1485
|
+
- slug: error-message
|
|
1486
|
+
label: Error
|
|
1487
|
+
valueType: string
|
|
1488
|
+
inputType: textarea
|
|
1489
|
+
- slug: composition-ref
|
|
1490
|
+
label: Composition
|
|
1491
|
+
valueType: string
|
|
1492
|
+
inputType: text
|
|
1493
|
+
|
|
1494
|
+
# ── brand-template (shared SSOT is brand-library.yaml) ──────────────────
|
|
1495
|
+
# RE-DECLARED, not owned — and not twinned: `scope: shared` dedups to Brand
|
|
1496
|
+
# Library's ONE pod-wide row (installed first by the `require: brand-library`
|
|
1497
|
+
# dependency). The declaration is what writes THIS workspace's
|
|
1498
|
+
# `profile_workspace_access` grant; without it `composition-template`'s
|
|
1499
|
+
# `targetProfileSlug: brand-template` resolves to a NULL target (see the meta
|
|
1500
|
+
# header). Only the properties this workspace uses are re-declared (as the
|
|
1501
|
+
# retired content-studio did); bodies must match brand-library's (guarded
|
|
1502
|
+
# by shared-role-ssot.test.ts).
|
|
1503
|
+
- slug: brand-template
|
|
1504
|
+
displayName: Brand Template
|
|
1505
|
+
icon: layout-template
|
|
1506
|
+
color: pink
|
|
1507
|
+
description: Reusable output template for artboards, decks, carousels, banners, documents, video, or generated HTML.
|
|
1508
|
+
semanticSlug: brand.template
|
|
1509
|
+
scope: shared
|
|
1510
|
+
# MIRROR of brand-library's lifecycle — every declaration of a shared kind's
|
|
1511
|
+
# lifecycle must be identical (validate: lifecycle-diverges).
|
|
1512
|
+
lifecycle:
|
|
1513
|
+
property: template-status
|
|
1514
|
+
options:
|
|
1515
|
+
- { key: draft, category: open }
|
|
1516
|
+
- { key: approved, category: done }
|
|
1517
|
+
- { key: deprecated, category: lost }
|
|
1518
|
+
# How it renders (entity rendering, decision 8): @synap-core/types/presentation.
|
|
1519
|
+
presentation:
|
|
1520
|
+
family: document
|
|
1521
|
+
properties:
|
|
1522
|
+
- slug: template-kind
|
|
1523
|
+
label: Kind
|
|
1524
|
+
valueType: string
|
|
1525
|
+
inputType: select
|
|
1526
|
+
enumValues:
|
|
1527
|
+
[artboard, deck, carousel, banner, document, html, social, video, other]
|
|
1528
|
+
- slug: template-status
|
|
1529
|
+
label: Status
|
|
1530
|
+
valueType: string
|
|
1531
|
+
inputType: select
|
|
1532
|
+
enumValues: [draft, approved, deprecated]
|
|
1533
|
+
constraints: { defaultValue: draft }
|
|
1534
|
+
# `layered` = a carousel / image built as a layered composition in the
|
|
1535
|
+
# pod, `open-design` = a deck or PDF document built in Open Design: the
|
|
1536
|
+
# words content-os `render-engine` uses (static edit loop, founder D1,
|
|
1537
|
+
# 2 Nov 2026). Additive only: the reconciler never drops a value.
|
|
1538
|
+
- slug: template-format
|
|
1539
|
+
label: Format
|
|
1540
|
+
valueType: string
|
|
1541
|
+
inputType: select
|
|
1542
|
+
enumValues: [tldraw, html, json, markdown, image, mixed, hyperframes, layered, open-design]
|
|
1543
|
+
- slug: template-config
|
|
1544
|
+
label: Config JSON
|
|
1545
|
+
valueType: string
|
|
1546
|
+
inputType: textarea
|
|
1547
|
+
|
|
807
1548
|
# CURATED views — the editorial spine
|
|
808
1549
|
views:
|
|
809
1550
|
- name: Posts
|
|
@@ -886,11 +1627,34 @@ views:
|
|
|
886
1627
|
type: list
|
|
887
1628
|
scopeProfileSlug: series
|
|
888
1629
|
description: The curriculums you're running — each an ordered arc through one pillar.
|
|
1630
|
+
# The video / visual pipeline (moved from content-studio.yaml). Not in the
|
|
1631
|
+
# sidebar: the Content home (`content-home`) is where videos are made and
|
|
1632
|
+
# read; these are the raw collections behind it. No Templates view — the
|
|
1633
|
+
# templates live in Brand Library's own Templates view (one owner).
|
|
1634
|
+
- name: Compositions
|
|
1635
|
+
slug: content-compositions
|
|
1636
|
+
type: grid
|
|
1637
|
+
scopeProfileSlug: content-composition
|
|
1638
|
+
description: Video compositions ready for rendering.
|
|
1639
|
+
- name: Assets
|
|
1640
|
+
slug: content-assets
|
|
1641
|
+
type: grid
|
|
1642
|
+
scopeProfileSlug: content-asset
|
|
1643
|
+
description: Media assets uploaded or generated.
|
|
1644
|
+
- name: Render Jobs
|
|
1645
|
+
slug: render-jobs
|
|
1646
|
+
type: table
|
|
1647
|
+
scopeProfileSlug: render-job
|
|
1648
|
+
description: Render queue and history.
|
|
889
1649
|
|
|
890
|
-
#
|
|
1650
|
+
# The workspace HOME is THE Content surface (plan W5, founder D1 + D11 — the
|
|
1651
|
+
# CRM precedent): the `content-home` cell, the lens page at this space's scope
|
|
1652
|
+
# (Needs you · Happening · Produced · Data with the coverage map). The same
|
|
1653
|
+
# cell is content-studio's home, so the two templates share ONE surface.
|
|
891
1654
|
app:
|
|
892
1655
|
kind: stored
|
|
893
|
-
cellKey:
|
|
1656
|
+
cellKey: content-home
|
|
1657
|
+
title: Content
|
|
894
1658
|
|
|
895
1659
|
# Layout config for browser sidebar
|
|
896
1660
|
layout:
|
|
@@ -929,8 +1693,8 @@ layout:
|
|
|
929
1693
|
label: Newsletters
|
|
930
1694
|
icon: mail
|
|
931
1695
|
|
|
932
|
-
# Bento dashboard
|
|
933
|
-
#
|
|
1696
|
+
# Bento dashboard — the space's editable DASHBOARD (pinned `dashboard`). The
|
|
1697
|
+
# landing surface is the `content-home` cell above (app:), not this bento.
|
|
934
1698
|
# Ordered so a FRESH workspace still says something: the Coverage grid leads
|
|
935
1699
|
# (the "where am I thin" read — a custom cell, `content-coverage-map`, since a
|
|
936
1700
|
# generic matrix can't render an entity_id×entity_id grid's empty cells), then
|
|
@@ -1004,58 +1768,1027 @@ entityLinks:
|
|
|
1004
1768
|
targetProfileSlug: post
|
|
1005
1769
|
type: derived_from
|
|
1006
1770
|
label: Repurposed From
|
|
1771
|
+
# Many-to-many: one idea feeds several posts, several ideas feed one post
|
|
1772
|
+
# (decision B). The "Package ideas" playbook writes it.
|
|
1773
|
+
- sourceProfileSlug: idea
|
|
1774
|
+
targetProfileSlug: post
|
|
1775
|
+
type: feeds
|
|
1776
|
+
label: Feeds
|
|
1777
|
+
- sourceProfileSlug: idea
|
|
1778
|
+
targetProfileSlug: pillar
|
|
1779
|
+
type: belongs_to_pillar
|
|
1780
|
+
label: Pillar
|
|
1781
|
+
# The video pipeline edges (moved from content-studio.yaml). content-studio's
|
|
1782
|
+
# `made_with_recipe` edge is NOT carried: its recipe kind was folded into
|
|
1783
|
+
# Brand Library's `brand-template` (decision D2, 2026-10-12), which the
|
|
1784
|
+
# composition already names through `composition-template`. The `post` is
|
|
1785
|
+
# this space's unit of work, and Produce Content already says to "record a
|
|
1786
|
+
# content-composition linked to this post" — so the composition points at its
|
|
1787
|
+
# post instead.
|
|
1788
|
+
- sourceProfileSlug: content-composition
|
|
1789
|
+
targetProfileSlug: post
|
|
1790
|
+
type: made_for
|
|
1791
|
+
label: Post
|
|
1792
|
+
- sourceProfileSlug: render-job
|
|
1793
|
+
targetProfileSlug: content-composition
|
|
1794
|
+
type: renders
|
|
1795
|
+
label: Composition
|
|
1796
|
+
- sourceProfileSlug: content-asset
|
|
1797
|
+
targetProfileSlug: render-job
|
|
1798
|
+
type: used_in
|
|
1799
|
+
label: Render Job
|
|
1800
|
+
- sourceProfileSlug: content-asset
|
|
1801
|
+
targetProfileSlug: content-composition
|
|
1802
|
+
type: used_in
|
|
1803
|
+
label: Composition
|
|
1007
1804
|
|
|
1008
1805
|
# Playbooks — session templates seeded with the workspace.
|
|
1009
|
-
# `subjectProfile` binds
|
|
1010
|
-
#
|
|
1011
|
-
#
|
|
1012
|
-
#
|
|
1806
|
+
# `subjectProfile` binds a playbook to a kind so the Capture → Session matcher
|
|
1807
|
+
# (playbooks.subject_profile->>'profileSlug') can offer it when one is captured.
|
|
1808
|
+
#
|
|
1809
|
+
# THE PROCESS CONTRACT (lifecycle-bound playbooks). `subjectProfile.statusProperty`
|
|
1810
|
+
# names the kind's lifecycle select (`post-status`); each stage's `subjectStatus`
|
|
1811
|
+
# is the value that stage IS — advancing into the stage writes it to the subject
|
|
1812
|
+
# (governed), and the subject entering it advances an open run to the stage.
|
|
1813
|
+
# `humanOnlyStatuses` lists the values no stage owns because a person sets them.
|
|
1814
|
+
# `activators` are DECLARATIONS the pod compiles into governed propose-mode rules
|
|
1815
|
+
# at install — never hand-written `automations:`. `relationToSubject` is the edge
|
|
1816
|
+
# written output → subject when a slot is satisfied. Validated by validate.ts
|
|
1817
|
+
# (`checkProcessContract`) and lifecycle-coverage.test.ts.
|
|
1013
1818
|
playbooks:
|
|
1014
1819
|
- name: Produce Content
|
|
1015
|
-
|
|
1820
|
+
# ONE playbook for every format (plan W3.3): text / visual / video are
|
|
1821
|
+
# BRANCHES of it, not sibling playbooks — never a second top-level
|
|
1822
|
+
# "Produce video". Its only home is this template (decision D6: the
|
|
1823
|
+
# content-idea-to-production capability no longer carries a copy).
|
|
1824
|
+
description: Take a briefed post from brief to script to published in any format — text, visual or video (HyperFrames) — from the pod's brand kit, one stage per post status, with a person approving the script before anything is generated.
|
|
1016
1825
|
goalTemplate: >-
|
|
1017
|
-
Produce content for {platform} from this idea ({post})
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1826
|
+
Produce content for {platform} from this idea ({post}) in the {format} format.
|
|
1827
|
+
Read the brand first from the pod's brand kit (GET /api/hub/brand/kit) — never
|
|
1828
|
+
re-derive it from a website capture — and load the creative-director skill
|
|
1829
|
+
(linked to this space) to pick the approved brand-template (layout and
|
|
1830
|
+
recipe), references and assets for this format and style; drafts never feed
|
|
1831
|
+
generation. Work the stages in order; each stage moves the post's status.
|
|
1832
|
+
SCRIPT before anything is made: write the script or outline for the format
|
|
1833
|
+
(carousel slide outline, deck outline, video script and storyboard, or the
|
|
1834
|
+
text draft) by filling the narrative frame for the post's goal — the
|
|
1835
|
+
approved content-guideline of type frame for that goal, else the default
|
|
1836
|
+
frame (sell: story, example, lesson, link; hot-take: claim, tension, reason,
|
|
1837
|
+
invitation; tutorial: promise, steps, result, next; product-demo: problem,
|
|
1838
|
+
real screen, outcome, try it). A person approves the script before the
|
|
1839
|
+
draft; nothing is generated from an unapproved script. At the draft, run the
|
|
1840
|
+
creative-director readiness check first (approved references, an approved
|
|
1841
|
+
base guideline, an approved brand-template for this format); if anything is
|
|
1842
|
+
missing, stop and route to the content-taste skill or brand onboarding
|
|
1843
|
+
instead of improvising. Then branch on the format. TEXT
|
|
1844
|
+
(article, thread, newsletter section, caption): draft it with a
|
|
1845
|
+
scroll-stopping hook and a clear CTA. VISUAL: build it pass by pass through
|
|
1846
|
+
the edit-loop skill, a person answering each pass — a carousel, LinkedIn
|
|
1847
|
+
PDF or image post as a layered composition saved on the post
|
|
1848
|
+
(carousel-format), a deck, PDF document or presentation in Open Design
|
|
1849
|
+
(open-design-edit). VIDEO (short or
|
|
1850
|
+
long video): first pick the route — FROM SCRATCH (script, storyboard,
|
|
1851
|
+
frames), FROM A VOICE (an existing voice-over or talk: transcribe it, cut on
|
|
1852
|
+
its beats, build the frames under it) or RE-EDIT (existing footage plus a
|
|
1853
|
+
voice: cut, caption, add music and motion); takes ({takes}) mean RE-EDIT,
|
|
1854
|
+
and the Draft stage says how takes, transcripts and passes are handled.
|
|
1855
|
+
Then run the HyperFrames
|
|
1856
|
+
workflow on your own agent with HYPERFRAMES_NO_TELEMETRY=1, from the kit's
|
|
1857
|
+
frame.md (format=frame-md), with GSAP vendored locally — never `hyperframes
|
|
1858
|
+
publish`, never a cloud render. For a visual or video format, record a
|
|
1859
|
+
content-composition made for this post (for a video: the brief and storyboard
|
|
1860
|
+
documents, the project zip, the MP4, key-frame stills and the brand-kit hash).
|
|
1861
|
+
Keep every format on the post's pillar and the brand voice. A human approves
|
|
1862
|
+
the script before the draft, each pass as it is built, and the
|
|
1863
|
+
draft on the preview at Review. At
|
|
1864
|
+
Schedule, a post whose account is a Postiz channel is queued through the
|
|
1865
|
+
capability that serves publish_post (postiz_schedule_post); that queueing is
|
|
1866
|
+
itself a proposal a person approves. Nothing publishes itself, and the post
|
|
1867
|
+
reads Published only once it went live: reported by Postiz, or confirmed
|
|
1868
|
+
by a person at Publish.
|
|
1021
1869
|
subjectProfile:
|
|
1022
1870
|
profileSlug: post
|
|
1871
|
+
statusProperty: post-status
|
|
1872
|
+
# Idea is where a capture lands and Archived is a person's call — no stage
|
|
1873
|
+
# owns either. The activator below starts the run once the post is Briefed.
|
|
1874
|
+
humanOnlyStatuses: [Idea, Archived]
|
|
1875
|
+
activators:
|
|
1876
|
+
- { on: enters_status, status: Briefed, mode: propose }
|
|
1023
1877
|
params:
|
|
1024
1878
|
- { name: post, type: entity, label: Post, required: true }
|
|
1025
1879
|
- { name: platform, type: text, label: Platform, required: true }
|
|
1026
1880
|
- { name: format, type: text, label: Format }
|
|
1881
|
+
# Recorded footage, as `file` entity ids (comma-separated). A TEXT param,
|
|
1882
|
+
# not `type: entity`: the pod's param contract has no list and no kind
|
|
1883
|
+
# filter (`PlaybookParamType`, @synap/playbooks), and the reader drops any
|
|
1884
|
+
# other key without a word, so "entity, file, multi" cannot be declared.
|
|
1885
|
+
# Present → the video route is RE-EDIT.
|
|
1886
|
+
- { name: takes, type: text, label: Takes (file ids) }
|
|
1887
|
+
# The look picked in Content Studio's booth before recording (founder,
|
|
1888
|
+
# 2026-10-28), as ONE JSON string: {"background": {"assetId","fileId"} |
|
|
1889
|
+
# {"mode":"blur"} | null, "colour": {"assetId","fileId","intensity"} |
|
|
1890
|
+
# null}. A TEXT param for the same reason as `takes`: the param contract
|
|
1891
|
+
# has no object type, and a `text` value must be a string. Content Studio
|
|
1892
|
+
# sends it only to a process that declares it. Present → the Background
|
|
1893
|
+
# and Colour passes start from it; absent → they are built as before.
|
|
1894
|
+
- { name: look, type: text, label: Look (JSON) }
|
|
1027
1895
|
executor: is-agent
|
|
1028
1896
|
status: active
|
|
1897
|
+
# What the process needs the pod to be able to do. Schedule queues the post
|
|
1898
|
+
# through whatever serves publish_post (the `postiz` capability's
|
|
1899
|
+
# postiz_schedule_post); a pod with nothing serving it reports the gap.
|
|
1900
|
+
requiredIntents: [publish_post]
|
|
1901
|
+
inputStrategy:
|
|
1902
|
+
kind: pinned
|
|
1903
|
+
fallback:
|
|
1904
|
+
filter:
|
|
1905
|
+
post-status: Idea
|
|
1906
|
+
orderBy: createdAt
|
|
1907
|
+
orderDir: asc
|
|
1908
|
+
stages:
|
|
1909
|
+
- key: brief
|
|
1910
|
+
name: Brief
|
|
1911
|
+
category: planned
|
|
1912
|
+
subjectStatus: Briefed
|
|
1913
|
+
# A run is BORN at its first stage, and nothing writes a stage's status
|
|
1914
|
+
# at birth (finding f36420d5: the pilot post stayed at Idea while its
|
|
1915
|
+
# run reached Draft). Said here, not only in goalTemplate: a template
|
|
1916
|
+
# update leaves an owner-edited goal alone.
|
|
1917
|
+
goal: >-
|
|
1918
|
+
Pin down the post's angle, platform, format, pillar and journey stage
|
|
1919
|
+
before anything is drafted. A run starts at this stage without moving
|
|
1920
|
+
the post: if the post still reads Idea, move it to Briefed first
|
|
1921
|
+
(synap_update_entity, post-status Briefed, expectedStatus Idea). Then
|
|
1922
|
+
advance the run through every stage in order (currentStage), never
|
|
1923
|
+
skipping one, takes included: entering a stage moves the post to its
|
|
1924
|
+
status, and a reply that says proposed is success.
|
|
1925
|
+
criteria:
|
|
1926
|
+
- key: brief-has-hook
|
|
1927
|
+
statement: The post has a hook or headline that states its one idea.
|
|
1928
|
+
check: { kind: judge }
|
|
1929
|
+
# One criterion for the whole placement (format, platform, pillar,
|
|
1930
|
+
# goal): the playbook sits at MAX_SESSION_CRITERIA (12) and the
|
|
1931
|
+
# script stage needed two.
|
|
1932
|
+
- key: brief-has-format-platform
|
|
1933
|
+
statement: The post's format, platform, pillar and goal are set.
|
|
1934
|
+
check: { kind: judge }
|
|
1935
|
+
# The written version, before anything is generated (D4, 2026-10-12).
|
|
1936
|
+
- key: script
|
|
1937
|
+
name: Script
|
|
1938
|
+
category: started
|
|
1939
|
+
subjectStatus: Scripted
|
|
1940
|
+
goal: >-
|
|
1941
|
+
Write the script or outline for the post's format — carousel slide
|
|
1942
|
+
outline, deck outline, video script and storyboard, or the text draft —
|
|
1943
|
+
by filling the narrative frame for the post's goal (the approved
|
|
1944
|
+
content-guideline of type frame for that goal, else the default frame).
|
|
1945
|
+
With takes (the RE-EDIT route), the script is the cut plan: what is
|
|
1946
|
+
kept of the takes' words, in order, and why (the facecam-edit skill's
|
|
1947
|
+
plan), so the person approves the cut before any pass is built.
|
|
1948
|
+
expectedOutputs:
|
|
1949
|
+
# A pod document. `relates_to` is the default relation (base.yaml):
|
|
1950
|
+
# the composition's brief/storyboard documents are stored as ids on
|
|
1951
|
+
# the composition, not as an edge, so there is no narrower one to reuse.
|
|
1952
|
+
- kind: document
|
|
1953
|
+
label: Script
|
|
1954
|
+
relationToSubject: relates_to
|
|
1955
|
+
criteria:
|
|
1956
|
+
- key: script-follows-frame
|
|
1957
|
+
statement: The script follows the narrative frame for the post's goal and ends on its call to action.
|
|
1958
|
+
check: { kind: judge }
|
|
1959
|
+
- key: script-fits-format
|
|
1960
|
+
statement: The script is shaped for the post's format (slides, scenes or sections) and the platform's limits.
|
|
1961
|
+
check: { kind: judge }
|
|
1962
|
+
- key: draft
|
|
1963
|
+
name: Draft
|
|
1964
|
+
category: started
|
|
1965
|
+
subjectStatus: In Draft
|
|
1966
|
+
# Per-pass approval (founder D3, 2026-10-27): ONE person-owned slot per
|
|
1967
|
+
# video pass, NOT a stage per pass (stages move post-status, and the
|
|
1968
|
+
# post lifecycle has no per-pass status). The slots are declared by
|
|
1969
|
+
# the agent as each pass's preview exists, never here in
|
|
1970
|
+
# `expectedOutputs`: a stage's slots are all owed on entry, by every
|
|
1971
|
+
# format (a text post would owe five video approvals, and the person
|
|
1972
|
+
# would be asked before anything is built), and an `optional` slot is
|
|
1973
|
+
# owed by nobody and cannot be turned owed again through the agent's
|
|
1974
|
+
# outcome door. The slot shape is fixed in the edit-loop skill (one
|
|
1975
|
+
# contract for every engine, 2 Nov) and named here, so every run asks
|
|
1976
|
+
# the same questions. Takes and transcripts are spelled here too,
|
|
1977
|
+
# not only in `goalTemplate`: a template update merges field by field
|
|
1978
|
+
# and leaves a field the owner edited alone (`threeWayMergeFields`), so
|
|
1979
|
+
# an install whose goal was edited on the pod still receives `stages`.
|
|
1980
|
+
# The pod caps a stage goal at 5,000 characters (backend
|
|
1981
|
+
# `playbook-stage.ts`); over it every install and update of Content OS
|
|
1982
|
+
# is refused (2026-11-02), and the catalog validator now refuses it too.
|
|
1983
|
+
goal: >-
|
|
1984
|
+
Entering Draft waits on the person: this stage's gate asks them to
|
|
1985
|
+
approve the script, and that approval moves the post to In Draft.
|
|
1986
|
+
Build nothing before it passes.
|
|
1987
|
+
Run the creative-director readiness check, then produce the draft in
|
|
1988
|
+
the post's format from the approved script, the brand kit and the
|
|
1989
|
+
approved brand-template (layout and recipe) for the format. A visual
|
|
1990
|
+
or video format is built pass by pass through the edit-loop skill and
|
|
1991
|
+
its engine's skill: carousel, LinkedIn PDF or image post, a layered
|
|
1992
|
+
composition saved on the post (carousel-format; passes
|
|
1993
|
+
layer-structure, layer-visual, layer-polish, an image the last two);
|
|
1994
|
+
deck, PDF document or presentation, Open Design (open-design-edit,
|
|
1995
|
+
same passes); video, HyperFrames.
|
|
1996
|
+
Takes given (the
|
|
1997
|
+
run's takes param, on the run session at metadata.params.takes:
|
|
1998
|
+
comma-separated file ids of recorded footage) mean the RE-EDIT video
|
|
1999
|
+
route, with the facecam-edit skill. A take's transcript is ONE
|
|
2000
|
+
content-asset, Kind script, whose File ID is the take's file; its
|
|
2001
|
+
body holds the readable text under a "## Transcript" heading, then a
|
|
2002
|
+
"## Words" heading followed by exactly one ```json fenced block with
|
|
2003
|
+
the word-level JSON in HyperFrames' shape [{"text","start","end"}]
|
|
2004
|
+
(seconds; [] until the precise pass fills it). Find it before transcribing: if it exists, reuse
|
|
2005
|
+
it and only fill the word timings; if not, create it and link it to
|
|
2006
|
+
the take's file with a references relation. Never a separate
|
|
2007
|
+
document per take.
|
|
2008
|
+
A VIDEO is built in passes, in this order, each one non-destructive (changing a pass never
|
|
2009
|
+
redoes an approved one): cut and order, background, colour,
|
|
2010
|
+
captions, graphics and animation. Skip a pass the route does not
|
|
2011
|
+
use. A run started in the older order builds the passes it has not done in this
|
|
2012
|
+
order, background first.
|
|
2013
|
+
When a pass starts, declare its slot as yours first (an outcome with
|
|
2014
|
+
the pass's key, kind file, no owner, no ask). After each pass, render
|
|
2015
|
+
its preview into the pod and hand the person ONE slot for it (an
|
|
2016
|
+
outcome) on the same key, layer-<pass> (a video: layer-cut,
|
|
2017
|
+
layer-background, layer-colour, layer-captions or layer-graphics):
|
|
2018
|
+
owner human, ref the preview, ask choose with allowOther, options
|
|
2019
|
+
Approve (recommended), Rework and Use my edit, answered as the
|
|
2020
|
+
edit-loop skill spells out (Approve claims the slot: completeOutput).
|
|
2021
|
+
Build the next pass only after the answer. Use my edit: their edit is
|
|
2022
|
+
the pass; never overwrite or redo their changes. Before each of your
|
|
2023
|
+
passes record a version, work from the diff to the current work (never
|
|
2024
|
+
regenerate from the prompt), never write while a pass is the person's,
|
|
2025
|
+
and acknowledge each answer in the room.
|
|
2026
|
+
With takes, every pass is built in ONE HyperFrames project, the folder
|
|
2027
|
+
edit in the run's work folder (~/synap-work/<this run's session
|
|
2028
|
+
id>/edit), shown live in HyperFrames Studio, whose address is the
|
|
2029
|
+
run's studio outcome, retired (retireOutput studio) whenever Studio
|
|
2030
|
+
stops, as the facecam-edit skill spells out.
|
|
2031
|
+
Every render the person hears, a preview included, carries the cut's
|
|
2032
|
+
audio copied (a remux, never the render's own encode), and a
|
|
2033
|
+
re-encode for size touches the video only, as the facecam-edit skill
|
|
2034
|
+
spells out.
|
|
2035
|
+
A LOOK given (the run's look param, at metadata.params.look: a JSON
|
|
2036
|
+
string {"background": {"assetId","fileId"} or {"mode":"blur"} or
|
|
2037
|
+
null, "colour": {"assetId","fileId","intensity"} or null}, picked in
|
|
2038
|
+
the booth before recording) means the Background pass and the Colour
|
|
2039
|
+
pass START from it, as the facecam-edit skill spells out. Background:
|
|
2040
|
+
matte the person from the raw take with the person's edit_video tool
|
|
2041
|
+
(synap_list_capabilities with intent edit_video), handing it the
|
|
2042
|
+
take's pod file id ({"videoFile": id}; the pod signs it at run
|
|
2043
|
+
time); gate the matte (its frame
|
|
2044
|
+
count and duration match the take, no flicker); defocus the
|
|
2045
|
+
background once; composite with ffmpeg (choke, feather, light wrap);
|
|
2046
|
+
then grain on the background and a light match on the person. The
|
|
2047
|
+
background is the background file (downloaded from the pod by its
|
|
2048
|
+
fileId), or, for mode blur, a blurred copy of the raw take. No edit_video tool, a failed run or a failed gate: matte with
|
|
2049
|
+
npx hyperframes remove-background instead and composite the same way
|
|
2050
|
+
over the same background; that failing
|
|
2051
|
+
too: the raw take, and the pass's question says "background not
|
|
2052
|
+
applied". Colour: download the
|
|
2053
|
+
.cube by its fileId and apply it as the grading block grading.lut
|
|
2054
|
+
(src the .cube, intensity the look's intensity). Each is still its
|
|
2055
|
+
own pass, with its own slot (layer-background, layer-colour) and its
|
|
2056
|
+
own question, asked like every other pass; for these two the options
|
|
2057
|
+
add Remove this pass (the raw take shows through: drop the pass,
|
|
2058
|
+
retire its slot with retireOutput, build the next). A null part of
|
|
2059
|
+
the look leaves its pass as it is without a look. The raw take is
|
|
2060
|
+
never overwritten: a pass writes new files beside it.
|
|
2061
|
+
# A person approves the written version before any generation: the
|
|
2062
|
+
# gate holds In Draft until it passes, and a rejection sends the run
|
|
2063
|
+
# back to rework the script.
|
|
2064
|
+
gate: { kind: human }
|
|
2065
|
+
onFail: { toStage: script }
|
|
2066
|
+
expectedOutputs:
|
|
2067
|
+
# Visual and video formats only: a text draft lives on the post itself.
|
|
2068
|
+
- kind: content-composition
|
|
2069
|
+
label: Composition (visual or video formats)
|
|
2070
|
+
relationToSubject: made_for
|
|
2071
|
+
criteria:
|
|
2072
|
+
- key: draft-brand-kit-read
|
|
2073
|
+
statement: The draft was built from the pod's brand kit and the approved brand-template for its format, not a re-derived brand.
|
|
2074
|
+
check: { kind: judge }
|
|
2075
|
+
- key: draft-matches-format
|
|
2076
|
+
statement: The draft follows the approved script, is in the post's format and fits the platform's constraints.
|
|
2077
|
+
check: { kind: judge }
|
|
2078
|
+
- key: review
|
|
2079
|
+
name: Review
|
|
2080
|
+
category: started
|
|
2081
|
+
subjectStatus: Review
|
|
2082
|
+
goal: Put the draft in front of a person on the preview, with a proposed publish date.
|
|
2083
|
+
gate: { kind: human }
|
|
2084
|
+
onFail: { toStage: draft }
|
|
2085
|
+
criteria:
|
|
2086
|
+
- key: review-date-proposed
|
|
2087
|
+
statement: A publish date is proposed.
|
|
2088
|
+
check: { kind: judge }
|
|
2089
|
+
- key: review-approved
|
|
2090
|
+
statement: A person approved the draft.
|
|
2091
|
+
check: { kind: human }
|
|
2092
|
+
- key: schedule
|
|
2093
|
+
name: Schedule
|
|
2094
|
+
category: started
|
|
2095
|
+
subjectStatus: Scheduled
|
|
2096
|
+
goal: >-
|
|
2097
|
+
Set the publish date and queue the post on its account: its
|
|
2098
|
+
post-account, else the Personal account (account-owner) of its
|
|
2099
|
+
network (founder, 4 Nov). When that platform carries
|
|
2100
|
+
unipile-account-id (Unipile, the default; decision A, 3 Nov:
|
|
2101
|
+
approving is the yes, nothing asks again), arm it for Publish due
|
|
2102
|
+
posts in one write: post-account set to that platform,
|
|
2103
|
+
publish-account-id to its unipile-account-id,
|
|
2104
|
+
publish-as-organization to its linkedin-organization-id when it is
|
|
2105
|
+
a LinkedIn company page, publish-text to the approved caption or
|
|
2106
|
+
text, publish-media to the approved pictures or video as pod file
|
|
2107
|
+
ids in order (at most 10), publish-at-ms to 09:00 Europe/Paris on
|
|
2108
|
+
the publish date in epoch milliseconds, and publish-outcome queued.
|
|
2109
|
+
Publish due posts sends it through unipile_publish_post on that day
|
|
2110
|
+
and writes the result back. For each other account in post-copy-to
|
|
2111
|
+
(the same post) or post-reshare-on (a LinkedIn repost), create one
|
|
2112
|
+
Scheduled post armed the same way on that account, with
|
|
2113
|
+
post-copy-of set to this post and post-send-mode copy or reshare; a
|
|
2114
|
+
repost has empty publish-text and no media, and waits
|
|
2115
|
+
(publish-outcome waiting) until this post is out.
|
|
2116
|
+
When the account is a Postiz channel (it carries
|
|
2117
|
+
postiz-integration-id and postiz-provider), queue it with
|
|
2118
|
+
postiz_schedule_post: type schedule, the publish date, the approved
|
|
2119
|
+
text, the media links in order, and postEntityId set to this post.
|
|
2120
|
+
When a person approves that run, the pod records the returned postId
|
|
2121
|
+
on the post as postiz-post-id. Any other account: queue it where the
|
|
2122
|
+
account lives and say how.
|
|
2123
|
+
criteria:
|
|
2124
|
+
- key: schedule-date-set
|
|
2125
|
+
statement: The post's publish date is set.
|
|
2126
|
+
check: { kind: evidence, evidenceKey: publish-date }
|
|
2127
|
+
# One judge criterion, not a postiz-post-id evidence check: an account
|
|
2128
|
+
# that is not on Postiz (a newsletter, a blog) has no Postiz id.
|
|
2129
|
+
- key: schedule-account-set
|
|
2130
|
+
statement: The post names the account it goes out on and is queued there; on a Unipile account it is armed (publish-account-id and publish-text set, publish-outcome queued); on a Postiz account its postiz-post-id is recorded.
|
|
2131
|
+
check: { kind: judge }
|
|
2132
|
+
- key: publish
|
|
2133
|
+
name: Publish
|
|
2134
|
+
category: completed
|
|
2135
|
+
subjectStatus: Published
|
|
2136
|
+
goal: >-
|
|
2137
|
+
The post is live at its URL. Armed for Unipile, Publish due posts
|
|
2138
|
+
sends it on its day and writes Published, post-url and
|
|
2139
|
+
published-at itself; a send that fails writes publish-outcome failed
|
|
2140
|
+
and publish-error and leaves the post at Scheduled, for a person to
|
|
2141
|
+
fix and arm again. Queued through Postiz, Postiz reports it (its
|
|
2142
|
+
webhook records the URL and Published); otherwise a person confirms
|
|
2143
|
+
it, and approving writes Published.
|
|
2144
|
+
# A HUMAN gate, not a check: the pod holds a gated stage's status until
|
|
2145
|
+
# the gate passes, and a check gate judges the stage being LEFT
|
|
2146
|
+
# (Schedule), so it would pass on entry and write Published before
|
|
2147
|
+
# anyone saw the post go out — firing Repurpose on a post that may
|
|
2148
|
+
# never have shipped. Only a person's approval writes Published.
|
|
2149
|
+
gate: { kind: human }
|
|
2150
|
+
onFail: { toStage: schedule }
|
|
2151
|
+
criteria:
|
|
2152
|
+
- key: publish-live-url
|
|
2153
|
+
statement: The post's published URL is recorded.
|
|
2154
|
+
check: { kind: evidence, evidenceKey: post-url }
|
|
2155
|
+
- key: publish-date-recorded
|
|
2156
|
+
statement: The post's publish date is set to the day it went live.
|
|
2157
|
+
check: { kind: evidence, evidenceKey: publish-date }
|
|
1029
2158
|
|
|
1030
|
-
#
|
|
1031
|
-
#
|
|
1032
|
-
#
|
|
1033
|
-
#
|
|
1034
|
-
#
|
|
1035
|
-
#
|
|
2159
|
+
# One take, several videos (D2, D3, 29 Oct). The take is a content-asset of
|
|
2160
|
+
# Kind recording; it has no lifecycle (its state is derived from what was
|
|
2161
|
+
# made from it), so this process declares no statusProperty and its stages
|
|
2162
|
+
# move no status. The person decides at Keep, through one question per
|
|
2163
|
+
# proposed piece (a slot ask: the door Content Studio and Relay answer), not
|
|
2164
|
+
# a stage gate: keeping a piece commits nothing outside the pod, and the raw
|
|
2165
|
+
# take is never touched. The per-piece slots are declared by the agent as
|
|
2166
|
+
# each piece exists, like Produce Content's pass slots: a stage's own slots
|
|
2167
|
+
# are owed on entry, and the number of pieces is not known before Analyse.
|
|
2168
|
+
- name: Work a take
|
|
2169
|
+
description: Find the videos inside one recorded take, cut the ones the person keeps into new files, and make one video post at Idea for each, on the person's computer, with the raw take never touched.
|
|
2170
|
+
goalTemplate: >-
|
|
2171
|
+
Work this take ({take}): a content-asset of Kind recording whose File ID
|
|
2172
|
+
is the raw take's file. Run on the person's computer, in this run's work
|
|
2173
|
+
folder (~/synap-work/<this run's session id>). Transcribe it precisely,
|
|
2174
|
+
find the pieces inside it (one or more videos that each stand alone),
|
|
2175
|
+
ask the person which to keep, cut each kept piece into its own new video
|
|
2176
|
+
file, and make one video post at Idea per piece. The raw take is never
|
|
2177
|
+
overwritten, moved or re-uploaded. Quote only words the person said;
|
|
2178
|
+
never invent a claim or a number. Nothing is published, and no Produce
|
|
2179
|
+
Content run starts unless the person asks for it.
|
|
2180
|
+
subjectProfile:
|
|
2181
|
+
profileSlug: content-asset
|
|
2182
|
+
params:
|
|
2183
|
+
- { name: take, type: entity, label: Take, required: true }
|
|
2184
|
+
executor: is-agent
|
|
2185
|
+
status: active
|
|
2186
|
+
stages:
|
|
2187
|
+
- key: transcribe
|
|
2188
|
+
name: Transcribe
|
|
2189
|
+
category: started
|
|
2190
|
+
goal: >-
|
|
2191
|
+
Download the take (GET /api/hub/files/<fileId>/url gives a signed
|
|
2192
|
+
URL) and ffprobe it. Fill the take's missing facts:
|
|
2193
|
+
duration-seconds, frame-width, frame-height, frame-rate, and a
|
|
2194
|
+
poster (one still frame as a JPEG, stored with synap_store_file,
|
|
2195
|
+
its id in poster-file-id). The take's transcript is ONE
|
|
2196
|
+
content-asset, Kind script, whose File ID is the take's file; its
|
|
2197
|
+
body holds the readable text under a "## Transcript" heading, then a
|
|
2198
|
+
"## Words" heading followed by exactly one ```json fenced block with
|
|
2199
|
+
the word-level JSON in HyperFrames' shape [{"text","start","end"}]
|
|
2200
|
+
(seconds). Find it before transcribing (Content Studio usually made
|
|
2201
|
+
it): reuse it and only fill the word timings (npx hyperframes
|
|
2202
|
+
transcribe, Parakeet on the Mac); if none exists, create it and link
|
|
2203
|
+
it to the take's file with a references relation. Never transcribe
|
|
2204
|
+
twice, never a separate document.
|
|
2205
|
+
criteria:
|
|
2206
|
+
- key: take-words-timed
|
|
2207
|
+
statement: The take's one transcript content-asset holds word timings for the whole take.
|
|
2208
|
+
check: { kind: judge }
|
|
2209
|
+
- key: take-facts-filled
|
|
2210
|
+
statement: The take records its duration, frame size, frame rate and a poster.
|
|
2211
|
+
check: { kind: judge }
|
|
2212
|
+
- key: analyse
|
|
2213
|
+
name: Analyse
|
|
2214
|
+
category: started
|
|
2215
|
+
goal: >-
|
|
2216
|
+
Read the transcript and find the pieces: each a run of the take that
|
|
2217
|
+
stands alone as one video, with a range whose edges sit on word
|
|
2218
|
+
boundaries (seconds, in the take's time), a working title, its hook
|
|
2219
|
+
(the person's exact opening sentence), a format (Short Video under
|
|
2220
|
+
about 90 seconds, else Long Video) and why it stands alone. A take
|
|
2221
|
+
that holds one video gives one piece. Write ONE document, the
|
|
2222
|
+
analysis, linked to the take with a references relation: the topics,
|
|
2223
|
+
the strong lines and a verdict on the hook, then a "## Pieces"
|
|
2224
|
+
heading followed by exactly one ```json fenced block
|
|
2225
|
+
[{"n","start","end","title","hook","format","why"}] (n from 1, in
|
|
2226
|
+
take order). Then ask about each piece: hand the person ONE slot per
|
|
2227
|
+
piece (an outcome), key keep-piece-<n>; kind decision; owner human;
|
|
2228
|
+
blockedReason decision; why the piece's range, title and hook; ref
|
|
2229
|
+
the analysis (kind document); ask
|
|
2230
|
+
choose with allowOther, options Keep (recommended when it stands
|
|
2231
|
+
alone and fits its format), Drop, and Adjust (the note gives the new
|
|
2232
|
+
range as <start>-<end> in seconds, or its first and last words).
|
|
2233
|
+
Lines worth a post that are not a piece: propose each as a post at
|
|
2234
|
+
Idea that quotes the person's sentence and links to the take with a
|
|
2235
|
+
references relation.
|
|
2236
|
+
expectedOutputs:
|
|
2237
|
+
- kind: document
|
|
2238
|
+
label: Analysis
|
|
2239
|
+
relationToSubject: references
|
|
2240
|
+
criteria:
|
|
2241
|
+
- key: pieces-ranged
|
|
2242
|
+
statement: Each piece has a word-boundary range, a working title, a hook in the person's exact words and a reason it stands alone.
|
|
2243
|
+
check: { kind: judge }
|
|
2244
|
+
- key: pieces-asked
|
|
2245
|
+
statement: Each piece has its own question to the person (keep-piece-n) with Keep, Drop and Adjust.
|
|
2246
|
+
check: { kind: judge }
|
|
2247
|
+
- key: keep
|
|
2248
|
+
name: Keep pieces
|
|
2249
|
+
category: started
|
|
2250
|
+
goal: >-
|
|
2251
|
+
Wait for the person's answer on every keep-piece-<n> question. Keep:
|
|
2252
|
+
the piece is cut as proposed. Drop: nothing is made from it. Adjust:
|
|
2253
|
+
take the range from the note, snap it to word boundaries, and treat
|
|
2254
|
+
the piece as kept. Claim each answered question (completeOutput).
|
|
2255
|
+
Cut only after every question is answered; a piece the person
|
|
2256
|
+
dropped is never cut.
|
|
2257
|
+
criteria:
|
|
2258
|
+
- key: every-piece-answered
|
|
2259
|
+
statement: The person kept, dropped or adjusted every proposed piece.
|
|
2260
|
+
check: { kind: judge }
|
|
2261
|
+
- key: none-cut-before-answers
|
|
2262
|
+
statement: No piece was cut before every keep-piece question was answered.
|
|
2263
|
+
check: { kind: judge }
|
|
2264
|
+
- key: cut
|
|
2265
|
+
name: Cut
|
|
2266
|
+
category: started
|
|
2267
|
+
goal: >-
|
|
2268
|
+
Cut each kept piece, one at a time. Declare its slot as yours first
|
|
2269
|
+
(an outcome, key piece-<n>, kind content-asset, no owner, no ask):
|
|
2270
|
+
that is how the person sees it being cut. Cut with ffmpeg, frame
|
|
2271
|
+
accurate, from the raw take into a NEW file,
|
|
2272
|
+
~/synap-work/<this run's session id>/pieces/<n>.mp4: ffmpeg -ss
|
|
2273
|
+
<start> -to <end> -i <raw take> -vf fps=<frame rate> -c:v libx264
|
|
2274
|
+
-crf 18 -preset medium -c:a aac -b:a 192k -af
|
|
2275
|
+
"afade=t=in:st=0:d=0.03,afade=t=out:st=<end minus start minus
|
|
2276
|
+
0.03>:d=0.03" -movflags +faststart <out> (the 30 ms fades keep the
|
|
2277
|
+
edges from popping). Check it with ffprobe (duration within one
|
|
2278
|
+
frame of end minus start, audio and video present). Upload it into
|
|
2279
|
+
this space (synap_request_upload, PUT the bytes, then
|
|
2280
|
+
synap_store_file with the uploadToken) as a file titled "Piece <n> ·
|
|
2281
|
+
<title> (m:ss–m:ss)". Record the piece: a content-asset, Kind
|
|
2282
|
+
recording, titled with its working title: file-id the new file,
|
|
2283
|
+
source-take the take, source-start and source-end, piece-hook,
|
|
2284
|
+
piece-format, duration-seconds, frame-width, frame-height,
|
|
2285
|
+
frame-rate, a poster (poster-file-id) and recorded-at copied from
|
|
2286
|
+
the take; link it to the take and to its file with references
|
|
2287
|
+
relations. Give it its own transcript in the same ONE shape (File
|
|
2288
|
+
ID = the piece's file): the take's words inside the range, re-timed
|
|
2289
|
+
to the piece (minus source-start). Point the slot at the piece
|
|
2290
|
+
(ref, kind entity) and claim it (completeOutput). The person may ask
|
|
2291
|
+
to adjust a piece after its cut: cut the new range into a NEW file
|
|
2292
|
+
the same way, set file-id to it and the new range, and append the
|
|
2293
|
+
old file id to earlier-file-ids; never delete or overwrite a file.
|
|
2294
|
+
criteria:
|
|
2295
|
+
- key: only-kept-cut
|
|
2296
|
+
statement: Only the kept pieces were cut, each into its own new file, and the raw take is unchanged.
|
|
2297
|
+
check: { kind: judge }
|
|
2298
|
+
- key: piece-lineage
|
|
2299
|
+
statement: Each piece records its take, its range and its own file, and references the take.
|
|
2300
|
+
check: { kind: judge }
|
|
2301
|
+
- key: posts
|
|
2302
|
+
name: Make the posts
|
|
2303
|
+
category: completed
|
|
2304
|
+
goal: >-
|
|
2305
|
+
Make one video post per cut piece, at Idea: post-format the piece's
|
|
2306
|
+
format, post-hook its hook, titled with its working title, linked to
|
|
2307
|
+
the piece with a references relation. Record each as an outcome, key
|
|
2308
|
+
post-<n>, kind post, ref the post (kind entity), and claim it
|
|
2309
|
+
(completeOutput). Start Produce Content for a post only if the
|
|
2310
|
+
person asks (its takes param is then the piece's file id).
|
|
2311
|
+
criteria:
|
|
2312
|
+
- key: one-post-per-piece
|
|
2313
|
+
statement: Each cut piece has one video post at Idea that references it, and no post moved past Idea by itself.
|
|
2314
|
+
check: { kind: judge }
|
|
2315
|
+
- key: nothing-started-unasked
|
|
2316
|
+
statement: Nothing was published, and no Produce Content run started unless the person asked for it.
|
|
2317
|
+
check: { kind: judge }
|
|
2318
|
+
|
|
2319
|
+
# The proactive half: the plan. It reasons over the corpus as a whole, so it
|
|
2320
|
+
# takes no params, and its WORKERS are per_moment: ONE run over every post,
|
|
2321
|
+
# never one run per post (a subject-bound playbook's `schedule:` field fans
|
|
2322
|
+
# out per entity; an activator's `schedule` and a per_moment `manual` do
|
|
2323
|
+
# not: api `run-playbook.ts` runWorkerBatch). The `subjectProfile` names the
|
|
2324
|
+
# kind those workers read; there are no stages and no `statusProperty`, so it
|
|
2325
|
+
# never moves a post's status.
|
|
2326
|
+
# Run by hand ("Re-plan" on Content Studio's calendar, or the pod's button)
|
|
2327
|
+
# and once a week (founder, report 22fec5ee §6, 3 Nov 2026). The weekly one
|
|
2328
|
+
# compiles to a governed rule the owner can pause on the pod; both propose
|
|
2329
|
+
# every write, so the dates arrive as ONE batch the person approves.
|
|
2330
|
+
# The plan's numbers are never in this text: cadence, goal mix, pillar
|
|
2331
|
+
# balance and the campaign are read from the pod, where the person edits them.
|
|
1036
2332
|
- name: Plan Next Content
|
|
1037
|
-
description: Audit the whole corpus against the journey, find the gap,
|
|
2333
|
+
description: Audit the whole corpus against the journey, find the gap, propose what to make next, and propose dates for the posts that have none or whose date has passed.
|
|
1038
2334
|
goalTemplate: >-
|
|
1039
2335
|
Audit the content corpus. Map every post onto its pillar and its journey
|
|
1040
2336
|
stage, and list which pillar × stage cells are empty or thin. Check the
|
|
1041
2337
|
running series for the chapter that is missing. If the pod has offers,
|
|
1042
2338
|
note which ones no content teaches toward; if it has none, plan purely for
|
|
1043
2339
|
audience-building and do not invent commerce. Then propose the next one or
|
|
1044
|
-
two pieces — each as a post
|
|
1045
|
-
platform, with the hook and the reason it is next.
|
|
2340
|
+
two pieces — each created as a post entity at the Idea status, placed on a
|
|
2341
|
+
pillar, a stage and a platform, with the hook and the reason it is next.
|
|
2342
|
+
Then re-plan the calendar. Read the plan's inputs from the pod, never from
|
|
2343
|
+
a number in this text: the active campaign whose dates cover the coming
|
|
2344
|
+
weeks (campaign-start-date, campaign-end-date, its notes), each publishing
|
|
2345
|
+
account's cadence (platform-posting-freq on its platform), each pillar's
|
|
2346
|
+
target (posting-frequency on the pillar) and the goal mix the person set
|
|
2347
|
+
in this space's brief. Propose a publish-date for every post past Idea
|
|
2348
|
+
that has none, and a new publish-date for every post whose date has
|
|
2349
|
+
passed while it is not Published, the longest-waiting first: follow the
|
|
2350
|
+
cadence, balance the pillars and the goals, keep a campaign's posts inside
|
|
2351
|
+
its dates, and never put two posts of one account on the same day. Never
|
|
2352
|
+
move a Published or Archived post, and never change a post's status. File
|
|
2353
|
+
every date as a proposal, all of them in this one run so the person
|
|
2354
|
+
approves them as one batch, each with a one-line reason (the cadence, the
|
|
2355
|
+
gap or the campaign it serves); never apply a date yourself. When an input
|
|
2356
|
+
is missing (no cadence, no goal mix), say which one in the run's summary
|
|
2357
|
+
and plan without it.
|
|
2358
|
+
subjectProfile:
|
|
2359
|
+
profileSlug: post
|
|
2360
|
+
activators:
|
|
2361
|
+
- { on: schedule, cron: "0 9 * * 1", mode: propose }
|
|
2362
|
+
- { on: manual, size: per_moment, mode: propose }
|
|
2363
|
+
# Typed output: each idea is a real `post` at Idea (the start of Produce
|
|
2364
|
+
# Content's lifecycle), not prose in a message. The run works the whole
|
|
2365
|
+
# set, so no edge to a subject.
|
|
2366
|
+
expectedOutputs:
|
|
2367
|
+
- { kind: post, label: Next post idea, relationToSubject: none }
|
|
1046
2368
|
executor: is-agent
|
|
1047
|
-
# Monday 09:00, shipped OFF — the operator opts in. `enabled` is read
|
|
1048
|
-
# STRICTLY (`cron-automation.ts:43` is `s.enabled === true`, and `:194`
|
|
1049
|
-
# skips a falsy one), so OMITTING the key does not mean "on by default":
|
|
1050
|
-
# it means the backing cron automation is never created and this playbook
|
|
1051
|
-
# silently never fires. Declaring it explicitly is the difference between
|
|
1052
|
-
# an opt-in cadence and a dead one. Off is the right default for a
|
|
1053
|
-
# `isPublic: true` template — every installer would otherwise be enrolled
|
|
1054
|
-
# in a recurring agent run (real token cost, a queue of proposals) they
|
|
1055
|
-
# never asked for. Matches `ecosystem.yaml:413,436`, same cadence.
|
|
1056
|
-
schedule: { cron: "0 9 * * 1", enabled: false }
|
|
1057
2369
|
status: active
|
|
1058
2370
|
|
|
2371
|
+
# The idea bank's door (decision B, review 22fec5ee §7): Content Studio's
|
|
2372
|
+
# "Package ideas" button runs it. Like Plan Next Content it reasons over the
|
|
2373
|
+
# whole bank at once, so it has NO `subjectProfile` and no params. It OPENS the
|
|
2374
|
+
# work: it proposes posts and their links in ONE capture the person approves,
|
|
2375
|
+
# and never writes the post. The `feeds` edge is the idea → post entityLink.
|
|
2376
|
+
- name: Package ideas
|
|
2377
|
+
description: Read the idea bank, group the ideas that belong together, and propose a set of posts, each linked to the ideas that feed it, as one batch to approve.
|
|
2378
|
+
goalTemplate: >-
|
|
2379
|
+
Package the idea bank of this space. Read every idea (kind idea) at New or
|
|
2380
|
+
Shaped: its words, its pillar, its journey stage and its age. Group the
|
|
2381
|
+
ideas that belong in one piece (the same claim, the same story, or the
|
|
2382
|
+
same question from the audience); one idea may feed several posts. Then
|
|
2383
|
+
propose one to five posts: each with a working title, a format, a pillar
|
|
2384
|
+
and the one sentence it is about, at the Idea status. Never a script, a
|
|
2385
|
+
hook to publish or a finished text: the person writes the post. File the
|
|
2386
|
+
whole set as ONE synap_capture graph (each post a new entity, each source
|
|
2387
|
+
idea by existingEntityId, linked idea feeds post), so the person approves
|
|
2388
|
+
it in one review. Every post has at least one idea; an idea with no cousin
|
|
2389
|
+
that is not a piece on its own is left out. Once the batch is approved,
|
|
2390
|
+
move each idea that feeds a post to Used (synap_update_entity,
|
|
2391
|
+
expectedStatus its status). Nothing is briefed, scheduled or published,
|
|
2392
|
+
and no Produce Content run starts.
|
|
2393
|
+
# Typed output: each packaged post is a real `post` at Idea, linked from its
|
|
2394
|
+
# ideas (`feeds`, written by the capture). No subject, so no edge to one.
|
|
2395
|
+
expectedOutputs:
|
|
2396
|
+
- { kind: post, label: Packaged post, relationToSubject: none }
|
|
2397
|
+
executor: is-agent
|
|
2398
|
+
status: active
|
|
2399
|
+
|
|
2400
|
+
# Once a post is Published, propose turning it into its siblings (thread →
|
|
2401
|
+
# video → newsletter section). It READS the post lifecycle (its activator) but
|
|
2402
|
+
# never moves it — the new pieces are new posts, linked `derived_from` (the
|
|
2403
|
+
# post → post edge above) — so it declares no stages and owns no status.
|
|
2404
|
+
- name: Repurpose
|
|
2405
|
+
description: Turn a published post into new posts for other formats and platforms, each linked to the original.
|
|
2406
|
+
goalTemplate: >-
|
|
2407
|
+
Repurpose this published post ({post}). Read what it says and how it
|
|
2408
|
+
performed, then propose one to three new pieces in other formats or on
|
|
2409
|
+
other platforms where the same idea travels (a thread from an article, a
|
|
2410
|
+
short video from a thread, a newsletter section from a series). Create each
|
|
2411
|
+
as a new post at the Idea status, on the same pillar, with its own hook,
|
|
2412
|
+
and link it derived_from the original. Never republish the original as is.
|
|
2413
|
+
subjectProfile:
|
|
2414
|
+
profileSlug: post
|
|
2415
|
+
statusProperty: post-status
|
|
2416
|
+
activators:
|
|
2417
|
+
# A copy or repost sent for another account (post-copy-of) is not repurposed: its original is.
|
|
2418
|
+
- { on: enters_status, status: Published, mode: propose, unless: { property: post-copy-of, present: true } }
|
|
2419
|
+
expectedOutputs:
|
|
2420
|
+
- { kind: post, label: Repurposed post, relationToSubject: derived_from }
|
|
2421
|
+
params:
|
|
2422
|
+
- { name: post, type: entity, label: Post, required: true }
|
|
2423
|
+
executor: is-agent
|
|
2424
|
+
status: active
|
|
2425
|
+
|
|
2426
|
+
# A newsletter issue from idea to sent, one stage per `newsletter-status`.
|
|
2427
|
+
# No schedule: a subject-bound playbook on a cron fans out one run per
|
|
2428
|
+
# existing newsletter, which is never what an issue cadence means.
|
|
2429
|
+
- name: Newsletter issue
|
|
2430
|
+
description: Take a newsletter issue from idea to sent — angle, draft, edit, schedule — with a person approving before it goes out.
|
|
2431
|
+
goalTemplate: >-
|
|
2432
|
+
Produce this newsletter issue ({newsletter}). Read the brand voice from the
|
|
2433
|
+
pod's brand kit, then work the stages in order: settle the issue's angle
|
|
2434
|
+
and the posts it draws on, draft it, edit it down with a subject line and
|
|
2435
|
+
preview text, and schedule the send. A person approves the final issue
|
|
2436
|
+
before it is sent, nothing sends itself, and the issue reads Sent only once
|
|
2437
|
+
a person confirms it went out.
|
|
2438
|
+
subjectProfile:
|
|
2439
|
+
profileSlug: newsletter
|
|
2440
|
+
statusProperty: newsletter-status
|
|
2441
|
+
humanOnlyStatuses: [Archived]
|
|
2442
|
+
params:
|
|
2443
|
+
- { name: newsletter, type: entity, label: Newsletter issue, required: true }
|
|
2444
|
+
executor: is-agent
|
|
2445
|
+
status: active
|
|
2446
|
+
stages:
|
|
2447
|
+
- key: idea
|
|
2448
|
+
name: Idea
|
|
2449
|
+
category: backlog
|
|
2450
|
+
subjectStatus: Idea
|
|
2451
|
+
goal: Settle the issue's one angle and the posts or sources it draws on.
|
|
2452
|
+
criteria:
|
|
2453
|
+
- key: issue-has-angle
|
|
2454
|
+
statement: The issue states its one angle in a sentence.
|
|
2455
|
+
check: { kind: judge }
|
|
2456
|
+
- key: issue-has-sources
|
|
2457
|
+
statement: The posts or sources the issue draws on are listed.
|
|
2458
|
+
check: { kind: judge }
|
|
2459
|
+
- key: drafting
|
|
2460
|
+
name: Drafting
|
|
2461
|
+
category: started
|
|
2462
|
+
subjectStatus: Drafting
|
|
2463
|
+
goal: Write the full issue in the brand voice.
|
|
2464
|
+
criteria:
|
|
2465
|
+
- key: draft-complete
|
|
2466
|
+
statement: Every section of the issue is written.
|
|
2467
|
+
check: { kind: judge }
|
|
2468
|
+
- key: draft-brand-voice
|
|
2469
|
+
statement: The issue reads in the brand voice from the kit.
|
|
2470
|
+
check: { kind: judge }
|
|
2471
|
+
- key: editing
|
|
2472
|
+
name: Editing
|
|
2473
|
+
category: started
|
|
2474
|
+
subjectStatus: Editing
|
|
2475
|
+
goal: Tighten the issue and write its subject line and preview text.
|
|
2476
|
+
criteria:
|
|
2477
|
+
- key: edit-subject-line
|
|
2478
|
+
statement: The issue has a subject line.
|
|
2479
|
+
check: { kind: judge }
|
|
2480
|
+
- key: edit-preview-text
|
|
2481
|
+
statement: The issue has preview text of about 45 characters.
|
|
2482
|
+
check: { kind: judge }
|
|
2483
|
+
- key: edit-links-work
|
|
2484
|
+
statement: Every link in the issue resolves.
|
|
2485
|
+
check: { kind: judge }
|
|
2486
|
+
- key: scheduled
|
|
2487
|
+
name: Scheduled
|
|
2488
|
+
category: started
|
|
2489
|
+
subjectStatus: Scheduled
|
|
2490
|
+
goal: Set the send date and queue the issue.
|
|
2491
|
+
criteria:
|
|
2492
|
+
- key: send-date-set
|
|
2493
|
+
statement: The issue's send date is set.
|
|
2494
|
+
check: { kind: evidence, evidenceKey: send-date }
|
|
2495
|
+
- key: send-approved
|
|
2496
|
+
statement: A person approved the final issue.
|
|
2497
|
+
check: { kind: human }
|
|
2498
|
+
- key: sent
|
|
2499
|
+
name: Sent
|
|
2500
|
+
category: completed
|
|
2501
|
+
subjectStatus: Sent
|
|
2502
|
+
goal: A person confirms the issue went out to the list; approving writes Sent.
|
|
2503
|
+
# A HUMAN gate: only a person's approval writes Sent, so the issue
|
|
2504
|
+
# never reads Sent before someone confirmed it went out.
|
|
2505
|
+
gate: { kind: human }
|
|
2506
|
+
onFail: { toStage: editing }
|
|
2507
|
+
criteria:
|
|
2508
|
+
- key: sent-confirmed
|
|
2509
|
+
statement: The issue was sent to the list.
|
|
2510
|
+
check: { kind: judge }
|
|
2511
|
+
- key: sent-date-set
|
|
2512
|
+
statement: The issue's send date is set to the day it went out.
|
|
2513
|
+
check: { kind: evidence, evidenceKey: send-date }
|
|
2514
|
+
- key: sent-numbers
|
|
2515
|
+
statement: Subscriber count at send is recorded.
|
|
2516
|
+
check: { kind: judge }
|
|
2517
|
+
|
|
2518
|
+
automations:
|
|
2519
|
+
- key: content-os-weekly-produce
|
|
2520
|
+
name: Weekly content production
|
|
2521
|
+
description: >
|
|
2522
|
+
Monday 09:00. Runs Produce Content once, with no subject. The playbook
|
|
2523
|
+
takes the first eligible pinned post, or the oldest post still in Idea
|
|
2524
|
+
when the pin list has nobody eligible. Shipped paused because Content OS
|
|
2525
|
+
is public: an active cron would spend a run for every installer. Turn
|
|
2526
|
+
it on to start the cycle. Produce Content's own schedule stays off.
|
|
2527
|
+
triggerType: cron
|
|
2528
|
+
triggerConfig:
|
|
2529
|
+
expression: "0 9 * * 1"
|
|
2530
|
+
status: paused
|
|
2531
|
+
flow:
|
|
2532
|
+
nodes:
|
|
2533
|
+
- id: trigger
|
|
2534
|
+
type: trigger
|
|
2535
|
+
position: { x: 0, y: 0 }
|
|
2536
|
+
data:
|
|
2537
|
+
label: Monday at 09:00
|
|
2538
|
+
triggerType: cron
|
|
2539
|
+
config:
|
|
2540
|
+
expression: "0 9 * * 1"
|
|
2541
|
+
- id: produce
|
|
2542
|
+
type: playbook_run
|
|
2543
|
+
position: { x: 0, y: 160 }
|
|
2544
|
+
data:
|
|
2545
|
+
label: Produce the next idea
|
|
2546
|
+
playbookName: Produce Content
|
|
2547
|
+
edges:
|
|
2548
|
+
- id: e-trigger-produce
|
|
2549
|
+
source: trigger
|
|
2550
|
+
target: produce
|
|
2551
|
+
|
|
2552
|
+
# Decision A (founder, 3 Nov): a Scheduled post goes out through Unipile on
|
|
2553
|
+
# its date, with no second approval. ONE post per run, at the top level: a
|
|
2554
|
+
# capability node inside a loop that fails aborts its item with no failure
|
|
2555
|
+
# branch, and the failure must be written on the post. The query compares
|
|
2556
|
+
# properties numerically only, hence publish-at-ms. Shipped paused, like
|
|
2557
|
+
# every catalog automation: the person turns it on, and lets
|
|
2558
|
+
# unipile_publish_post run without a second approval for it (a run the pod
|
|
2559
|
+
# would propose fails the send, and the post says so).
|
|
2560
|
+
- key: content-os-publish-due-posts
|
|
2561
|
+
name: Publish due posts
|
|
2562
|
+
description: >
|
|
2563
|
+
Every 15 minutes, sends the oldest approved post whose time has come
|
|
2564
|
+
through Unipile, with no second approval. A post is due when it is
|
|
2565
|
+
Scheduled, armed when it was approved (publish-outcome queued) and its
|
|
2566
|
+
publish-at-ms has passed. It is claimed first (publish-outcome
|
|
2567
|
+
sending), so it is never sent twice; then its frozen words, account and
|
|
2568
|
+
pictures (publish-text, publish-account-id, publish-media) go out with
|
|
2569
|
+
unipile_publish_post, as a LinkedIn company page when it names one
|
|
2570
|
+
(publish-as-organization) and as a repost when it is one
|
|
2571
|
+
(publish-quote-id). Sent: Published, published-at, post-url and its
|
|
2572
|
+
Unipile id (publish-post-id), and its reposts waiting for it are
|
|
2573
|
+
queued with that id. Failed: publish-outcome failed and publish-error,
|
|
2574
|
+
the post left at Scheduled. Shipped paused: turn it on to start.
|
|
2575
|
+
triggerType: cron
|
|
2576
|
+
triggerConfig:
|
|
2577
|
+
expression: "*/15 * * * *"
|
|
2578
|
+
status: paused
|
|
2579
|
+
flow:
|
|
2580
|
+
nodes:
|
|
2581
|
+
- id: trigger
|
|
2582
|
+
type: trigger
|
|
2583
|
+
position: { x: 0, y: 0 }
|
|
2584
|
+
data:
|
|
2585
|
+
label: Every 15 minutes
|
|
2586
|
+
triggerType: cron
|
|
2587
|
+
config:
|
|
2588
|
+
expression: "*/15 * * * *"
|
|
2589
|
+
- id: now
|
|
2590
|
+
type: transform
|
|
2591
|
+
position: { x: 0, y: 120 }
|
|
2592
|
+
data:
|
|
2593
|
+
label: When this run was due, in milliseconds
|
|
2594
|
+
expression: "{{trigger.payload.scheduledAt}} | to_ms"
|
|
2595
|
+
- id: due
|
|
2596
|
+
type: query
|
|
2597
|
+
position: { x: 0, y: 240 }
|
|
2598
|
+
data:
|
|
2599
|
+
label: The oldest armed post whose time has come
|
|
2600
|
+
profileSlug: post
|
|
2601
|
+
filter: '{"post-status": "Scheduled", "publish-outcome": "queued", "publish-at-ms": {"$lte": {{steps.now.output.result}}}}'
|
|
2602
|
+
orderBy: properties.publish-at-ms
|
|
2603
|
+
orderDir: asc
|
|
2604
|
+
limit: 1
|
|
2605
|
+
- id: any-due
|
|
2606
|
+
type: condition
|
|
2607
|
+
position: { x: 0, y: 360 }
|
|
2608
|
+
data:
|
|
2609
|
+
label: A post is due?
|
|
2610
|
+
expression: "steps.due.output.count > 0"
|
|
2611
|
+
- id: claim
|
|
2612
|
+
type: output
|
|
2613
|
+
position: { x: 0, y: 480 }
|
|
2614
|
+
data:
|
|
2615
|
+
label: Claim it, so it is never sent twice
|
|
2616
|
+
outputType: entity_update
|
|
2617
|
+
config:
|
|
2618
|
+
entityId: "{{steps.due.output.entities.0.id}}"
|
|
2619
|
+
properties:
|
|
2620
|
+
publish-outcome: sending
|
|
2621
|
+
# The claim must have LANDED: a claim the pod only proposed leaves the
|
|
2622
|
+
# post queued, and the next run would send it a second time.
|
|
2623
|
+
- id: claimed
|
|
2624
|
+
type: condition
|
|
2625
|
+
position: { x: 0, y: 540 }
|
|
2626
|
+
data:
|
|
2627
|
+
label: The claim is written?
|
|
2628
|
+
expression: "steps.claim.output.status === 'updated'"
|
|
2629
|
+
- id: send
|
|
2630
|
+
type: capability
|
|
2631
|
+
position: { x: 0, y: 600 }
|
|
2632
|
+
data:
|
|
2633
|
+
label: Post it through Unipile
|
|
2634
|
+
verbId: unipile_publish_post
|
|
2635
|
+
inputMapping:
|
|
2636
|
+
accountId: "{{steps.due.output.entities.0.properties.publish-account-id}}"
|
|
2637
|
+
text: "{{steps.due.output.entities.0.properties.publish-text}}"
|
|
2638
|
+
media: "{{steps.due.output.entities.0.properties.publish-media}}"
|
|
2639
|
+
# A LinkedIn company page (post_as) and a repost's original
|
|
2640
|
+
# (quoted_post_id); both empty for an ordinary post.
|
|
2641
|
+
postAs: "{{steps.due.output.entities.0.properties.publish-as-organization}}"
|
|
2642
|
+
quotedPostId: "{{steps.due.output.entities.0.properties.publish-quote-id}}"
|
|
2643
|
+
# Never retried (an outward post), and a failure goes on to the
|
|
2644
|
+
# failure branch instead of stopping the run.
|
|
2645
|
+
errorHandling:
|
|
2646
|
+
continueOnError: true
|
|
2647
|
+
- id: sent
|
|
2648
|
+
type: condition
|
|
2649
|
+
position: { x: 0, y: 720 }
|
|
2650
|
+
data:
|
|
2651
|
+
label: Unipile posted it?
|
|
2652
|
+
expression: "steps.send.output.error === ''"
|
|
2653
|
+
- id: published
|
|
2654
|
+
type: output
|
|
2655
|
+
position: { x: -160, y: 840 }
|
|
2656
|
+
data:
|
|
2657
|
+
label: Published, and when
|
|
2658
|
+
outputType: entity_update
|
|
2659
|
+
config:
|
|
2660
|
+
entityId: "{{steps.due.output.entities.0.id}}"
|
|
2661
|
+
properties:
|
|
2662
|
+
post-status: Published
|
|
2663
|
+
published-at: "{{trigger.payload.scheduledAt}}"
|
|
2664
|
+
publish-outcome: succeeded
|
|
2665
|
+
# Its own node: Unipile may return no link, and an empty post-url is
|
|
2666
|
+
# refused; the post is Published either way.
|
|
2667
|
+
- id: link
|
|
2668
|
+
type: output
|
|
2669
|
+
position: { x: -160, y: 960 }
|
|
2670
|
+
data:
|
|
2671
|
+
label: The live post's link
|
|
2672
|
+
outputType: entity_update
|
|
2673
|
+
config:
|
|
2674
|
+
entityId: "{{steps.due.output.entities.0.id}}"
|
|
2675
|
+
properties:
|
|
2676
|
+
post-url: "{{steps.send.output.shareUrl}}"
|
|
2677
|
+
errorHandling:
|
|
2678
|
+
continueOnError: true
|
|
2679
|
+
# Unipile's id of the post it just sent: what a repost of it quotes.
|
|
2680
|
+
# With no id there is nothing to quote, so its reposts stay waiting
|
|
2681
|
+
# (Content Studio says so on the post).
|
|
2682
|
+
- id: has-id
|
|
2683
|
+
type: condition
|
|
2684
|
+
position: { x: -160, y: 1080 }
|
|
2685
|
+
data:
|
|
2686
|
+
label: Unipile gave the post's id?
|
|
2687
|
+
expression: "steps.send.output.postId !== ''"
|
|
2688
|
+
- id: remember-id
|
|
2689
|
+
type: output
|
|
2690
|
+
position: { x: -160, y: 1200 }
|
|
2691
|
+
data:
|
|
2692
|
+
label: Keep the post's Unipile id
|
|
2693
|
+
outputType: entity_update
|
|
2694
|
+
config:
|
|
2695
|
+
entityId: "{{steps.due.output.entities.0.id}}"
|
|
2696
|
+
properties:
|
|
2697
|
+
publish-post-id: "{{steps.send.output.postId}}"
|
|
2698
|
+
errorHandling:
|
|
2699
|
+
continueOnError: true
|
|
2700
|
+
# Its reposts (written at approval, each waiting for this post), now
|
|
2701
|
+
# queued with the id they quote. Only writes run per item here: the
|
|
2702
|
+
# send itself is never inside a loop.
|
|
2703
|
+
- id: reposts
|
|
2704
|
+
type: query
|
|
2705
|
+
position: { x: -160, y: 1320 }
|
|
2706
|
+
data:
|
|
2707
|
+
label: The reposts waiting for this post
|
|
2708
|
+
profileSlug: post
|
|
2709
|
+
filter: '{"post-copy-of": "{{steps.due.output.entities.0.id}}", "publish-outcome": "waiting"}'
|
|
2710
|
+
limit: 20
|
|
2711
|
+
- id: each-repost
|
|
2712
|
+
type: loop
|
|
2713
|
+
position: { x: -160, y: 1440 }
|
|
2714
|
+
data:
|
|
2715
|
+
label: For each waiting repost
|
|
2716
|
+
iteratorExpression: steps.reposts.output.entities
|
|
2717
|
+
itemVariable: repost
|
|
2718
|
+
- id: arm-repost
|
|
2719
|
+
type: output
|
|
2720
|
+
position: { x: -160, y: 1560 }
|
|
2721
|
+
data:
|
|
2722
|
+
label: Queue it, with the post it reposts
|
|
2723
|
+
outputType: entity_update
|
|
2724
|
+
config:
|
|
2725
|
+
entityId: "{{loop.item.id}}"
|
|
2726
|
+
properties:
|
|
2727
|
+
publish-quote-id: "{{steps.send.output.postId}}"
|
|
2728
|
+
publish-outcome: queued
|
|
2729
|
+
- id: failed
|
|
2730
|
+
type: output
|
|
2731
|
+
position: { x: 160, y: 840 }
|
|
2732
|
+
data:
|
|
2733
|
+
label: It didn't go out, and why
|
|
2734
|
+
outputType: entity_update
|
|
2735
|
+
config:
|
|
2736
|
+
entityId: "{{steps.due.output.entities.0.id}}"
|
|
2737
|
+
properties:
|
|
2738
|
+
publish-outcome: failed
|
|
2739
|
+
publish-error: "{{steps.send.output.error}}"
|
|
2740
|
+
edges:
|
|
2741
|
+
- id: e-trigger-now
|
|
2742
|
+
source: trigger
|
|
2743
|
+
target: now
|
|
2744
|
+
- id: e-now-due
|
|
2745
|
+
source: now
|
|
2746
|
+
target: due
|
|
2747
|
+
- id: e-due-any
|
|
2748
|
+
source: due
|
|
2749
|
+
target: any-due
|
|
2750
|
+
- id: e-any-claim
|
|
2751
|
+
source: any-due
|
|
2752
|
+
target: claim
|
|
2753
|
+
sourceHandle: "yes"
|
|
2754
|
+
- id: e-claim-claimed
|
|
2755
|
+
source: claim
|
|
2756
|
+
target: claimed
|
|
2757
|
+
- id: e-claimed-send
|
|
2758
|
+
source: claimed
|
|
2759
|
+
target: send
|
|
2760
|
+
sourceHandle: "yes"
|
|
2761
|
+
- id: e-send-sent
|
|
2762
|
+
source: send
|
|
2763
|
+
target: sent
|
|
2764
|
+
- id: e-sent-published
|
|
2765
|
+
source: sent
|
|
2766
|
+
target: published
|
|
2767
|
+
sourceHandle: "yes"
|
|
2768
|
+
- id: e-published-link
|
|
2769
|
+
source: published
|
|
2770
|
+
target: link
|
|
2771
|
+
- id: e-link-has-id
|
|
2772
|
+
source: link
|
|
2773
|
+
target: has-id
|
|
2774
|
+
- id: e-has-id-remember
|
|
2775
|
+
source: has-id
|
|
2776
|
+
target: remember-id
|
|
2777
|
+
sourceHandle: "yes"
|
|
2778
|
+
- id: e-remember-reposts
|
|
2779
|
+
source: remember-id
|
|
2780
|
+
target: reposts
|
|
2781
|
+
- id: e-reposts-each
|
|
2782
|
+
source: reposts
|
|
2783
|
+
target: each-repost
|
|
2784
|
+
- id: e-each-arm
|
|
2785
|
+
source: each-repost
|
|
2786
|
+
target: arm-repost
|
|
2787
|
+
- id: e-sent-failed
|
|
2788
|
+
source: sent
|
|
2789
|
+
target: failed
|
|
2790
|
+
sourceHandle: "no"
|
|
2791
|
+
|
|
1059
2792
|
onboarding:
|
|
1060
2793
|
goal: >
|
|
1061
2794
|
Stand up the editorial engine — the pillars you're known for, the journey a
|
|
@@ -1074,6 +2807,12 @@ onboarding:
|
|
|
1074
2807
|
every piece lands on-strategy. Push for a cadence they can hold for a year,
|
|
1075
2808
|
not a heroic month. Be concrete: real platforms, real formats, a real first
|
|
1076
2809
|
post — a fuzzy "I'll post more" helps no one.
|
|
2810
|
+
READY BEFORE PRODUCING: a post is produced only from approved brand
|
|
2811
|
+
material — at least three approved references, an approved base content
|
|
2812
|
+
guideline, and an approved brand-template for each format the post uses.
|
|
2813
|
+
If any is missing, do not improvise a look: say which, and route to the
|
|
2814
|
+
content-taste skill (or Brand Library onboarding when there is no brand
|
|
2815
|
+
identity or voice yet) before the first post is produced.
|
|
1077
2816
|
COMMERCE IS OPTIONAL AND YOU MUST TREAT IT THAT WAY. Ask ONCE whether they
|
|
1078
2817
|
sell anything. If the answer is no, or vague, SKIP offers entirely, never
|
|
1079
2818
|
mention them again, and build the journey as pure audience-building — a
|
|
@@ -1138,6 +2877,7 @@ onboarding:
|
|
|
1138
2877
|
thin — so the coverage read is a habit, not a feature they never find.
|
|
1139
2878
|
collect:
|
|
1140
2879
|
- profileSlug: pillar
|
|
2880
|
+
min: 1
|
|
1141
2881
|
what: The recurring themes you're known for — the 2–4 lenses your audience comes to you for, each pointed at the audience it speaks to and the goal it serves.
|
|
1142
2882
|
cardinality: few
|
|
1143
2883
|
keyFields: [pillar-status, pillar-audience-ref, pillar-goal]
|
|
@@ -1150,13 +2890,15 @@ onboarding:
|
|
|
1150
2890
|
cardinality: one
|
|
1151
2891
|
keyFields: [series-status, series-goal, series-pillar]
|
|
1152
2892
|
- profileSlug: platform
|
|
2893
|
+
min: 1
|
|
1153
2894
|
what: The channels you actually publish on — the social accounts, newsletter, or podcast — with their status and the cadence you can hold.
|
|
1154
2895
|
cardinality: several
|
|
1155
2896
|
keyFields: [platform-type, platform-status, platform-posting-freq]
|
|
1156
2897
|
- profileSlug: post
|
|
1157
|
-
|
|
2898
|
+
min: 1
|
|
2899
|
+
what: A first post or two already in motion, each placed on a pillar and a platform at its true status (Idea through Published), with its goal (sell, hot-take, tutorial, product-demo…) so it gets the right narrative frame, and on a stage if the journey exists.
|
|
1158
2900
|
cardinality: few
|
|
1159
|
-
keyFields: [post-status, post-pillar, post-platform]
|
|
2901
|
+
keyFields: [post-status, post-pillar, post-platform, post-goal]
|
|
1160
2902
|
- profileSlug: offer
|
|
1161
2903
|
what: >-
|
|
1162
2904
|
ONLY IF THEY SELL SOMETHING. Ask once; if they don't sell, or hedge,
|
|
@@ -1176,3 +2918,52 @@ onboarding:
|
|
|
1176
2918
|
# one. Mirrored verbatim by standards/content-os.standard.yaml — the journey
|
|
1177
2919
|
# layer lives in its `recommends`, not its `requires`.
|
|
1178
2920
|
doneWhen: at least one pillar + one platform with a cadence + a first post placed on a pillar.
|
|
2921
|
+
|
|
2922
|
+
skills:
|
|
2923
|
+
# DECLARED, not asked for in prose: every AI working in Content OS is told this
|
|
2924
|
+
# skill exists, is prepended with it each turn (`always`), and finds it ranked
|
|
2925
|
+
# first — the producing space, where every content ask routes through it.
|
|
2926
|
+
#
|
|
2927
|
+
# A REF, not a copy: the skill ships as its own package
|
|
2928
|
+
# (`skills/creative-director.skill.json`) and is installed by the `require`
|
|
2929
|
+
# dependency below. This is the LINK; that is the INSTALL.
|
|
2930
|
+
- slug: creative-director
|
|
2931
|
+
when: Every content ask in this space — it picks the approved brand-template, references and assets
|
|
2932
|
+
mode: always
|
|
2933
|
+
# On-demand, not always: needed only when the readiness check finds no
|
|
2934
|
+
# approved references, base guideline or template for the format.
|
|
2935
|
+
- slug: content-taste
|
|
2936
|
+
when: Studying inspiration, writing content guidelines, building the asset kit
|
|
2937
|
+
mode: on-demand
|
|
2938
|
+
- slug: carousel-format
|
|
2939
|
+
when: Scripting or drafting a carousel, LinkedIn PDF or image post (the layered composition)
|
|
2940
|
+
mode: on-demand
|
|
2941
|
+
- slug: edit-loop
|
|
2942
|
+
when: Building any visual or video post pass by pass — the slots, answers and rules every engine shares
|
|
2943
|
+
mode: on-demand
|
|
2944
|
+
- slug: open-design-edit
|
|
2945
|
+
when: Building a deck, PDF document or presentation in Open Design
|
|
2946
|
+
mode: on-demand
|
|
2947
|
+
# Video: the toolbox first (which tool, where it renders, which skill next),
|
|
2948
|
+
# then the job skill it names.
|
|
2949
|
+
- slug: video-toolbox
|
|
2950
|
+
when: Any video or animation ask, before choosing a tool — making, editing or animating video
|
|
2951
|
+
mode: on-demand
|
|
2952
|
+
- slug: video-direction
|
|
2953
|
+
when: Planning a launch, promo or explainer film before building it — brief, story, treatment, storyboard
|
|
2954
|
+
mode: on-demand
|
|
2955
|
+
- slug: video-style-vocabulary
|
|
2956
|
+
when: Choosing the visual style of a video or writing its style sheet
|
|
2957
|
+
mode: on-demand
|
|
2958
|
+
- slug: motion-quality-gates
|
|
2959
|
+
when: Checking a HyperFrames draft before showing it or rendering the final
|
|
2960
|
+
mode: on-demand
|
|
2961
|
+
- slug: math-explainer-manimgx
|
|
2962
|
+
when: Animating math, equations, graphs, algorithms or 3D concepts for a video
|
|
2963
|
+
mode: on-demand
|
|
2964
|
+
- slug: facecam-edit
|
|
2965
|
+
when: Editing face-camera, interview or screen-recording footage — cuts, captions, graphics
|
|
2966
|
+
mode: on-demand
|
|
2967
|
+
- slug: collage-broll
|
|
2968
|
+
when: Generating b-roll clips or illustrative stills for a video through fal.ai
|
|
2969
|
+
mode: on-demand
|