Producers & the Producer SPI¶
Producers are the agents that turn a blueprint into a posting proposal — and, if they declare a render surface, into the finished media. Today there is one: SimilarContent, a kind:clone agent that recreates a winning clip 1:1 and renders approved recipes into actual reels. Three more are planned from the same scaffold. This page documents the SPI (service provider interface) every producer must implement, walks through SimilarContent's full call sequence, and shows how to build a new producer with the copy-scaffold recipe.
Prerequisite reading
This page assumes familiarity with the hub's REST contract (API Reference) and the 7-stage pipeline (Architecture). The blueprint schema that producers consume is documented in Agents: AnalysisEngine.
What a producer is¶
A producer reads the shared corpus, blueprints, audio trends, and cross-agent insights, and writes a studio proposal — a markdown file describing exactly what to generate (or re-generate) for a platform. It never posts to the platform itself and never touches another agent's files. Every read and write goes through the hub's /api/* surface.
consumes: corpus, analysis (blueprints), audio, insights
produces: studio_markdown → POST /api/studio/{platform}
gate: human approve/reject in the Dashboard
render: (optional) approved item → media → POST /api/renders/{platform}
The proposal half is free — it reads blueprints and writes markdown. The render half costs money per frame, so it is a separate, human-triggered invocation, never part of a pipeline run.
| Producer | kind |
human_gate |
needs_reference |
Status |
|---|---|---|---|---|
| SimilarContent | clone |
false |
false |
Live |
| proposal-content | proposal |
true |
false |
Planned |
| creative-idea | idea |
— | false |
Planned |
| template-content | template |
— | true |
Planned |
Human gate is universal, human_gate in the manifest is not
Every studio proposal is gated by a human approve/reject step in the Dashboard regardless of the manifest flag — that gate lives in the studio/{p}/gate.jsonl workflow, not in the producer. The manifest's human_gate field is closer to a declared intent/UX hint (e.g. proposal-content flags it explicitly because it's producing candidate scripts meant for human selection); it doesn't change the mechanics described below.
The Producer SPI¶
Every generation agent — SimilarContent today, proposal/idea/template producers tomorrow — implements the same contract with the hub: four mandatory parts, plus an optional fifth for producers that render.
1. Register¶
On startup, a producer self-registers its manifest. Registration is an idempotent upsert keyed by name, so restarting an agent, or redeploying it, never creates duplicate roster entries.
POST /api/producers/register
{
"name": "similar-content",
"kind": "clone",
"consumes": ["corpus", "analysis", "audio", "insights"],
"human_gate": false,
"needs_reference": false,
"produces": "studio_markdown",
"output_status": "proposed",
"renderable": true,
"dir": "SimilarContent",
"render_cmd": ["uv", "run", "cli.py", "render"],
"config_schema": { "...": "..." },
"secrets": [{ "name": "...", "env_var": "...", "required": true }],
"workflow_stages": ["Queued", "Generating", "Self-eval", "Proposed", "Approved",
"Rendering", "Rendered", "Rejected"]
}
Declaring a render surface¶
The three fields in the middle are what make a producer renderable — able to turn an approved proposal into an actual media file. They are optional; a producer that only writes markdown omits all three.
| Field | Meaning |
|---|---|
renderable |
true opts this producer into POST /api/studio/{p}/{file}/render. |
dir |
The producer's own directory name. Must be a direct sibling of the hub repo — no slashes, no leading dot. This is the working directory the command runs in. |
render_cmd |
The argv the hub executes there. Must start with an allowlisted launcher (uv, python, python3, node, npm), and every argument must match ^[A-Za-z0-9._/=:-]{1,120}$ with no absolute path and no ... |
The hub hardcodes no producer name or path: it resolves the producer from the studio item's agent field, then reads these fields from that agent's registered manifest. Adding a second renderable producer needs no hub change.
render_cmd is validated, not trusted
POST /api/producers/register is unauthenticated and ProducerManifest allows extra fields, so render_cmd is caller-supplied and ends up as argv for subprocess.run. The launcher allowlist and argument shape-check are the actual security boundary, not defence in depth — dir only pins the working directory, never the command. shell=True is never used. See SECURITY.md at the repo root.
The registry is what makes new producers appear on the Dashboard automatically — GET /api/producers drives the producer lanes shown on the board, so shipping a new producer is purely additive: register it, and the UI grows a lane with no frontend change.
GET /api/producers # full roster
GET /api/producers/{name} # one manifest
workflow_stages is the ordered lane list the agent board renders for this producer (see Agents: AutoSearch for the parallel discovery-kind board reducer). Approved and Rejected are always the last two stages and are never emitted by the agent itself — the hub derives them by left-joining the human gate decision log onto the board.
2. Consume corpus, analysis, audio, insights¶
Before generating anything, a producer reads four shared substrates, all via GET:
| Substrate | Endpoint | Purpose |
|---|---|---|
| Corpus | GET /api/corpus/{p}/top?n= |
Pick candidate targets by virality |
| Corpus | GET /api/corpus/{p}/search?q=&k= |
Recall search when a topic is given |
| Content | GET /api/content/{p} |
Full normalized record for a target |
| Analysis | GET /api/analysis/{p} / GET /api/analysis/{p}/{content_id} |
The schema_version 2 blueprint — source of truth |
| Audio | GET /api/audio/{p}/trending?reusable_only=&mood= |
Trending sounds when the blueprint's own sound isn't reusable |
| Audio | GET /api/audio/{p}/sound/{audio_id} |
Detail for one sound |
| Insights | GET /api/insights |
Prior transferable learnings from any agent |
The blueprint (from stage 6, "Blueprint," in Architecture) is the shared substrate every producer reads: video_metadata, global_style, audio, audio_strategy, characters_and_subjects[], text_overlays[], shots[] (each shot carrying a generation_prompt/negative_prompt), regeneration_guide, virality_formula, evaluation. A producer should prefer the blueprint over reverse-engineering from raw content whenever one exists.
3. Produce studio_markdown¶
Once assembled, the proposal is written to the hub as markdown:
POST /api/studio/{platform}
{
"text": "...",
"filename": "2026-07-18-similar-content-<slug>.md",
"agent": "similar-content",
"kind": "clone",
"status": "proposed"
}
kind is one of clone | proposal | idea | template, matching the manifest's kind. The filename convention is <date>-<agent>-<slug>.md; a sidecar studio/{p}/meta.json tracks status/agent/kind alongside it.
Omitting status preserves it — it does not reset to proposed
The hub resolves the saved status as body.status or existing.status or "proposed". So proposed is the default only on a first insert; on a re-POST of an existing filename the current gate state is kept.
This is what makes re-posting safe. A renderable producer re-POSTs its own markdown after a render to stamp the finished media onto the item — without preservation that write would silently flip a human's approved back to proposed and un-gate it. Producers should omit status on re-POST for exactly this reason, and only ever send it deliberately.
The ## Audio block is mandatory
Every proposal markdown file must include a ## Audio section. Because sound selection frequently needs a human ear and can't be safely automated end-to-end, this section is a manual-post instruction block rather than an executable directive — see below.
4. Human gate¶
A proposal sits at status: "proposed" until a human reviews it in the Dashboard and calls:
POST /api/studio/{platform}/{file}/status
{ "status": "approved", "note": "optional reviewer note" }
This is appended to studio/{p}/gate.jsonl and is the only way a proposal moves to approved or rejected. The hub's GET /api/agents/{name}/board reducer left-joins this gate log by filename onto the producer's board, overwriting the Proposed lane with Approved/Rejected. The producer agent itself never emits those two events.
5. Render (renderable producers only)¶
Approved is no longer the end of the line. For a producer that declared renderable: true, approval is what unlocks the paid half:
POST /api/studio/{p}/{file}/render # body: {"force": false}
The hub resolves the producing agent from the item, validates its dir and render_cmd, and launches it as a background job. The producer generates the media and uploads it:
POST /api/renders/{p} # RenderIn: file (join key) + metadata + base64 assets
Rules the hub enforces:
- 409 unless the item is
approved— and 409 if it has no producing agent recorded. - One render per studio item. The
render_idis derived server-side from the studio filename, so re-rendering overwrites in place and path traversal is impossible at the source. - A per-item lock. The job id is deterministic (
{platform}:render:{file}); a second call while one is in flight returnsalready_running: truerather than starting a duplicate paid render. - Never in
run-all. Rendering costs money per frame, so it is excluded from the one-click pipeline and only ever runs when a human presses the button.
Generated media lands in renders/{p}/{render_id}/, served at /renders — structurally separate from /media, which is the scraped corpus. See API Reference → Renders.
How the media is made is up to the producer — including video-to-video
The contract is only "produce an approved item's media and POST /api/renders/{p}." SimilarContent generates still frames with an image model and stitches them with ffmpeg, but nothing here requires that. A video-to-video producer can call a video model (e.g. Google Veo / Flow), get an .mp4 directly, and upload it — no image generation, no ffmpeg. It uploads the finished clip (plus a poster) as the render assets instead of per-frame stills; the base64 upload is capped at 64 MB decoded (MAX_RENDER_BYTES, HTTP 413 over that), which fits a typical 20–40 MB clip. Such an agent is spun up from _producer-template/ exactly like any other — it just declares renderable: true and points render_cmd at its own render subcommand.
Output shape knobs¶
A renderable producer declares its canvas in config_schema, so it is editable from the Dashboard. SimilarContent's two:
| Knob | Default | Values | Meaning |
|---|---|---|---|
aspect_ratio |
9:16 |
9:16, 4:5, 1:1 |
The output canvas. 9:16 (1080×1920) is the reels/shorts/tiktok format and the only one that fills a phone full-bleed. 4:5 = 1080×1350 (IG feed portrait), 1:1 = 1080×1080. |
video_fit |
auto |
auto, cover, contain |
How a generated frame meets that canvas. auto crops when the frame is within 10% of the canvas aspect and letterboxes when it is further out; cover always crops; contain always letterboxes. None of them ever stretch. |
No width/height knob, deliberately
The canvas is derived from aspect_ratio, so the output can never be a size that disagrees with the aspect it claims to be.
Sequence: register → propose → gate → render¶
sequenceDiagram
participant P as Producer (e.g. SimilarContent)
participant Hub as Hub (:8787)
participant D as Dashboard (human gate)
P->>Hub: POST /api/producers/register (manifest)
Hub-->>P: 200 (upserted, producers/registry.json)
P->>Hub: GET /api/corpus/{p}/top / search
Hub-->>P: candidate targets
P->>Hub: GET /api/analysis/{p}/{content_id}
Hub-->>P: blueprint (schema_version 2)
Note over P: generate visuals locally,<br/>assemble proposal markdown
P->>Hub: POST /api/studio/{p} {text, agent, kind, status:"proposed"}
Hub-->>P: 200 (studio/{p}/<file>.md + meta.json)
D->>Hub: GET /api/studio/{p}?status=proposed
Hub-->>D: proposal list
Note over D: human reviews proposal in Dashboard
D->>Hub: POST /api/studio/{p}/{file}/status {status:"approved"|"rejected"}
Hub-->>D: 200 (studio/{p}/gate.jsonl appended)
Note over Hub,D: Approved unlocks render<br/>(gate lane derived, never emitted by P)
D->>Hub: POST /api/studio/{p}/{file}/render
Hub->>P: launch render_cmd in dir (validated argv)
Note over P: generate frames, stitch, caption<br/>(paid — human-triggered only)
P->>Hub: POST /api/renders/{p} {file, metadata, assets[]}
Hub-->>P: 200 (renders/{p}/{render_id}/ + video_url)
D->>Hub: GET /api/renders/{p}
Hub-->>D: rendered reels, playable inline at /renders
Per-item events emitted by the producer along this sequence: item.start (Generating) → item.stage (Self-eval) → item.done (Proposed, must include data.file — the join key the board's gate-join reducer uses) → external gate → Approved/Rejected (derived only). item.error → the implicit Failed lane on any failure. See Agents: Dashboard for how the board folds live SSE log events onto this reduced snapshot.
The ## Audio manual-post block¶
Because sound licensing, trend timing, and platform-native audio selection are hard to fully automate, every studio proposal's markdown carries a ## Audio section written as an instruction to the human operator posting the clip, not as a machine-actionable field.
## Audio
Use the original sound from the source clip if `audio_strategy` marks it
reusable; sound_id `<audio_id>`, title "<title>". If not reusable, use a
trending Rising/Hot sound instead — see the platform's Sounds panel in the
Dashboard for the ranked shortlist, then attach it manually when posting.
Do not auto-attach: verify the sound is still available on the target
platform before publishing.
The producer arrives at this recommendation by branching on the blueprint's audio_strategy field: if the source sound is reusable, the block names it directly; otherwise the producer calls GET /api/audio/{p}/trending?reusable_only=&mood= (optionally GET /api/audio/{p}/sound/{audio_id} for detail) and recommends a trending alternative. Either way, the actual attach-to-post action is manual — the pipeline stops at "propose," never "publish."
Why this is manual, not automated
Trending-sound relevance windows are short and platform sound catalogs aren't reliably reachable via API for attach actions. Rather than silently guess or risk a broken/expired sound reference, the SPI treats audio selection as a recommendation surfaced to the human at post time.
Copy-scaffold recipe: building a new producer¶
New producers are spun up from _producer-template/, never written from scratch. This keeps every producer's registration, config, logging, and secrets handling identical, which is what lets the Dashboard render new producer lanes automatically.
cp -r _producer-template MyNewProducer
cd MyNewProducer
-
Edit
agent.json— the manifest: setname,kind,consumes,produces(almost alwaysstudio_markdown),output_status(almost alwaysproposed),human_gate,needs_reference,config_schema,secrets[], andworkflow_stages[].If the producer will also render approved items into media, add the three render fields:
renderable: true,dir(this directory's name — it must be a direct sibling of the hub repo), andrender_cmd(an allowlisted launcher plus plain arguments, e.g.["uv", "run", "cli.py", "render"]). Extendworkflow_stages[]pastApprovedaccordingly — SimilarContent uses…, "Approved", "Rendering", "Rendered", "Rejected". If it only writes markdown, omit all three. 2. EditCLAUDE.md— describe the producer's specific generation method (what it reads, what transform it applies, what it writes) in terms of the SPI above. 3. Reviewmemory/—MEMORY.md,persona.md,patterns.mdship as empty scaffolding; the agent accumulates learnings here across runs (local, not synced to the hub except viaPOST /api/insights). 4. Check.claude/settings.local.json— the hub-only allowlist; confirm it still matches yourBACKEND_APIexpectations. 5. Copy.env.example→.envand fill in secret values — referenced by env-var NAME only inagent.json'ssecrets[]; the hub never stores the value, only presence (GET /api/config/agent/{agent}/secrets/status). 6. Open a session and implement the per-run method: register → consume corpus/analysis/audio/insights → generate → self-eval →POST /api/studio/{p}→ emit lifecycle logs withdata.fileonitem.done. A renderable producer adds a second, separately-invoked path: read one approved item, produce the media,POST /api/renders/{p}.
_producer-template/
├── CLAUDE.md
├── agent.json # manifest (kind, consumes, produces, human_gate, ...)
├── logsetup.py
├── memory/
│ ├── MEMORY.md
│ ├── persona.md
│ └── patterns.md
├── .claude/settings.local.json # hub-only allowlist
├── .env.example
└── .gitignore
Integrate via BACKEND_API, never a hardcoded path
Every producer, including new ones built from the scaffold, must read the hub's base URL from the BACKEND_API env var (default http://127.0.0.1:8787). This is the forward-compat rule that keeps the hub the single integration point — see Architecture.
The three future producers¶
These are not yet built, but are scoped precisely enough to spin up from the template on demand.
proposal-content (kind: proposal)¶
Generates N original script proposals grounded in the corpus's winning factors, rather than cloning one specific clip. Manifest declares human_gate: true explicitly, signaling that the output is a set of candidate scripts intended for human selection among alternatives — distinct from SimilarContent's single best-effort recreation. Otherwise follows the identical SPI: reads GET /api/corpus/{p}/factors and GET /api/corpus/{p}/brief for grounding, writes one or more kind:"proposal" studio proposals.
creative-idea (kind: idea)¶
Produces net-new viral concepts by cross-referencing the corpus's virality factors and formulas with trending audio, rather than adapting any single existing clip. Reads GET /api/corpus/{p}/factors, blueprint virality_formula fields across multiple analyses, and GET /api/audio/{p}/trending to find concept/sound pairings not yet represented in the corpus. Writes kind:"idea" studio proposals.
template-content (kind: template, needs_reference: true)¶
The only planned producer that needs external material. An operator supplies a reference video (not from the tracked corpus) via:
POST /api/reference/{platform} { "url": "...", "note": "..." }
The hub downloads the media (yt-dlp if available, else a direct GET — never login/cookie-based), assigns a synthetic ref_<hash> id, and marks it pending. AnalysisEngine picks this up from the reference queue (GET /api/reference/{p}/pending, alongside its normal blueprint queue) and writes a blueprint for it with is_reference: true, served at the same GET /api/analysis/{p}/{ref_id} route as ordinary blueprints. template-content then reads that reference blueprint's structure and applies it to the operator's own topic, producing a kind:"template" studio proposal. Reference videos are explicitly not corpus content — they are not scored and not treated as a real tracked reel.
Reference¶
| Concept | Detail |
|---|---|
| Registration | POST /api/producers/register, idempotent upsert by name |
| Roster | GET /api/producers, GET /api/producers/{name} |
| Proposal write | POST /api/studio/{platform} — {text, filename?, agent?, kind?, status?} |
| Proposal list | GET /api/studio/{platform}?status=&agent= |
| Human gate | POST /api/studio/{platform}/{file}/status — {status, note?} |
| Gate log | studio/{p}/gate.jsonl |
| Sidecar metadata | studio/{p}/meta.json |
| Terminal join key | data.file on the producer's item.done log event |
kind values |
clone \| proposal \| idea \| template |
See also¶
- Architecture — the 7-stage pipeline and component ownership
- Agents: AnalysisEngine — how blueprints are generated
- Agents: AutoSearch — the discovery-kind producer and its parallel gate
- Agents: Dashboard — how the board renders producer lanes and the human gate
- API Reference — full REST surface