Documentation source
Growth Studio (Social Suite)
Founder-led content engine — ingest raw cognition, strategize scored ideas, draft channel variants, gate through safety and brand review, publish, measure, and capture attributed leads.
## Overview
Growth Studio is the Sprinter Platform's founder content engine. It lives inside the **social-suite** custom workspace and turns raw founder input — commits, transcripts, call notes, agent session summaries — into on-brand, proof-tied content that flows through a deterministic safety and approval gate before reaching any publishing channel.
The loop has seven stages: **Ingest → Strategize → Draft → Review → Approve → Publish → Measure → Leads**. Five specialized agents automate the middle stages; humans hold two hard approval gates (content approval and publishing schedule). Publishing autonomy is a tenant-level setting that can be widened gradually — the safety gate is never bypassed.
Module location: `features/custom/workspaces/social-suite/`
Key files:
| File | Purpose |
|---|---|
| `autonomy.ts` | `PublishingAutonomy` types and `canAutoApprove()` policy function |
| `workflow.ts` | `classifyRiskTier()`, `approvePost()`, content builders |
| `entity-types.ts` | Thirteen `sm_*` Zod schemas |
| `agents.ts` | Five agent declarations (GTM Strategist, Content Producer, Brand Reviewer, Analytics Operator, Proof Scout) |
| `actions/content-pipeline.ts` | Eight pipeline action definitions |
| `actions/leads.ts` | `socialSuite.captureLead` action definition |
| `actions/connect-channel.ts` | `connectSocialChannel` server action — stands up a publishing channel from the UI |
| `actions/connect-channel-shared.ts` | Shared constants and types for the connect-channel action (client-safe) |
| `components/connect-channel-dialog.tsx` | Founder-facing "Connect channel" dialog (platform picker → app credentials → OAuth start) |
| `content-library.ts` | `SOCIAL_SUITE_CONTENT_LIBRARY` ViewManifest — filterable data-table Library over `sm_post` |
| `integrations/social-publishing.ts` | LinkedIn, X (thread chaining), Email publish paths; Taplio/Tweet Hunter config presets |
| `integrations/metric-fetchers.ts` | LinkedIn and X live metric fetchers |
---
## Key Concepts
### The sm_* Entity Types
All data lives as entities in the platform graph. There are thirteen types; all install with `install_scope: "tenant"` and `visibility: "team"`.
| Type | Role |
|---|---|
| `sm_brand_profile` | Voice, banned/approved terms, CTA, publishing autonomy default |
| `sm_audience` | Persona, pains, outcomes, buying stage |
| `sm_channel` | Platform handle, connection state, per-channel autonomy override |
| `sm_campaign` | Goal, dates, KPI targets |
| `sm_idea` | Raw capture + research + quality score — the Vault |
| `sm_content_piece` | Anchor asset (brief → body); 1 anchor → N post variants |
| `sm_post` | Platform-specific variant with full safety, risk, and approval fields |
| `sm_publish_job` | Scheduled publish job tied to a post and channel connection |
| `sm_insight` | Analytics observation (promote/remix/retire/experiment) |
| `sm_metric_snapshot` | Point-in-time channel metric reading |
| `sm_campaign_report` | Aggregated campaign-level metrics |
| `sm_inbox_item` | Triaged inbound DM, request, or partnership inquiry |
| `sm_lead` | Captured lead attributed to the post or campaign that produced it |
### Publishing Autonomy
`PublishingAutonomy` is a three-level setting on `sm_brand_profile` (brand default) and `sm_channel` (per-channel override):
| Level | Behaviour |
|---|---|
| `review_all` | Nothing auto-approves. Every post waits for a human. **Default.** |
| `auto_green` | Green-risk posts auto-approve after the safety gate passes; Yellow/Red wait for a human. |
| `full_auto` | Green and Yellow auto-approve after the gate; only Red waits for a human. |
`ChannelAutonomy` adds `inherit` (use the brand default). The effective autonomy resolves via `resolveEffectiveAutonomy(brandAutonomy, channelAutonomy)`. A channel can therefore stay on `review_all` (e.g., LinkedIn) while X runs on `auto_green`.
The safety gate is an unconditional floor: `canAutoApprove()` returns `false` if the safety gate did not pass, regardless of autonomy level.
### Risk Tiers
`classifyRiskTier()` in `workflow.ts` classifies every post's hook + copy before approval:
| Tier | Content signals | Auto-approve eligible? |
|---|---|---|
| `green` | No commercial CTAs, no pricing, no results claims, no legal terms | Yes, under `auto_green` or `full_auto` |
| `yellow` | Commercial CTA, client/results reference | Yes, under `full_auto` only |
| `red` | Pricing/dollar figures, named result metrics, case-study framing, guarantee language, legal/compliance, failed safety, or banned terms | Never — always requires a human |
Risk reclassifies at review time (`reviewPostForApproval`) — copy may have changed since the post was first drafted.
### Safety Gate (Hard Floor)
`sm_post` carries two binary safety fields:
- `claimSafetyStatus` — `"pass"` / `"fail"`. Fails on banned terms or unsupported factual claims.
- `phiSafetyStatus` — `"pass"` / `"fail"`. Fails on any private or protected health information.
`SM_POST_READINESS_SAFETY_DIMS` installs via `CriteriaDimensionManifest` (which now carries `type: "select"` + `options` + `optionStyles` with `hardBlock: true` on `"fail"`). The live criteria executor hard-stops an unsafe post without a human having to inspect each dimension. Auto-approve paths also check `safetyPassed` before calling `canAutoApprove()` — the gate enforces at both the action layer and the bundle layer.
### Lead Attribution
`sm_lead` records a captured lead deduped by email. `socialSuite.captureLead` upserts the record and draws an `attributed-to` relation from the lead to the `sm_post` or `sm_campaign` that produced it. Attribution lives as a platform relation — not a parallel attribution table. The lead carries the canonical intake stamps (`intake_source`, `intake_channel`, `consent_phi`, `marketing_opt_in`) the platform enforces across all lead-target types.
---
## How It Works
### The Loop
```
1. INGEST
socialSuite.ingestFounderInput
(commits/PRs/transcripts/sessions/notes → sm_idea)
OR
socialSuite.captureIdea (single idea)
2. STRATEGIZE
GTM Strategist (agent) reads learnedLessons + external signal
→ scores, selects, proposes campaign angles
3. DRAFT
socialSuite.createContentPiece → sm_content_piece (anchor)
socialSuite.generatePostVariants → sm_post per connected channel
Content Producer (agent) writes platform-specific copy
4. REVIEW
socialSuite.submitPostForReview → reclassifies risk + safety
Brand Reviewer (agent) scores claim safety, PHI, brand fit
Safety gate hard-blocks any "fail" safety verdict
5. APPROVE
Human reviews post in Growth Studio
→ socialSuite.approvePost (approvedVia: "human")
OR
socialSuite.autoApproveEligible cron scans pending posts
→ calls canAutoApprove(brandAutonomy, channelAutonomy, riskTier, safetyPassed)
→ approves eligible posts (approvedVia: "auto_policy")
6. PUBLISH
sm_publish_job created at approval time (pre-staged integration operation)
sm-auto-approve-eligible cron due-worker fires connection-specific path:
- LinkedIn: REST publishing path
- X: OAuth2 PKCE bearer, thread chaining enabled
- Email: SMTP/send path
- Taplio / Tweet Hunter: bearer-token presets
7. MEASURE
Metric fetchers (LinkedIn, X) pull engagement → sm_metric_snapshot
Analytics Operator reads snapshots → sm_insight (promote/remix/retire/experiment)
appendInsightToLearnedLessons promotes insights to brand.learnedLessons
GTM Strategist reads learnedLessons on next cycle → loop closes
8. LEADS
socialSuite.captureLead(email, name, attributedPostId)
→ upsert sm_lead + draw attributed-to relation
```
### Ingest: Front Mile
`socialSuite.ingestFounderInput` accepts batches of up to 50 items. Each item carries:
- `text` — the raw content (required, min 1 char)
- `sourceKind` — `commit | pr | transcript | session | note | manual`
- `sourceRef` — stable ref (commit SHA, session ID) for idempotency
Every item becomes an `sm_idea` row keyed on `founder-{sourceKind}-{ref}`. Re-ingesting the same ref re-reads the existing idea rather than duplicating it. The action is callable by a cron action, an MCP/API caller (so local tooling can POST commits the deployed app cannot reach), or the goal loop.
### Approve: Dual-Path Audit Trail
`approvePost` records the `approvedVia` field:
- `"human"` — stamps `humanApprovedAt` and `humanApprovedBy`.
- `"auto_policy"` — does NOT stamp human fields. An auto-approval never masquerades as human review in audit logs.
`socialSuite.setBrandAutonomy` writes the brand profile's `publishingAutonomy` field and optionally a specific channel's override — autonomy is a runtime configuration, not a deployment.
### Publish: Channel Branches
`social-publishing.ts` dispatches based on `presetId` or `platform`:
- **LinkedIn** — REST publishing path (existing).
- **X** — OAuth2 PKCE bearer; thread chaining enabled (long-form content splits into threads).
- **Email** — SMTP/send path.
- **Taplio / Tweet Hunter** — Bearer-token presets over the generic path; useful for teams using third-party scheduling tools.
---
## API Reference
### Autonomy
| Function | Location | Purpose |
|---|---|---|
| `canAutoApprove(args)` | `autonomy.ts` | Returns `true` only when policy, risk tier, and safety all permit auto-approval |
| `resolveEffectiveAutonomy(brand, channel)` | `autonomy.ts` | Resolves a channel's effective autonomy (`inherit` defers to brand) |
| `autoApproveDeclineReason(args)` | `autonomy.ts` | Human-readable reason auto-approval was declined (for audit logs) |
### Risk Classification
| Function | Location | Purpose |
|---|---|---|
| `classifyRiskTier(input)` | `workflow.ts` | Returns `{ riskTier, riskReasons }` for a post's hook + copy |
| `detectBannedTerms(copy, terms)` | `workflow.ts` | Returns banned terms found in copy (case-insensitive) |
### Channel Setup
| Function | Location | Purpose |
|---|---|---|
| `connectSocialChannel(input)` | `actions/connect-channel.ts` | Creates an `agent_connections` row + `sm_channel` entity and returns the OAuth-start path for browser redirect |
### Content Builders
| Function | Location | Purpose |
|---|---|---|
| `buildIdeaContent(input)` | `workflow.ts` | Constructs a valid `sm_idea` content object |
| `buildContentPieceFromIdea(input)` | `workflow.ts` | Promotes an idea into an anchor `sm_content_piece` |
| `buildPostVariant(input)` | `workflow.ts` | Creates a platform-specific `sm_post` from a content piece + channel |
| `reviewPostForApproval(input)` | `workflow.ts` | Reclassifies risk/safety and moves post to `pending_approval` |
| `approvePost(input)` | `workflow.ts` | Stamps approval; records `approvedVia` (`human` or `auto_policy`) |
| `buildPublishJob(input)` | `workflow.ts` | Constructs an `sm_publish_job` for a post + channel |
| `appendInsightToLearnedLessons(input)` | `workflow.ts` | Promotes a classified insight into `brand.learnedLessons` |
| `deriveIdeaTitle(text, fallback)` | `actions/content-pipeline.ts` | Extracts a clean title from the first line of raw text |
---
## For Agents
### Actions Available to Agents
| Action key | Description |
|---|---|
| `socialSuite.ingestFounderInput` | Batch-ingest raw founder signals (commits, notes, transcripts) as `sm_idea` records |
| `socialSuite.captureIdea` | Capture a single scored idea |
| `socialSuite.createContentPiece` | Promote an idea into an anchor `sm_content_piece` |
| `socialSuite.generatePostVariants` | Fan out to one `sm_post` per configured/connected channel |
| `socialSuite.submitPostForReview` | Move a draft through the safety + risk gate |
| `socialSuite.approvePost` | Approve a pending post (human path or auto_policy path) |
| `socialSuite.autoApproveEligible` | Scan all `pending_approval` posts and auto-approve eligible ones under the current policy |
| `socialSuite.setBrandAutonomy` | Write the brand profile's `publishingAutonomy` (or a channel's override) |
| `socialSuite.captureLead` | Upsert an `sm_lead` and attribute it to the post/campaign that produced it |
| `socialSuite.promoteInsight` | Record an analytics insight and promote it to `brand.learnedLessons` |
### Views Available to Agents
| View slug | Entity type | Purpose |
|---|---|---|
| `social-suite-content-library` | `sm_post` | Filterable/sortable data-table Library over all posts — status, risk tier, platform, and date |
### Cron Actions
| Cron slug | Trigger | Purpose |
|---|---|---|
| `sm-auto-approve-eligible` | Scheduled | Runs `socialSuite.autoApproveEligible` to auto-approve eligible pending posts |
| `sm-promote-new-idea` | `entity_created` on `sm_idea` | Fires the GTM Strategist to score and research a new idea immediately |
### Five Specialized Agents
| Slug | Role | toolGroups |
|---|---|---|
| `sm-gtm-strategist` | Proposes campaigns and content angles; reads `learnedLessons` and external signal | `entity`, `context`, `web` |
| `sm-content-producer` | Drafts channel-specific copy from anchor piece + verbatim source quotes | `entity`, `context` |
| `sm-brand-reviewer` | Scores claim safety, PHI safety, brand voice; emits structured `claim_safety` / `phi_safety` gate values | `entity` |
| `sm-analytics-operator` | Reads metric snapshots; classifies patterns as promote/remix/retire/experiment | `entity` |
| `sm-proof-scout` | Finds approved proof records that back a claim; flags unsupported claims | `entity` |
The Brand Reviewer's structured `claim_safety` / `phi_safety` output values are load-bearing. The safety-gate evaluator reads them to hard-block posts. A missing or wrong value defeats the gate — the agent prompt makes this explicit.
### Goal Loop Template
The `founder-content-engine` template (`features/entities/lib/goal-loop-templates.ts`) encodes the weekly cadence:
- Category: `growth`; `completionMode: "persistent"` (recurring operating loop)
- Work source kind: `content` — reads recent learnings, proposes scored ideas, drafts variants
- Acceptance: scored ideas + proof-tied drafts in the human approval queue
- Scorecard: approval-ready drafts (45%), claims tied to proof (35%), cadence on track (20%)
---
## Connect-channel Wizard
`connectSocialChannel` (server action in `actions/connect-channel.ts`) lets a founder self-serve connect a LinkedIn or X publishing channel from the Brand page with no backend seeding. It chains three platform primitives end-to-end:
1. `requireAdmin()` — permission gate matching the existing OAuth-start route; a half-created connection can never be stranded.
2. `getConnectionPreset(presetId)` — resolves a `linkedin-publisher` or `x-publisher` preset from `features/agents/connection-presets.ts`. The allow-list is enforced both client-side (filter on `capabilities.includes("publishing")`) and server-side (assert after preset resolution). No unknown or non-publishing preset can reach `createConnection`.
3. `createConnection(...)` — writes the `agent_connections` row with auto-encrypted credentials. The `sm_channel` entity is created immediately after with `connectionStatus: "needs_auth"`.
4. `buildConnectionOAuthStartPath(conn.id)` — returns the OAuth-start URL. The action returns this path; the client redirects to it. The existing platform OAuth callback (`/api/oauth/connection-callback`) writes the tokens and sets `status: "active"` — no new callback, no new OAuth logic.
The client component (`components/connect-channel-dialog.tsx`) surfaces three steps: platform picker, app credentials entry (with the required redirect URI pre-computed and copyable — load-bearing UX to avoid OAuth misconfiguration), and display-name confirmation. Identity (tenantId, userId) is read from server context only; the client secret is never logged or returned to the browser.
---
## Content Library
`SOCIAL_SUITE_CONTENT_LIBRARY` (`content-library.ts`) is a `ViewManifest` produced by `buildEntityLibraryView` over `sm_post`. It gives founders a single browsable, filterable view of the entire post catalog — by approval status, risk tier, platform, and date — alongside the bespoke kanban pipeline board.
Column-key convention: content fields use bare schema property names (`hook`, `platform`, `approvalStatus`, `riskTier`, `scheduledAt`); entity base columns use the built-in column IDs (`_title`, `_created`). `sortBy: "updated_at"` is a server sort field, not a visible column. Any column key not present in the entity's schema properties or `BUILT_IN_COLUMNS` is silently dropped at the block layer.
The manifest receives the view via `manifest.views = [SOCIAL_SUITE_CONTENT_LIBRARY]`. It surfaces in the workspace navigation and via a "Browse all in Content Library" link on the pipeline page (`pages/content.tsx`). FormSpec was not applied to any existing Growth Studio forms in this change; the Library is the appropriate additive blocks-v2 surface because it is a genuinely new surface rather than a replacement.
---
## Architecture Seams
These facts were verified across six research passes before the seam-lift build. Future agents should treat them as settled so the same ground is not re-litigated.
| Seam | Verdict |
|---|---|
| **Page rendering model** | The nine Growth Studio pages are bespoke finance-grade React components — intentional and idiomatic for a flagship product workspace (per `custom-workspaces.md` and the DOC360 clinic precedent). Do NOT rewrite them as ViewSpec; ViewSpec is for generic config-driven surfaces. |
| **OAuth channel table** | `agent_connections` is the current table for storing OAuth publishing channels. A table called `integration_connections` does not exist. The social suite reads `agent_connections` correctly; `connectSocialChannel` writes to it. |
| **ConnectorSpec vs. IntegrationDefinition** | `ConnectorSpec` (`features/integrations/declarative/schema.ts`) is ingestion-only (REST feed / web → entity / metrics). Outbound publishing uses the bespoke `IntegrationDefinition` (`kind: "publish"`) registered in the unified `listIntegrations`/`manageIntegration` registry (`features/integrations/framework/definition-registry.ts`). These are not parallel systems; they share the same registry under different `kind` values. |
| **End-to-end agent loop** | The extract→draft→safety→publish→measure loop is wired: `ingestFounderInput` → agent draft → `submitPostForReview` (risk + safety) → `approvePost` / `autoApproveEligible` cron → `sm_publish_job` → `social-publishing.ts` publish paths → metric fetchers → `sm_metric_snapshot` → `sm_insight` → `appendInsightToLearnedLessons` → GTM Strategist reads on next cycle. |
| **Content Library vs. pipeline board** | `SOCIAL_SUITE_CONTENT_LIBRARY` (blocks-v2 ViewManifest) is the "browse and filter all posts" complement to the bespoke kanban workflow page (`pages/content.tsx`). The pipeline board is not replaced. |
| **FormSpec deferral** | FormSpec was deliberately not applied to existing working Growth Studio forms to avoid churn and regression risk. The Content Library is where blocks-v2 was applied because it is an additive surface with no prior implementation. |
---
## Design Decisions
**Autonomy is a setting, not code.** The three-level `PublishingAutonomy` enum lives on the brand entity and changes at runtime without a deploy. `canAutoApprove()` is a pure function over (policy × risk tier × safety) — easy to test, easy to audit in logs, and easy to explain to a client. Hardcoding autonomy levels as conditional paths would conflate policy change with code change.
**Safety is a hard floor under every auto path.** Widening autonomy (from `review_all` to `full_auto`) changes who may approve — human or engine — but never what may publish unsafe. The gate enforces at two independent layers: `canAutoApprove()` rejects any post where `safetyPassed === false`, and `CriteriaDimensionManifest`'s `hardBlock` on the `"fail"` option stops the live criteria executor independently. Defense in depth: either layer alone is sufficient to block an unsafe post.
**Risk reclassifies at review time.** Copy can change between `draft` and `pending_approval`. `reviewPostForApproval` re-runs `classifyRiskTier` on the current copy so the approval gate always acts on the actual content, not a stale classification from when the post was first generated.
**Two lead planes are not a parallel system.** The platform has a funnel `lead` entity type (DOC'S, Sprinter AI retainer intake) governed by ADR-0054. `sm_lead` is a Growth Studio–scoped content-attributed lead: different domain shape, different dedupe key (email per tenant), and attribution via a platform relation rather than a funnel stage. The two types coexist without overlap because they serve different data models. A future merge would require explicit ADR review.
**Attribution as a relation, not a column.** The `attributed-to` predicate on `entities_relations` ties an `sm_lead` to the `sm_post` or `sm_campaign` that produced it. This reuses the platform's existing relation primitive rather than adding an attribution column to `sm_lead`. The relation is idempotent via `createEntityRelationKeyed`.
**`approvedVia` audit field prevents impersonation.** The `"human"` path stamps `humanApprovedAt` and `humanApprovedBy`. The `"auto_policy"` path explicitly does not stamp these fields. An auto-approval is never mistaken for a human review in audit logs or reports.
**Ingest is the only external entry point.** The GTM Strategist carries `toolGroups: ["entity", "context", "web"]` — it can pull external signal to pair with brand content. Every other agent is `entity`-only: proof, voice, and approved terms come from the graph, not web calls. This limits hallucination surface without blocking the weekly research-sprint use case.
---
## Related Modules
- **Entity System** (`features/entities/`) — all `sm_*` types are entities; pipeline actions use `upsertEntityKeyed` / `createEntityRelationKeyed`; see [Entity System](/docs/features/entity-system)
- **Bundles** (`features/bundles/`) — `CriteriaDimensionManifest` now carries `select` + `optionStyles` for the safety hard-block; the social suite manifest installs all 13 types, 5 agents, and criteria sets; see [Bundles](/docs/features/bundles)
- **Actions** (`features/actions/`) — all pipeline steps are registered `ActionDefinition`s with idempotency keys and required permissions; see [Actions](/docs/features/actions)
- **Agent System** (`features/agents/`) — the five agents install via the bundle manifest; the GTM Strategist uses the `web` toolGroup; see [Agent System](/docs/features/agent-system)
- **Goal Loops** (`features/entities/lib/goal-loop-templates.ts`) — the `founder-content-engine` template encodes the weekly cadence, acceptance criteria, and proof requirements; see [Loops](/docs/features/loops)
- **Custom Workspaces** (`features/custom/workspaces/`) — Growth Studio follows the `buildProductWorkspaceManifest` substrate; see [Custom Workspaces](/docs/features/custom-workspaces)
- **Lead Generation** — `sm_lead` co-exists with funnel leads (ADR-0054); see [Lead Generation](/docs/features/lead-generation)
- **Social Features** (`features/tenant/`) — user profiles, connections, and notifications are a separate platform module; see [Social Features](/docs/features/social-features)