Skip to main content
A Ralphy template is a template.yaml manifest plus supporting prose (TEMPLATE.md, composition.md, hooks.md, prompt-cookbook.md). Public templates are served from the hosted content library (Supabase); your own author into workspace/templates/<slug>/ (user-local). The old repo-public templates/<category>/<slug>/ folder was retired in #084. This page is the wire-format spec: the manifest Zod schema, the two kinds, the segment categories, the slug discipline, and the version gate.

Full example

A minimal vibe-style template at workspace/templates/brand-story/template.yaml:
version: 1
id: brand-story
aliases: []
kind: vibe-style
category: b2b-saas
name: Brand Story
description: >-
  Longer-form cinematic brand narrative (30-60s, sometimes 60-120s for hero
  films): founder voice or customer transformation arc carries problem → mission
  → solution → impact. Reference-required — a real brand identity (logo, founder
  photo, archive material, product, environment) must be supplied; AI cannot
  improvise a brand.
tags:
  - brand-story
  - brand-film
  - founder-story
  - cinematic
  - long-form
requires:
  refs: 1
scenes: []
references: []
A vibe-reference template adds a composition.md walkthrough plus a reference/ folder of worked artifacts alongside the manifest. Both kinds use the same schema.

File layout

templates/
  b2b-saas/
    brand-story/
      template.yaml          # the manifest (this page)
      TEMPLATE.md            # human-readable overview
      composition.md         # vibe-reference only — narrated walkthrough
      hooks.md               # hook library (prose)
      prompt-cookbook.md     # per-modality prompt fragments
      template.json          # legacy shape; loader prefers .yaml
  dtc-commerce/
  creator-lifestyle/
  entertainment-viral/
  cinematic-narrative/
workspace/
  templates/                 # user-local; flat layout (no category folder)
    my-template/
      template.yaml
The five category folders are fixed. workspace/templates/ is flat (no category subdir). When a slug exists in both, workspace overrides repo — per AGENTS invariant #10.

Zod schema

cli/lib/schemas/template.ts exports TemplateYamlSchema. Field-by-field:
FieldTypeDefaultValidation
version1 (literal)required; loader rejects unknown values with E_TEMPLATE_VERSION_UNSUPPORTED
idstringrequired; kebab-case; passes validateSlug()
aliasesstring[][]old slugs the template shipped as; kept ≥ one major-release cycle
kind"vibe-reference" or "vibe-style"required
categoryenum (5 values)required; see table below
namestringrequired; min length 1
descriptionstringrequired; min length 1
tagsstring[][]free-form discovery tags
requiresTemplateRequires{}declares brand / persona / ref dependencies
scenesSceneTemplate[][]optional scene scaffolding
estimated_cost_usdnumberoptional; non-negative
estimated_duration_snumberoptional; positive
referencesstring[][]asset slugs or paths the template uses

category — the five segment-persona folders

export const TEMPLATE_CATEGORIES = [
  "b2b-saas",
  "dtc-commerce",
  "creator-lifestyle",
  "entertainment-viral",
  "cinematic-narrative",
] as const;
Each value is a directory under templates/. The lint walks only these five.

kind — vibe-reference vs vibe-style

KindWhat shipsUse it when
vibe-referenceManifest + composition.md walkthrough + reference/ worked artifactsThe template encodes a full production: scene scaffolding, named refs, ready-to-render prompts.
vibe-styleManifest + hooks.md + prompt-cookbook.md (prose only)The template encodes a cookbook — hooks, camera vocab, worked example prompts — without a fixed scene list.
Five vibe-reference templates ship in repo; 38 vibe-style. Both kinds are loadable by ralphy template use <slug>.

requires

TemplateRequiresSchema = z.object({
  brand: z.boolean().optional(),
  persona: z.boolean().optional(),
  refs: z.number().int().nonnegative().optional(),  // single int count for v1.0
  music_style: z.string().optional(),
  voice_style: z.string().optional(),
}).partial().default({});
diagnoseRequiredInputs() checks the user’s project inputs against this block and returns the first missing requirement. Callers map the result to E_TEMPLATE_INPUT_MISSING. The refs field is a single count for v1.0; the 3-slot {cref, sref, pref} shape lands post-launch (decision D-02).

scenes — optional scaffolding

SceneTemplateSchema = z.object({
  id: z.string().regex(/^scene-\d{2,3}$/u),  // `scene-01`, `scene-001`
  role: z.enum(["hook", "body", "cta"]),
  duration_s: z.number().positive().max(120),
  direction: z.string().optional(),
});
vibe-reference templates fill this in. vibe-style templates leave it as []. A worked vibe-reference scenes block:
scenes:
  - id: scene-01
    role: hook
    duration_s: 3
    direction: "Cold open. Hand reaches for the product. Camera follows."
  - id: scene-02
    role: body
    duration_s: 8
    direction: "First use. Reaction. 35mm selfie, eye-level, hand-held."
  - id: scene-03
    role: body
    duration_s: 6
    direction: "Comparison cut. Before / after side-by-side."
  - id: scene-04
    role: cta
    duration_s: 3
    direction: "Direct address. Hand-product-reveal. Voice-over: 'try it'."
The scenarist consumes this scaffolding and emits a full Scenario conforming to cli/lib/schemas/scene.ts → ScenarioSchema. The full scene shape (with vo_text, camera, lighting, gesture, broll, refs, notes) is the scenarist’s output contract, not the template’s input. See Memory schemas and the scenarist playbook for the downstream shape.

Gesture vocabulary

When the template’s direction field hints at a body-language beat, the scenarist may attach a gesture to the resulting scene. The vocabulary is the finite enum in cli/lib/schemas/gestures.ts:
point-camera, nod, head-shake, laugh, shrug, lean-in,
hand-product-reveal, eye-roll, facepalm, thumbs-up, palm-open, pause-still
Niche one-off body language goes in Scene.notes (the free-text escape hatch), never in direction. The enum is closed by design (decision D-06) — adapters silently omit unknown values rather than throw, so a forward-compatible PR that adds a new gesture degrades gracefully on older binaries.

Slug discipline

Slugs are kebab-case. The regex:
const SLUG_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/u;
No uppercase, no underscores, no leading or trailing hyphen, no single-character slugs.

Banned tokens

DENIED_SLUG_TOKENS in cli/lib/schemas/template.ts blocks real-creator and brand tokens from appearing in slugs. The match is whole-token (delimited by start, end, or hyphen) so arogant-pov is fine but joe-rogan-pod is not. Currently denied:
hormozi, mr-beast, mrbeast, oldspice, old-spice, kardashian,
rogan, huberman, tornow, codie-sanchez, alex-becker
Creator references stay legal as prose inside TEMPLATE.md / composition.md — never in the slug. The rule is decision D-05: archetypal slugs only. validateSlug(slug) returns {ok: true} or {ok: false, reason, token?}. Callers build E_TEMPLATE_SLUG_INVALID from the rejection payload.

Version gating

version: 1
Every template.yaml must declare version: 1. The loader in cli/lib/templater/loader.ts → parseTemplateManifest() reads the version field first; if it is not in SUPPORTED_VERSIONS = [1], the loader calls raiseError("E_TEMPLATE_VERSION_UNSUPPORTED", {version, id}) and exits.
// cli/lib/templater/loader.ts
const versionField = (value as Record<string, unknown>).version;
if (!isSupportedVersion(versionField)) {
  raiseError("E_TEMPLATE_VERSION_UNSUPPORTED", {
    version: String(versionField),
    id,
  });
}
return TemplateYamlSchema.parse(value);
When scenes[] eventually gains a required field, the loader keeps a v1 reader alive for at least one major-release cycle (decision D-03). Authors bump version: 2 only when the schema breaks compatibly.

Manifest discovery

locateTemplateManifest(dir) prefers template.yaml, then falls back to template.json for not-yet-migrated templates (02.05.04 migration window). Both shapes parse through the same Zod schema. New templates should land as .yaml only.

Slug resolution

ralphy template use <slug> resolves a slug across both tiers:
  1. Search workspace/templates/<slug>/template.yaml.
  2. If not found, look the slug up in the public content library (Supabase).
  3. If still not found, search aliases (aliases: [old-slug]).
Workspace overrides the public tier on id collision. Aliases let you rename a template without breaking existing user scripts.

Validation

The manifest parses through the v1 Zod loader (cli/lib/templater/loader.ts) on every ralphy template show / use. It enforces:
  • The category is one of TEMPLATE_CATEGORIES.
  • The slug passes validateSlug() — kebab-case, no banned tokens.
  • template.yaml (or legacy template.json) is present and parses without throwing.
(The repo-folder lint:templates script was retired with the templates/ folder in #084 — there is no longer a committed repo catalog to walk.)

Authoring checklist

  1. Pick a category. One of b2b-saas, dtc-commerce, creator-lifestyle, entertainment-viral, cinematic-narrative.
  2. Pick a slug. Kebab-case, no creator names.
  3. Decide the kind. vibe-reference if you’re shipping a full production with refs; vibe-style if you’re shipping a prompt cookbook.
  4. Write template.yaml at workspace/templates/<slug>/. Start with version: 1, fill required fields, default the rest.
  5. Write TEMPLATE.md. Human-readable overview. Creator references go here, not in the slug.
  6. For vibe-reference: add composition.md (the narrated walkthrough) and reference/ (worked artifacts).
  7. For vibe-style: add hooks.md (hook library) and prompt-cookbook.md (per-modality fragments).
  8. Run ralphy template register <slug> then ralphy template suggest "<your test brief>". Verify the new template surfaces with a sensible score.
  9. To publish to the public Library, use the templater / dev-publish-template path.