Lesson Studio
Lesson Studio: studio projects, compositions, slides, project versions, comments, plus slide templates + themes, image rendering and YouTube thumbnails.
| Endpoint | https://studio.mcp.devfellowship.com/mcp |
|---|---|
| Tools | 73 in 17 groups |
| Package | packages/dfl-mcp-studio |
| Auth |
Your dfl-auth login token as Authorization: Bearer <token>. Every call runs as
you, under RLS. See Auth & security.
|
{ "mcpServers": { "dfl-studio": { "type": "http", "url": "https://studio.mcp.devfellowship.com/mcp", "headers": { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } } }}Other clients (Cursor, VS Code, codex, the Anthropic SDK): see Getting started, step 3.
| Backing data | studio projects, compositions, slides, versions, comments, video exports; slide templates + themes (read/update); PNG renders via the dfl-render headless capture; YouTube thumbnails via the Thumbify renderer on dfl-services. |
Comments
Section titled “Comments”Review comments on slides, and all comments of a version as one text block.
create_slide_comment
Create a review comment anchored to a slide in a Lesson Studio project (Course Canvas comments).
create_slide_commentCreate Slide Comment
Create a review comment anchored to a slide in a Lesson Studio project (Course Canvas comments). Stamped with the project current version; version 1 is auto-created if none exists.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project. |
body | string | yes | The comment text (change request / note) |
slide_id | string | no | UUID of the slide the comment targets. |
composition_id | string | no | UUID of the composition (for reference / canvas grouping) |
anchor_x | number | no | Optional Figma-style pin X coordinate on the canvas. |
anchor_y | number | no | Optional Figma-style pin Y coordinate on the canvas. |
list_slide_comments
List Course Canvas comments of a project (newest first), optionally filtered by version and/or resolved state.
list_slide_commentsList Slide Comments
List Course Canvas comments of a project (newest first), optionally filtered by version and/or resolved state. Enriched with composition title + slide order for reference.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project. |
project_version_id | string | no | Filter to comments stamped with this version. |
resolved | boolean | no | Filter by resolved state (true/false) |
update_slide_comment
Update a Course Canvas comment body and/or its resolved state.
update_slide_commentUpdate Slide Comment
Update a Course Canvas comment body and/or its resolved state.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the comment. |
body | string | no | New comment text. |
resolved | boolean | no | Mark resolved (true) or reopen (false) |
delete_slide_comment
Delete a Course Canvas comment by id.
delete_slide_commentDelete Slide Comment
Delete a Course Canvas comment by id.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the comment to delete. |
get_version_comments
Get all comments for a project version as a clipboard-ready text block, one comment per line prefixed with project/composition/slide references.
get_version_commentsGet Version Comments (clipboard text)
Get all comments for a project version as a clipboard-ready text block, one comment per line prefixed with project/composition/slide references. Defaults to the project current version. Use for the "copy all comments → paste to Claude" flow.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project. |
project_version_id | string | no | UUID of the version. If omitted, the current (max version_number) is used. |
Compositions
Section titled “Compositions”The lessons (compositions) inside a Studio project.
create_composition
Create a composition (lesson-level grouping / "frame") under a Lesson Studio project.
create_compositionCreate Composition
Create a composition (lesson-level grouping / "frame") under a Lesson Studio project. Slides attach to a composition.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the parent project. |
title | string | no | Composition title (default: "Untitled Composition") |
order_index | number | no | Display order within the project. If omitted, appended after existing compositions. |
list_compositions
List the compositions of a Lesson Studio project, ordered by order_index.
list_compositionsList Compositions
List the compositions of a Lesson Studio project, ordered by order_index.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the parent project. |
update_composition
Update a composition title and/or order_index.
update_compositionUpdate Composition
Update a composition title and/or order_index.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the composition. |
title | string | no | New title. |
order_index | number | no | New display order. |
delete_composition
Delete a composition. Its slides must be removed or reassigned first (slides.composition_id is NOT NULL).
delete_compositionDelete Composition
Delete a composition. Its slides must be removed or reassigned first (slides.composition_id is NOT NULL).
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the composition to delete. |
Exports
Section titled “Exports”Render a lesson to MP4, follow the render, check the result, and cut it into Story parts.
expire_stale_studio_exports
Find Lesson Studio video exports stuck in a non-terminal state (pending/processing) older than N hours and mark them failed with an explanatory error_message, so an abandoned export stops looking like it is still…
expire_stale_studio_exportsExpire Stale Lesson Studio Exports
Find Lesson Studio video exports stuck in a non-terminal state (pending/processing) older than N hours and mark them failed with an explanatory error_message, so an abandoned export stops looking like it is still running. Defaults to a DRY RUN — pass dry_run: false to actually write. RLS-scoped to the caller's own projects.
| Parameter | Type | Required | Description |
|---|---|---|---|
older_than_hours | number | no | Only expire exports whose started_at is at least this many hours ago (default 6, minimum 1). Keep this comfortably above your longest legitimate render. |
project_id | string | no | Restrict the sweep to a single project UUID. Omit to sweep all of the caller's projects. |
statuses | string[] | no | Which non-terminal statuses to sweep (default ["pending","processing"]). |
reason | string | no | Message written to error_message. Defaults to a user-facing explanation. |
limit | number | no | Maximum number of rows to expire in one call (safety cap). |
dry_run | boolean | no | When true (the DEFAULT), only report what WOULD be expired without writing. |
start_composition_export
Render a whole composition into an MP4, server-side.
start_composition_exportExport a Lesson Studio composition to MP4
Render a whole composition into an MP4, server-side. Returns as soon as the job is accepted — rendering takes minutes, so this does NOT wait: poll get_studio_export with the returned export_id until it is completed, then pass that export to register_export_as_media to get a media_id the post calendar accepts. Recording a webcam over the deck is still done in the Lesson Studio UI; this exports what is there. Set variant to mobile for a 9:16 reel/short/story cut; omit it to follow the project's own orientation.
| Parameter | Type | Required | Description |
|---|---|---|---|
composition_id | string | yes | The composition to render (from list_compositions). |
variant | enum | no | Aspect to cut to: desktop is landscape 16:9 (YouTube), mobile is portrait 9:16 (Instagram reels, YouTube shorts, stories). Omit to follow the project orientation (set at create_studio_project, changeable with update_studio_project). Use mobile to cut a portrait video out of a landscape project without changing it. One of: desktop, mobile. |
resolution | enum | no | Output size. Omit for the render service default (1080p). One of: 720p, 1080p, 4k. |
fps | number | no | Output frame rate. Omit for the render service default. |
get_studio_export
Read one export and, while it is still rendering, ask the render service where it is and write the answer back to the row.
get_studio_exportGet one Lesson Studio export
Read one export and, while it is still rendering, ask the render service where it is and write the answer back to the row. Poll this after start_composition_export until completed, then pass the export id to register_export_as_media. A failed export carries the real reason, not a generic one.
| Parameter | Type | Required | Description |
|---|---|---|---|
export_id | string | yes | The export to read (from start_composition_export). |
list_studio_exports
List the rendered video exports (MP4) of the caller's Lesson Studio projects, newest first.
list_studio_exportsList Lesson Studio Exports
List the rendered video exports (MP4) of the caller's Lesson Studio projects, newest first. Defaults to completed exports only. Use the returned export id with register_export_as_media to turn a video into a media_id the post calendar accepts.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | no | Restrict to one project. |
status | enum | no | Export status to match (default completed). all lists every state. One of: completed, failed, pending, processing, all. |
limit | number | no | Maximum rows (default 20, max 100). |
register_export_as_media
Make a completed Lesson Studio export (MP4) usable by the post calendar: creates a public.media row pointing at the exported file and returns the media_id plus the stable media.devfellowship.com/<id> link.
register_export_as_mediaRegister a Lesson Studio Export as Post Media
Make a completed Lesson Studio export (MP4) usable by the post calendar: creates a public.media row pointing at the exported file and returns the media_id plus the stable media.devfellowship.com/<id> link. Pass that media_id to draft_post_for_review on the campaigns MCP. Idempotent — the same export returns the same media_id. Only exports stored in the DevFellowship bucket can be registered.
| Parameter | Type | Required | Description |
|---|---|---|---|
export_id | string | yes | An export id from list_studio_exports (status must be completed). |
name | string | no | Display name for the media row. Defaults to the export file name. |
split_export_for_stories
Cut a completed export (MP4) into ordered Story parts of at most 60 s each, and register each part as media.
split_export_for_storiesSplit a Lesson Studio export into Instagram Story parts
Cut a completed export (MP4) into ordered Story parts of at most 60 s each, and register each part as media. The cuts follow the slide changes when the render service knows them, else scene changes. Returns the parts in order, each with a media_id — pass those media ids, in order, to the campaigns Story sequence. Waits up to ~45 s; if the split is still running, it returns split_job_id — then poll get_story_split with it.
| Parameter | Type | Required | Description |
|---|---|---|---|
export_id | string | yes | A completed export (from get_studio_export or list_studio_exports). |
max_segment_s | number | no | Longest part in seconds (6–60). Omit for the render service default (59). |
check_studio_export
A machine review of a COMPLETED export (dfl-render /media/export-check): the duration (and the gap to expected_duration_s), the frame size, black-frame ranges, JPEG frames just after each slide starts, at its middle and…
check_studio_exportCheck a Lesson Studio export
A machine review of a COMPLETED export (dfl-render /media/export-check): the duration (and the gap to expected_duration_s), the frame size, black-frame ranges, JPEG frames just after each slide starts, at its middle and just before it ends (slides from the render job), and a caption-over-face flag per mid-slide frame (one vision-LLM pass; face_check false skips it). flags lists what a reviewer must look at first. Waits ~50 s; if the check still runs it returns check_job_id — then call get_export_check.
| Parameter | Type | Required | Description |
|---|---|---|---|
export_id | string | yes | A completed export (get_studio_export / list_studio_exports). |
expected_duration_s | number | no | The length you expect, seconds. |
face_check | boolean | no | Default true: the caption/face overlap pass. |
get_export_check
Read an export check that check_studio_export started (the same answer when it is done).
get_export_checkGet an export check
Read an export check that check_studio_export started (the same answer when it is done).
| Parameter | Type | Required | Description |
|---|---|---|---|
check_job_id | string | yes | The check_job_id check_studio_export returned. |
get_story_split
Read a Story split job that split_export_for_stories started.
get_story_splitGet a Story split job
Read a Story split job that split_export_for_stories started. While it runs, this returns its status — call it again. When it is done, it registers each part as media and returns the parts in order with their media ids (the same answer split_export_for_stories gives). Safe to call again: a part keeps its media_id.
| Parameter | Type | Required | Description |
|---|---|---|---|
split_job_id | string | yes | The split_job_id that split_export_for_stories returned. |
Studio Projects
Section titled “Studio Projects”Create, list, edit and delete Lesson Studio projects.
create_studio_project
Create a new Lesson Studio project in the lesson_studio schema, plus a "Default" composition.
create_studio_projectCreate Lesson Studio Project
Create a new Lesson Studio project in the lesson_studio schema, plus a "Default" composition. Returns the project_id and the default composition_id (attach slides to a composition via create_slide).
| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | yes | Project title. |
owner_id | string | no | UUID of the owner (auth.users.id). Defaults to the authenticated caller. |
orientation | enum | no | Canvas orientation (default: landscape). social-portrait = Feed/carousel post, 1080×1350 (4:5): static slides, no camera/captions/video export; only templates declaring the social-portrait canvas. One of: landscape, portrait, social-portrait. |
description | string | no | Optional project description. |
settings | object | no | Optional settings JSON, merged OVER the defaults (resolution, fps, backgroundColor, theme_id, and portrait's 1:1 camera) rather than replacing them. Pass theme_id here to start on a theme other than the DFL one; update_studio_project changes it later. |
list_studio_projects
List Lesson Studio projects owned by the caller (RLS-scoped), most recently updated first.
list_studio_projectsList Lesson Studio Projects
List Lesson Studio projects owned by the caller (RLS-scoped), most recently updated first.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | no | Max projects to return (default: 50, max: 100) |
offset | number | no | Number of projects to skip (pagination) |
search | string | no | Search by title (ilike) |
delete_studio_project
Delete a Lesson Studio project.
delete_studio_projectDelete Lesson Studio Project
Delete a Lesson Studio project. Its compositions, slides, slide_elements and slide_media cascade. RLS-scoped: only the caller's own projects can be deleted.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project to delete. |
update_studio_project
Update a project's title, description, thumbnail, orientation, active theme (settings.theme_id), burned-in captions (settings.captions_*) or export watermark (settings.watermark).
update_studio_projectUpdate Lesson Studio Project
Update a project's title, description, thumbnail, orientation, active theme (settings.theme_id), burned-in captions (settings.captions_*) or export watermark (settings.watermark). For a 9:16 reel, pass captions {enabled: true, position: "top"} and watermark {enabled: true} before start_composition_export. Camera defaults are set_project_camera's job, not this tool's. theme_id is checked against list_themes when the registry is reachable; an unknown id is rejected. A settings write (theme_id, captions, watermark) is guarded on updated_at (it reads settings first to merge; keys you leave out keep their stored value); a change to the other fields overwrites only the columns passed. Changing orientation does NOT reshape camera boxes — it warns and points at set_project_camera.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project. |
title | string | no | New title. |
description | string | no | New description, or null to clear it. |
thumbnail_url | string | no | New thumbnail URL, or null to clear it. |
orientation | enum | no | Canvas orientation. social-portrait = Feed/carousel post, 1080×1350 (4:5): static slides, no camera/captions/video export; only templates declaring the social-portrait canvas. Changing an existing project to or from social-portrait is refused unless it has no slides (soft-deleted included); an empty project switching resets safe_area_top and camera settings to the target orientation defaults. One of: landscape, portrait, social-portrait. |
theme_id | string | no | Theme id to apply project-wide, as reported by list_themes (e.g. "default", "devfellowship"). |
captions | object | no | Burned-in caption settings, merged key by key into settings.captions_*. |
watermark | object | no | Export watermark settings, merged key by key into settings.watermark. The logo comes from the theme. |
Slides
Section titled “Slides”Create a slide or an image slide, edit, delete or restore slides, and find stock photos for them.
create_slide
Create a slide under a composition (mirrors the Lesson Studio editor insert shape so it is valid + editable in the UI).
create_slideCreate Slide
Create a slide under a composition (mirrors the Lesson Studio editor insert shape so it is valid + editable in the UI). Optional template_data.animation animates the slide. Preset 'steps-reveal' shows list items one at a time; use it when the narration walks through steps, on a template whose items carry data-anim-target="step". Example: {preset:'steps-reveal',version:1,beat_source:'manual',beats:[{kind:'reveal',target:'step',index:0,start_ms:600,duration_ms:300}]}. Sort beats by start_ms. Every beat must end before the slide duration_ms. The response can list warnings. sfx: [{sound, at_ms, volume?}] — see list_sound_effects.
| Parameter | Type | Required | Description |
|---|---|---|---|
composition_id | string | yes | UUID of the parent composition. |
project_id | string | no | UUID of the parent project. Resolved from the composition if omitted. |
order_index | number | no | Display order within the composition. If omitted, appended at the end. |
template_id | string | no | Slide template id, as published in the dfl-slide-templates registry. Call search_templates("<what the slide should do>") or list_templates to pick one, and get_template("<id>") for its exact slots — this list is NOT enumerated here on purpose, so it can never go stale. An id that is not in the registry is rejected. Required slots must carry real content (see template_data). For a single image filling the whole canvas, prefer the dedicated create_image_slide tool. |
template_data | object | no | Template data JSON (text/props consumed by the template renderer). Validated against the slots the template declares in the registry (templates/<id>/config.yaml) — call get_template("<id>") to see each slot's type and a valid sample. Required slots must be non-empty and correctly shaped: text fields need real text (not "" or whitespace); array fields need at least one entry matching the sample shape (e.g. bullet-list items: [{ "text": "Agents run tools autonomously" }], table rows: [{ "cells": [{ "value": "Revenue" }] }]). Use template_id "blank" for an intentionally empty slide. |
duration_ms | number | no | Slide duration in ms, a positive integer (default: 5000) |
background_color | string | no | Hex background color (default: "#1a1a2e"). Only a color other than the default sets template_data.background_override = true, so the Studio shows it over the template background. |
background_image_url | string | no | Optional background image URL. A non-empty URL also sets template_data.background_override = true. |
transition_type | string | no | Transition type (default: "fade") |
transition_duration_ms | number | no | Transition duration in ms (default: 500) |
create_image_slide
Create a slide that displays a single image filling the entire slide canvas, using the fullscreen-image template.
create_image_slideCreate Full-screen Image Slide
Create a slide that displays a single image filling the entire slide canvas, using the fullscreen-image template. Use this to bring your own finished slide images (e.g. an image hosted on S3, or a pre-rendered slide exported from another tool) instead of reusing the built-in content templates. The fit option controls how the image fills the canvas: cover (default) crops to fill, contain letterboxes to show the whole image.
| Parameter | Type | Required | Description |
|---|---|---|---|
composition_id | string | yes | UUID of the parent composition. |
project_id | string | no | UUID of the parent project. Resolved from the composition if omitted. |
image_url | string | yes | URL of the image to display full-screen (e.g. an S3 URL) |
fit | enum | no | How the image fills the canvas: "cover" (crop to fill, default) or "contain" (letterbox the whole image) One of: cover, contain. |
bg | string | no | Letterbox/background color (hex) used behind the image in "contain" mode. Defaults handled by the template CSS. |
image_alt | string | no | Accessibility alt text for the image. |
order_index | number | no | Display order within the composition. If omitted, appended at the end. |
duration_ms | number | no | Slide duration in ms (default: 5000) |
background_color | string | no | Hex background color (default: "#1a1a2e") |
transition_type | string | no | Transition type (default: "fade") |
transition_duration_ms | number | no | Transition duration in ms (default: 500) |
list_slides
List the slides of a composition, ordered by order_index.
list_slidesList Slides
List the slides of a composition, ordered by order_index. Soft-deleted slides are excluded by default; pass include_deleted:true to return all (including soft-deleted).
| Parameter | Type | Required | Description |
|---|---|---|---|
composition_id | string | yes | UUID of the parent composition. |
include_deleted | boolean | no | When true, also return soft-deleted slides (deleted_at IS NOT NULL). Default false → only live slides. |
update_slide
Update a slide. template_data replaces the slots; an omitted slot is removed.
update_slideUpdate Slide
Update a slide. template_data replaces the slots; an omitted slot is removed. Studio keys (not slots): narration_notes, animation, background_override, camera, camera_box, capture, camera_box_custom, watermark, webcam_layout, pointer_track, is_cover, sfx, framing, caption_cues, clips, style_overrides, slide_name, caption_cues_source, caption_language, camera_segments, content_over_camera, caption_position, camera_focus, insert. An omitted Studio key is kept, except animation: a template_id change removes it unless you send it. Remove one via clear_template_data_keys, only when the user asks. narration_notes and pointer_track are user work and cannot be restored. The response lists removed_template_data_keys. A new background sets background_override = true. To show the template background again, send clear_template_data_keys: ['background_override'] and no background field. A sent animation is checked as the create_slide description says, against the slide duration_ms. A new duration_ms is checked against the stored animation. The response can list warnings. sfx: [{sound, at_ms, volume?}] — see list_sound_effects.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the slide. |
order_index | number | no | New display order in the composition. |
composition_id | string | no | Move the slide to another composition. |
template_id | string | no | Registry template id. An unknown id is rejected. |
template_data | object | no | Template slot values, validated against get_template("<id>"). |
clear_template_data_keys | enum[] | no | Studio keys to remove; do not also send in template_data. |
duration_ms | number | no | Slide duration in ms (positive int) |
background_color | string | no | Hex background color. |
background_image_url | string | no | Background image URL. |
transition_type | string | no | Transition type. |
transition_duration_ms | number | no | Transition duration, ms. |
delete_slide
Delete a slide. By default this is a SOFT delete (recoverable via restore_slide; slide_elements/slide_media are kept).
delete_slideDelete Slide
Delete a slide. By default this is a SOFT delete (recoverable via restore_slide; slide_elements/slide_media are kept). Pass hard:true for a permanent delete (slide_elements + slide_media cascade).
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the slide to delete. |
hard | boolean | no | When true, permanently delete the slide (slide_elements + slide_media cascade). Default false → soft delete (recoverable via restore_slide). |
restore_slide
Restore a soft-deleted slide (clears deleted_at/deleted_by so it reappears in list_slides).
restore_slideRestore Slide
Restore a soft-deleted slide (clears deleted_at/deleted_by so it reappears in list_slides). Only works on slides that were soft-deleted via delete_slide; a hard-deleted slide is gone permanently.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the slide to restore. |
search_slide_images
Search the stock photo library for an image to put on a slide, and get back candidate URLs.
search_slide_imagesSearch Slide Images
Search the stock photo library for an image to put on a slide, and get back candidate URLs. Use it to fill an image-type template slot without a human opening the editor: pick a result and write its url with update_slide (template_data.image) or create_image_slide (image_url). This tool only reads — it never touches a slide. Match orientation to the project canvas so the crop is not fighting the layout. Results carry photographer and pexels_url: the licence asks for that credit, so keep it with the image.
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | yes | What to look for, in English — e.g. "developer at a laptop", "solar panels". Stock search is keyword-based, so a short concrete noun phrase beats a sentence. |
orientation | enum | no | Crop to ask the provider for. Defaults to landscape; pass portrait for a 9:16 project. social-portrait (Feed, 1080×1350 4:5) searches as portrait. One of: landscape, portrait, social-portrait. |
per_page | number | no | How many candidates to return (default 8, max 20). |
Free-canvas Elements
Section titled “Free-canvas Elements”The boxes drawn on top of a slide’s template — text, image, or shape — that set_slide_animation’s element-reveal preset animates. Previously an agent could only animate a box the editor UI had already drawn; these four create, list, edit and delete the boxes themselves. create_slide_element/list_slide_elements return animation_index: a box’s position among the slide’s elements, ordered by creation time — the exact index a reveal beat with target: "element" must reference. delete_slide_element also repairs template_data.animation in the same call, so a deleted box’s beat does not silently keep pointing at the wrong one.
Text boxes, images and shapes placed freely on a slide.
create_slide_element
Create a free-canvas box on a slide: a text box, an image, or a shape drawn on top of the slide's template.
create_slide_elementCreate Slide Element
Create a free-canvas box on a slide: a text box, an image, or a shape drawn on top of the slide's template. position_x/position_y/width/height/rotation are DESIGN pixels — the canvas is 1280x720 for a landscape project and 720x1280 for a portrait one (the parent project's own orientation, not an argument here). A box entirely outside that canvas is rejected; a box that only partly overlaps it is allowed (that is a normal partial-off-canvas placement, not an error). content is validated by type: text.text is at most 2000 characters; image.url must be an http(s) URL; every color (text.color, shape.fill, shape.stroke, ...) must be a hex color (#rgb, #rrggbb, #rrggbbaa) or an rgba()/rgb() function; an unknown content key is rejected. A 'text' box may omit content fields — missing ones are filled from the Studio's own default text style, so { type: 'text', content: { text: 'Hello' } } is enough for a fully-styled box. 'image' and 'shape' boxes must supply every field their type needs. The response carries animation_index: the box's position among the slide's elements, ordered by creation time — THIS is the index a set_slide_animation "element-reveal" beat ({ kind: "reveal", target: "element", index }) must use to animate THIS box, because that index is the box's position in the stored array, never its z_index or the order it was drawn on screen. Example: create_slide_element({ slide_id, type: "text", content: { text: "Welcome" }, position_x: 100, position_y: 80, width: 600, height: 120 }) → { element: {...}, animation_index: 0 }.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide to add the box to. |
type | enum | yes | Box kind: text, image, or shape. One of: text, image, shape. |
content | object | no | Type-specific content. text: { text, fontFamily?, fontSize?, fontWeight?, color?, textAlign?, lineHeight?, letterSpacing?, fontStyle?, textTransform?, backgroundColor?, verticalAlign? } — missing fields fall back to the Studio default text style. image: { url, alt, objectFit, crop?, enter?, growFrom? } — url, alt and objectFit are required, no defaults. shape: { shapeType, fill, stroke, strokeWidth, borderRadius? } — all of shapeType/fill/stroke/strokeWidth are required. |
position_x | number | yes | Left edge, design pixels. |
position_y | number | yes | Top edge, design pixels. |
width | number | yes | Box width, design pixels. |
height | number | yes | Box height, design pixels. |
rotation | number | no | Degrees, clockwise (default 0) |
opacity | number | no | 0 (invisible) to 1 (opaque); default 1. |
z_index | number | no | Stacking order among the slide's boxes. Default: one above the highest existing box. |
list_slide_elements
List a slide's free-canvas boxes (text/image/shape), ordered by creation time.
list_slide_elementsList Slide Elements
List a slide's free-canvas boxes (text/image/shape), ordered by creation time. Each element carries animation_index: its position in this order, and the exact index a reveal beat with target: "element" must reference to animate it — never the box's z_index or draw order. Example response: { elements: [{ id, type: "text", content: {...}, position_x, position_y, width, height, rotation, opacity, z_index, animation_index: 0 }, ...] }.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide. |
update_slide_element
Update a free-canvas box's content and/or geometry (position_x/position_y/width/height/rotation/opacity/z_index).
update_slide_elementUpdate Slide Element
Update a free-canvas box's content and/or geometry (position_x/position_y/width/height/rotation/opacity/z_index). Every field is optional — send only what changes. content is a PARTIAL patch: it is merged over the box's stored content and the merged object is re-validated against its type's rules (same rules as create_slide_element — text max 2000 chars, image url must be http(s), colors must be hex or rgba()/rgb(), unknown keys rejected). Geometry is design pixels, same canvas rule as create_slide_element: a box moved/resized fully outside the canvas is rejected. The box's type and its animation_index (set by create order) cannot be changed here. Example: update_slide_element({ id, content: { text: "Updated headline" }, position_x: 140 }).
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the slide element. |
content | object | no | Partial content patch, merged then re-validated. |
position_x | number | no | Left edge, design pixels. |
position_y | number | no | Top edge, design pixels. |
width | number | no | Box width, design pixels. |
height | number | no | Box height, design pixels. |
rotation | number | no | Degrees, clockwise. |
opacity | number | no | 0 (invisible) to 1 (opaque) |
z_index | number | no | Stacking order among the slide's boxes. |
delete_slide_element
Delete a free-canvas box and repair the slide's animation in the same call.
delete_slide_elementDelete Slide Element
Delete a free-canvas box and repair the slide's animation in the same call. A reveal beat (target: "element") addresses a box by its position among the slide's elements (animation_index), so removing a box shifts every later box's beat index down by one — this tool does that shift automatically: the removed box's own beat is dropped, later beats are reindexed keeping their effect/focus, and if no element beat survives template_data.animation is removed entirely (an animation with zero beats is not valid). The response reports removed_beat_count, shifted_beat_count and animation_cleared so the caller knows exactly what changed.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the slide element to delete. |
Slide Animation
Section titled “Slide Animation”template_data.animation makes a slide play: list items revealed one by one, a chat prompt typed out, a highlight wiped over a definition, an image easing in, a push-in zoom. The timings are a contract shared with dfl-render, so the editor preview and the exported MP4 show the same frames.
create_slide and update_slide already accept and validate a hand-written animation. These two tools exist so nobody has to write one by hand: the beats come from the slide’s own template_data and duration_ms, through the same defaults the editor’s “Add animation” button applies — already fitted to the slide length and valid at 24, 30 and 60 fps.
zoom-focus is the one preset with no template slot — it reads nothing from the slide, so list_animation_presets always reports it as runnable, on any template. A zoom beat also composes with every other preset: mix one into a steps-reveal/chat-typing/highlight/image-enter spec’s beats array and it pushes in on top, because it drives the synthetic slide target no template markup ever declares.
To remove an animation, call update_slide with clear_template_data_keys: ['animation'].
Single-beat editing
Section titled “Single-beat editing”set_slide_animation writes the WHOLE beats array — the right call when applying or re-fitting a preset. These four edit ONE beat of an existing spec without recomputing every other default, and each write marks beat_source: 'manual' so the editor knows a beat was hand-tuned rather than re-derived from the preset.
Animate a slide with a preset, and edit its animation step by step.
list_animation_presets
The slide animation presets set_slide_animation can apply, with the template_data slots each one reads.
list_animation_presetsList Animation Presets
The slide animation presets set_slide_animation can apply, with the template_data slots each one reads. Pass template_id to also learn which of them that template can actually run: the answer is read from the live template markup (data-anim-target), so it cannot go stale. A preset whose slots the slide does not carry produces no beats — that is what requires describes.
| Parameter | Type | Required | Description |
|---|---|---|---|
template_id | string | no | Slide template id, as published in the dfl-slide-templates registry. When given, each preset also reports runs_on_template and the targets the template declares. |
set_slide_animation
Animate a slide with one of the presets from list_animation_presets.
set_slide_animationSet Slide Animation
Animate a slide with one of the presets from list_animation_presets. With no beats, the timings are computed from the slide's own template_data and duration_ms — the same defaults the Lesson Studio editor applies, already fitted to the slide length and valid at 24, 30 and 60 fps. Pass beats only for a timing the defaults do not express. "zoom-focus" is the one preset with no template slot — it works on any slide, pushing in and holding until a later zoom beat frames back out. A "zoom" beat also COMPOSES with every other preset (add one to the "beats" array of a steps-reveal/chat-typing/highlight/image-enter spec): it drives the synthetic "slide" target no template markup ever declares, so it never collides with that preset's own beats. Writes template_data.animation and leaves every other template_data key untouched. To remove an animation, call update_slide with clear_template_data_keys: ['animation'].
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide to animate. |
preset | enum | yes | Animation preset. A preset the slide's template cannot drive is REFUSED here, not silently stored: call list_animation_presets("<template_id>") to see which ones run. A preset whose targets the template does not declare drives nothing, and one whose slots the slide does not carry is rejected here. "zoom-focus" always runs_on_template: true — it reads no slot and needs no declared target. "element-reveal" is the same kind of preset for the free-canvas elements of the slide (slide_elements rows, never a template_data slot): it needs at least one element on the slide. One of: steps-reveal, chat-typing, highlight, image-enter, zoom-focus, element-reveal. |
beats | object[] | no | Explicit beats, sorted by start_ms, every one ending before the slide duration_ms. Omit this to get the preset defaults fitted to the slide — that is the recommended path. Sending beats marks the spec beat_source "manual", so the editor will not silently re-time them. A "zoom" beat ({ kind: "zoom", target: "slide", start_ms, duration_ms?, rect: { x, y, width } }) may be mixed into ANY preset's beats to push in on part of the canvas; rect is fractions of the canvas (same shape as camera_box), width floored at 0.1 (10x zoom); a rect of { x:0, y:0, width:1 } zooms back out to the full canvas. A "reveal" beat on target "element" (one of a slide's free-canvas boxes, see create_slide_element/list_slide_elements for the index) may also carry: effect, one of "fade", "rise", "drop", "slide-left", "slide-right", "zoom-in", "shrink-in", "spin-shrink", "pop", "blur-in", "swing-in", "bounce-in", "scramble-in", "glitch-in", "slide-up", "slide-down", "flip-in", "wipe-right", "wipe-up", "typewriter-in", "count-up", "pulse", "shake", "wobble", "tada", "heartbeat", "spin", "ken-burns", "float", "breathe", "fade-out", "drop-out", "shrink-out", "spin-out", "blur-out", "glitch-out", "rise-out", "slide-left-out", "slide-right-out", "zoom-through-out", "wipe-left-out" — plays instead of the default fade + rise; and focus (boolean) — dims and blurs the rest of the slide while this box is the subject, for a camera-lens feel with no separate zoom beat. Example: { kind: "reveal", target: "element", index: 0, start_ms: 600, duration_ms: 400, effect: "pop", focus: true }. |
tracks | object[] | no | Keyframe tracks for free-canvas boxes (spec version 2). Each track is { index, property, keyframes: [{ t_ms, value, easing }] }: property is one of "x", "y", "scale", "rotate", "opacity", "blur"; easing one of "linear", "in", "out", "inOut", "hold" and shapes the segment that STARTS at that keyframe ("hold" jumps at the next one). Values are relative to the box rest pose: x/y are design-pixel offsets, rotate a degree offset, blur added px, scale and opacity (0..1) multipliers. Keyframes strictly increasing in t_ms and before the slide end; one track per box and property; a box revealed by a beat with no effect cannot take tracks. With tracks and no beats the spec animates the boxes by tracks alone (no default entrances are added). Example: { index: 0, property: "x", keyframes: [{ t_ms: 0, value: 0, easing: "inOut" }, { t_ms: 1200, value: 300, easing: "linear" }] }. |
formulas | object[] | no | Formula-driven box properties (spec version 2): { index, property, source, start_ms, end_ms }. source is an expression (max 200 chars) over t (seconds), p (0..1 across [start_ms, end_ms]), i (box index), w/h (the canvas) with + - * / % ^, unary minus, pi and sin cos abs min max clamp lerp step smoothstep noise(x, seed?). Same units as a track value; outside its window the value holds its end values; a non-finite result is identity. One driver per box and property: a property with a track cannot also take a formula. A parse error names the character position. Example: { index: 0, property: "y", source: "sin(t * 2 * pi) * 20", start_ms: 0, end_ms: 3000 }. |
canvas | object | no | Design canvas a formula reads as w/h. Defaults to the project's design canvas: 1280x720 landscape, 720x1280 portrait. |
behaviours | object[] | no | Optional metadata naming the behaviour that wrote a box's formulas, so the Lesson Studio picker re-opens it: { index, id, params: { name: number }, owned: [properties] }, one per box; every owned property needs a formula on that box. It is NOT expanded here: send the formulas it stands for (the Studio catalog: float → y, orbit → x/y, shake → x/y, pulse → scale, drift → x/y, typewriter-cursor → opacity). |
list_slide_beats
List the beats of a slide's template_data.animation, each tagged with the beat_index add_slide_beat/update_slide_beat/remove_slide_beat address.
list_slide_beatsList Slide Beats
List the beats of a slide's template_data.animation, each tagged with the beat_index add_slide_beat/update_slide_beat/remove_slide_beat address. Returns an empty beats array (not an error) when the slide carries no animation.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide. |
add_slide_beat
Insert one beat into a slide's template_data.animation.beats.
add_slide_beatAdd Slide Beat
Insert one beat into a slide's template_data.animation.beats. The slide must already carry an animation (create one with set_slide_animation first). Beats must stay sorted by start_ms; a beat or at_index that breaks the order is rejected with the same validator set_slide_animation uses. Marks beat_source "manual". Returns the new beat_index.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide. |
beat | object | yes | The beat to insert — same shape set_slide_animation's "beats" entries take, e.g. {kind:"zoom", target:"slide", start_ms, rect:{x,y,width}} or {kind:"reveal", target:"step", index, start_ms, duration_ms}. |
at_index | number | no | Position to insert at (0 = first). Omit to append at the end. |
update_slide_beat
Replace one beat of a slide's template_data.animation.beats, addressed by beat_index (from list_slide_beats).
update_slide_beatUpdate Slide Beat
Replace one beat of a slide's template_data.animation.beats, addressed by beat_index (from list_slide_beats). beat replaces the ENTIRE beat, not just the fields you pass — send every field the beat needs, same shape set_slide_animation's "beats" entries take. Beats must stay sorted by start_ms after the replacement. Marks beat_source "manual".
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide. |
beat_index | number | yes | Index of the beat to replace (from list_slide_beats) |
beat | object | yes | The full replacement beat, including kind. |
remove_slide_beat
Remove one beat from a slide's template_data.animation.beats, addressed by beat_index (from list_slide_beats).
remove_slide_beatRemove Slide Beat
Remove one beat from a slide's template_data.animation.beats, addressed by beat_index (from list_slide_beats). A spec needs at least one beat, so removing the only beat is rejected — use update_slide with clear_template_data_keys: ['animation'] to drop the animation entirely. Marks beat_source "manual".
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide. |
beat_index | number | yes | Index of the beat to remove (from list_slide_beats) |
Camera
Section titled “Camera”The presenter’s webcam box: a project-wide default (projects.settings.camera_box / camera_aspect / camera_default) that any slide can override (slides.template_data.camera_box / camera) — the same precedence dfl-render resolves at export time, so the editor preview and the MP4 never disagree about which value won.
A box is either a named preset (size: small/medium/large × corner: one of the four corners), resolved against the project’s orientation and camera aspect, or an exact custom rectangle (x/y/width as canvas fractions, mirroring a zoom rect). A custom box is stored tagged mode: 'custom', mirroring dfl-lesson-studio’s own flag (PR #422) — this is what stops the editor from reading a hand-placed box as though some preset button chose it.
Camera aspect is 16:9 (the legacy PiP shape) or 1:1 (the square crop). Changing a project’s aspect with no explicit box in the same set_project_camera call reshapes the current project box AND every slide’s own camera_box override to fit the new aspect — the same cascade the Lesson Studio editor’s aspect switch performs.
Where the presenter's camera shows on the project or on one slide.
get_camera
Read the camera box + visibility for a project (project_id) and optionally one of its slides (slide_id).
get_cameraGet Camera
Read the camera box + visibility for a project (project_id) and optionally one of its slides (slide_id). Reports the RAW override at each level (project default, slide override) plus the RESOLVED value that actually applies, following the same precedence dfl-render uses (slide, then project, then the built-in default). Pass slide_id alone to also resolve the project it belongs to.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | no | UUID of the project. Required unless slide_id is given. |
slide_id | string | no | UUID of a slide, to also report its resolved/override camera. |
set_project_camera
Set the project-wide camera default: aspect (16:9 or 1:1), box (preset size+corner or custom fractions), and default visibility (off/overlay).
set_project_cameraSet Project Camera
Set the project-wide camera default: aspect (16:9 or 1:1), box (preset size+corner or custom fractions), and default visibility (off/overlay). Applies to every slide that does not override its own camera via set_slide_camera. Changing aspect with no box in the same call reshapes the current project box (and every slide's own camera_box override) to fit the new aspect, mirroring the Lesson Studio editor. Guarded on updated_at. focus {x, y} (0..1 of the camera frame) sets the project default crop point (settings.camera_focus); a slide focus (set_slide_camera) wins.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project. |
aspect | enum | no | Camera shape. Omit to leave it unchanged. One of: 16:9, 1:1. |
box | object | object | no | New project-default camera box. |
clear_box | boolean | no | Remove settings.camera_box, so the project falls back to the built-in default. |
default_visibility | enum | no | Camera visibility for a slide with no override (default: overlay). One of: off, overlay. |
clear_default_visibility | boolean | no | Remove settings.camera_default. |
focus | object | no | Project default camera crop point (settings.camera_focus). |
clear_focus | boolean | no | Remove settings.camera_focus (the centre). |
set_slide_camera
Override this slide's camera box and/or visibility, on top of the project default set_project_camera sets.
set_slide_cameraSet Slide Camera
Override this slide's camera box and/or visibility, on top of the project default set_project_camera sets. box resolves against the parent project's orientation and camera aspect. clear_box removes the box override (the slide follows the project box again); clear_visibility removes the visibility override. segments limits WHEN the corner (picture-in-picture) camera is drawn — refused on a fullscreen-webcam slide: a list of { start_ms, end_ms } windows in slide time (whole ms, sorted, non-overlapping, within the slide duration, 1-50 of them); the voice keeps playing outside them. clear_segments shows the camera for the whole slide again. To hide it on the whole slide use visibility: 'off'. focus {x, y} (0..1 of the camera frame) moves the crop of the camera off the centre, e.g. {x: 0.3, y: 0.5} for a face left of centre in a 16:9 take on a portrait slide; clear_focus goes back to the project focus / the centre. Preserves every other template_data key. Guarded on updated_at.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide. |
box | object | object | no | New camera box override for this slide. |
clear_box | boolean | no | Remove this slide's camera_box override. |
visibility | enum | no | Camera visibility override for this slide. One of: off, overlay. |
clear_visibility | boolean | no | Remove this slide's camera visibility override. |
segments | object[] | no | Windows (slide ms) in which the camera is drawn; stored as template_data.camera_segments. |
clear_segments | boolean | no | Remove this slide's camera_segments (camera on the whole slide). |
focus | object | no | The point of the camera frame the crop keeps (template_data.camera_focus). |
clear_focus | boolean | no | Remove this slide's camera_focus (the project focus, else the centre). |
Recordings & Captions
Section titled “Recordings & Captions”A recorded slide has ONE lesson_studio.slide_media row (the recording URL in storage_path, a trim window [start_trim_ms, end_trim_ms) on the SOURCE clock) and its captions in template_data.caption_cues ({text, startMs, endMs} on the same source clock). template_data.caption_cues_source names the recording the cues came from: the Lesson Studio editor re-transcribes a slide whose stamp is not its recording URL, so a tool that writes cues stamps them. caption_language records the cue language.
Attach a recording to slides, transcribe it, edit the captions, and make a project from one video.
set_slide_captions
Replace a slide's caption cues (template_data.caption_cues).
set_slide_captionsSet Slide Captions
Replace a slide's caption cues (template_data.caption_cues). Cues use the recording's SOURCE clock in ms (the file, not the slide), sorted and non-overlapping, max 2000. source_stamp 'current' (default) stamps caption_cues_source with the slide's recording URL, so the Studio editor does not re-transcribe and overwrite them; 'none' removes the stamp (the editor re-transcribes). Keeps every other template_data key. Guarded on updated_at.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide. |
cues | object[] | yes | Cues on the recording (SOURCE) clock, ms: sorted, non-overlapping, endMs > startMs, text non-empty. |
language | string | no | Language of the cues, BCP-47 (pt, en, pt-BR). Stored as caption_language. |
source_stamp | enum | no | 'current' (default): stamp with the slide recording URL. 'none': remove the stamp. One of: current, none. |
edit_caption_cue
Edit ONE cue of a slide's caption_cues by its 0-based index: new text and/or startMs/endMs (source clock, ms).
edit_caption_cueEdit Caption Cue
Edit ONE cue of a slide's caption_cues by its 0-based index: new text and/or startMs/endMs (source clock, ms). The whole list must stay sorted and non-overlapping. Keeps the caption_cues_source stamp and every other key. Guarded on updated_at.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide. |
index | number | yes | 0-based index of the cue in caption_cues. |
text | string | no | New text (non-empty) |
startMs | number | no | New start, source ms. |
endMs | number | no | New end, source ms. |
set_slide_layout
Apply a camera/content layout preset to one slide.
set_slide_layoutSet Slide Layout
Apply a camera/content layout preset to one slide. 'fullscreen-camera': the webcam fills the frame, no slide content. 'overlay-top': webcam fullscreen, slide content drawn OVER it in the top half (content_over_camera). 'overlay-full': webcam fullscreen, slide content drawn OVER it on the WHOLE canvas (no safe band, scale 1) — for a full-canvas image. 'split-top-image': slide content on top, full-width camera band at the bottom (portrait only). The band crop keeps the FACE: it finds the face in the slide's recording and writes camera_focus there (else it keeps the upper part of the frame); an existing camera_focus is kept. On an image-only slide (fullscreen-image, no free elements) it trims the image's flat margins and contains the drawn area in the top band (content_zoom/offset; the trim is recorded in image_trim); a slide with text keeps the plain band fit. 'background-pip': slide content fullscreen, small camera bottom-right. Resolves boxes against the project orientation and camera aspect. One write, guarded on updated_at; returns the written and removed keys. Keeps every other template_data key. caption_position (with or without a preset) sets this slide's caption placement, e.g. 'over-camera' on a split slide so the captions do not cover the image.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide. |
preset | enum | no | The layout preset. Optional when caption_position is set. One of: fullscreen-camera, overlay-top, overlay-full, split-top-image, background-pip. |
caption_position | enum | no | Where THIS slide's burned-in captions sit, over the project setting: top | middle | bottom (project semantics, the camera box is avoided), over-camera (bottom edge, NOT avoiding the camera: on a split slide the captions sit on the camera band, off the image), project (remove the override). One of: top, middle, bottom, over-camera, project. |
transcribe_media
Transcribe a video/audio (URL or media_id) with dfl-render, optionally translated.
transcribe_mediaTranscribe Media
Transcribe a video/audio (URL or media_id) with dfl-render, optionally translated. Returns the text and caption cues {text, startMs, endMs} on the source clock. Waits up to ~45 s; if still running it returns job_id — then call get_media_transcription. Writes nothing; attach_slide_recording transcribes on its own.
| Parameter | Type | Required | Description |
|---|---|---|---|
source | object | object | yes | The source: {url} or {media_id}. |
language | enum | no | Spoken language; default 'auto'. One of: auto, pt, en. |
translate_to | string | no | Also return the cues translated to this language. |
project_id | string | no | Use this project's caption glossary (set_project_glossary). |
glossary | string[] | no | Canonical terms (names, tickers) the captions must spell right. Default: the project glossary. |
fix_cues | boolean | no | Also run the LLM cue-fix pass (word order, misheard words, glossary). Default false. |
get_media_transcription
Read a transcription job that transcribe_media started: its status while it runs, else the text, cues and translation.
get_media_transcriptionGet Media Transcription
Read a transcription job that transcribe_media started: its status while it runs, else the text, cues and translation.
| Parameter | Type | Required | Description |
|---|---|---|---|
job_id | string | yes | The job_id transcribe_media returned. |
attach_slide_recording
Attach a raw recording (URL or media_id) to a slide: writes its slide_media row (one per slide), sets the slide duration to the trim window, starts the server transcode, and writes captions — transcribed by dfl-render…
attach_slide_recordingAttach Slide Recording
Attach a raw recording (URL or media_id) to a slide: writes its slide_media row (one per slide), sets the slide duration to the trim window, starts the server transcode, and writes captions — transcribed by dfl-render (optionally translated) or the cues you pass — stamped so the editor keeps them. Waits up to ~45 s; if the transcription still runs, it returns transcription_job_id: then call get_slide_recording. WARNING: an open Lesson Studio tab on this project can overwrite the row on autosave; close it first.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide. |
source | object | object | yes | The source: {url} or {media_id}. |
media_type | enum | no | Default 'webcam_video'. One of: webcam_video, screen_recording, audio. |
webcam_layout | enum | no | Also set the slide webcam_layout. One of: fullscreen, pip. |
trim | object | no | Window of the recording that plays on the slide, SOURCE ms [start, end). |
duration_ms | number | no | Source length, ms. Else estimated from the transcription. |
transcribe | boolean | no | Default true. Ignored when captions is given. |
language | enum | no | Spoken language; default 'auto'. One of: auto, pt, en. |
translate_to | string | no | Write the captions translated to this language. |
captions | object[] | no | Write these cues (source clock) and skip transcription. |
glossary | string[] | no | Canonical terms (names, tickers) the captions must spell right. Default: the project glossary. |
fix_cues | boolean | no | Also run the LLM cue-fix pass (word order, misheard words, glossary). Default false. |
get_slide_recording
Read a slide's recording: source URL, trim window, transcode_status (export needs 'done'; 'processing' still runs) and captions.
get_slide_recordingGet Slide Recording
Read a slide's recording: source URL, trim window, transcode_status (export needs 'done'; 'processing' still runs) and captions. With transcription_job_id (from attach_slide_recording) it finishes the attach once the job is done: fills an unknown length and writes the stamped captions, unless the slide already has captions for this recording.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide. |
transcription_job_id | string | no | The job attach_slide_recording returned. |
split_recording_across_slides
Spread ONE raw take over several slides.
split_recording_across_slidesSplit Recording Across Slides
Spread ONE raw take over several slides. mode window (default): every slide points at the same file with its own trim window [start_ms, end_ms), keep ranges become template_data.clips, and ONE transcription of the whole file gives each slide ONLY its own cues (a cue goes to the slide whose window, or keep range, holds its midpoint, clamped to it). mode cut: dfl-render cuts real files (/media/cut, optional fit) and each slide gets its part, with its own cues shifted onto it. create_slides appends new slides for parts without slide_id. Waits ~45 s; if a job still runs it returns job ids to resume with. An open Studio tab on the project can overwrite the rows on autosave.
| Parameter | Type | Required | Description |
|---|---|---|---|
source | object | object | yes | The source: {url} or {media_id}. |
parts | object[] | yes | One entry per slide, in slide order. |
create_slides | object | no | — |
mode | enum | no | Default 'window'. One of: window, cut. |
fit | enum | no | cut mode only. One of: none, portrait-crop, portrait-letterbox. |
media_type | enum | no | Default 'webcam_video'. One of: webcam_video, screen_recording, audio. |
duration_ms | number | no | Source length, ms, when known. |
transcribe | boolean | no | Default true. |
language | enum | no | One of: auto, pt, en. |
translate_to | string | no | Language of the cues, BCP-47 (pt, en, pt-BR). Stored as caption_language. |
transcription_job_id | string | no | Resume: the job a previous call returned. |
cut_job_id | string | no | Resume: the cut job a previous call returned. |
glossary | string[] | no | Canonical terms (names, tickers) the captions must spell right. Default: the project glossary. |
fix_cues | boolean | no | Also run the LLM cue-fix pass (word order, misheard words, glossary). Default false. |
import_media_from_url
Import a clip from a public video URL (e.g. a YouTube/X post) through dfl-render: download, optional [start_s, end_s) cut, optional portrait fit, transcription and translation.
import_media_from_urlImport Media From URL
Import a clip from a public video URL (e.g. a YouTube/X post) through dfl-render: download, optional [start_s, end_s) cut, optional portrait fit, transcription and translation. register_media makes it a public.media row; attach_to_slide_id attaches it to a slide with its (translated) cues, with no second transcription. as_insert (with attach_to_slide_id) makes it an INSERT slide: the clip on its own slide, contained, no camera treatment, optional insert_label. Waits ~45 s; else returns job_id — then call get_media_import. Use only media you may reuse.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | yes | Public page or file URL of the video. |
start_s | number | no | Clip start, seconds. |
end_s | number | no | Clip end, seconds. |
fit | enum | no | One of: none, portrait-crop, portrait-letterbox. |
transcribe | boolean | no | Default true. |
language | enum | no | One of: auto, pt, en. |
translate_to | string | no | Translate the cues to this language. |
attach_to_slide_id | string | no | Attach the clip to this slide. |
webcam_layout | enum | no | With attach_to_slide_id. One of: fullscreen, pip. |
as_insert | boolean | no | With attach_to_slide_id: make it an INSERT slide (its own slide, contained, no camera treatment). |
insert_label | string | no | With as_insert: a name label under the clip. |
register_media | boolean | no | Also create a public.media row (default false). |
get_media_import
Read an import_media_from_url job.
get_media_importGet Media Import
Read an import_media_from_url job. While it runs, its status. When done, it does the same finish: register_media and/or attach_to_slide_id (pass them again). Safe to call again.
| Parameter | Type | Required | Description |
|---|---|---|---|
job_id | string | yes | The job_id import_media_from_url returned. |
attach_to_slide_id | string | no | — |
webcam_layout | enum | no | One of: fullscreen, pip. |
as_insert | boolean | no | — |
insert_label | string | no | — |
register_media | boolean | no | — |
set_slide_insert
Make a slide an INSERT slide: an inserted video on its own slide (the video cuts from the camera to the clip, then back).
set_slide_insertSet Slide Insert
Make a slide an INSERT slide: an inserted video on its own slide (the video cuts from the camera to the clip, then back). The slide recording plays as a screen recording — contained, no camera box, no camera focus, no face crop — and the camera keys go. label (e.g. "@rohanpaul_ai") adds a name label box under the clip, drawn above the video in the export; label null removes it. The slide needs a recording (import_media_from_url with as_insert does all of this in one call). Captions stay as they are.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide with the clip. |
label | string | no | Name label text; null removes it; omit to keep it. |
create_project_from_video
Turn one raw take into a new Lesson Studio project in one call: creates the project (portrait by default, no project camera box) with burned-in caption settings, a fullscreen-camera slide per part (default: one slide…
create_project_from_videoCreate Project From Video
Turn one raw take into a new Lesson Studio project in one call: creates the project (portrait by default, no project camera box) with burned-in caption settings, a fullscreen-camera slide per part (default: one slide, the whole take), attaches the recording, transcribes (optionally translates) and writes stamped captions. parts has the split_recording_across_slides shape. Returns project, composition and slide ids; if a job still runs it returns the ids to finish with get_slide_recording or split_recording_across_slides.
| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | yes | Project title. |
source | object | object | yes | The source: {url} or {media_id}. |
orientation | enum | no | Default 'portrait'. One of: portrait, landscape. |
captions | object | no | — |
parts | object[] | no | Default: one slide, the whole take. |
mode | enum | no | With parts. Default 'window'. One of: window, cut. |
webcam_layout | enum | no | Default 'fullscreen'. One of: fullscreen, pip. |
duration_ms | number | no | Source length, ms, when known. |
language | enum | no | One of: auto, pt, en. |
translate_to | string | no | Language of the cues, BCP-47 (pt, en, pt-BR). Stored as caption_language. |
glossary | string[] | no | Caption glossary: stored on the new project and applied now. |
fix_cues | boolean | no | Also run the LLM cue-fix pass (word order, misheard words, glossary). Default false. |
set_project_glossary
Set a Lesson Studio project's caption glossary: the canonical spellings (names, tickers — 'Bessent', 'USDC') that the server transcription applies to the captions (a whole-word near match becomes the term; timings never…
set_project_glossarySet Project Caption Glossary
Set a Lesson Studio project's caption glossary: the canonical spellings (names, tickers — 'Bessent', 'USDC') that the server transcription applies to the captions (a whole-word near match becomes the term; timings never change). transcribe_media (with project_id), attach_slide_recording, split_recording_across_slides and create_project_from_video use it by default. Replaces the list; terms: [] clears it. Stored in projects.settings.caption_glossary; guarded on updated_at.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | The project. |
terms | string[] | yes | The full list (replaces the old one). |
upload_slide_image
Upload an image (base64 PNG, JPEG, WebP or GIF, at most 6 MB; the type is read from the bytes, SVG is refused) to DFL media as the caller, PUBLIC (the editor and the export load slide images by URL).
upload_slide_imageUpload Slide Image
Upload an image (base64 PNG, JPEG, WebP or GIF, at most 6 MB; the type is read from the bytes, SVG is refused) to DFL media as the caller, PUBLIC (the editor and the export load slide images by URL). Returns the url. With slide_id it also places the image as an image box on that slide: by default the WHOLE canvas (720x1280 portrait, 1280x720 landscape) with fit 'contain', above the other boxes — pair it with set_slide_layout 'overlay-full' for a full-canvas overlay over a fullscreen camera. box {x,y,width,height} in design pixels places it elsewhere. Use it in place of an upload outside the Studio.
| Parameter | Type | Required | Description |
|---|---|---|---|
file_content | string | yes | The image, base64. |
file_name | string | yes | A name for the file; the extension comes from the bytes. |
slide_id | string | no | Place the image on this slide. |
alt | string | no | Alt text for the box (default: the file name). |
fit | enum | no | Default 'contain'. One of: contain, cover, fill. |
box | object | no | Design pixels. Default: the whole canvas. |
Sound Effects
Section titled “Sound Effects”template_data.sfx is a Studio-owned key: a list of {id, sound, src, at_ms, volume?} entries, sound a key from the catalog below, at_ms the played ms offset from the slide start (same clock as animation beats) a sound must START before, volume 0..2 (omitted when 1). create_slide and update_slide accept the short-hand template_data.sfx: [{sound, at_ms, volume?}] directly — id/src are filled in from the catalog.
Add sound effects to a slide.
list_sound_effects
The sound-effect catalog available to template_data.sfx / set_slide_sfx: every valid sound key, its category, use-case hint and duration.
list_sound_effectsList Sound Effects
The sound-effect catalog available to template_data.sfx / set_slide_sfx: every valid sound key, its category, use-case hint and duration. Read-only, no database access. Pass category to filter.
| Parameter | Type | Required | Description |
|---|---|---|---|
category | string | no | Restrict to one category id (see the returned categories list). |
set_slide_sfx
Write this slide's sound effects (template_data.sfx).
set_slide_sfxSet Slide Sound Effects
Write this slide's sound effects (template_data.sfx). Each entry is {sound, at_ms, volume?} — sound is a catalog key from list_sound_effects, at_ms must start before the slide duration_ms. mode "replace" (default) discards the stored list; "append" adds to it. An empty array with "replace" removes the key. Preserves every other template_data key. Guarded on updated_at.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide. |
sfx | object[] | yes | The sound effects to write (or add, in append mode). |
mode | enum | no | Default "replace". One of: replace, append. |
Cover Slide
Section titled “Cover Slide”A deck may mark ONE slide as its cover (template_data.is_cover === true, strictly). The cover is captured on its own for the deck’s video thumbnail (capture_cover_slide, see Render below), but dfl-render excludes it from the rendered video entirely: not a frame, not the slide count, not the 01/09 deck index. See dfl-services:services/dfl-render/src/lib/cover-slide.ts for the canonical contract this mirrors.
Pick the cover slide of the deck.
set_cover_slide
Mark a slide as the deck's cover (template_data.is_cover), or unmark it (is_cover: false).
set_cover_slideSet Cover Slide
Mark a slide as the deck's cover (template_data.is_cover), or unmark it (is_cover: false). The cover is used for the video thumbnail via capture_cover_slide, but dfl-render EXCLUDES it from the rendered video, its slide count, and the 01/09 deck index. Setting is_cover: true clears the flag from every other slide in the same composition first — a deck has at most one cover. Preserves every other template_data key. To set the composition's thumbnail after capturing it, chain the result into whichever tool writes projects.thumbnail_url (this tool never writes it).
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide to mark or unmark as the cover. |
is_cover | boolean | no | true (the default) to make this slide the cover; false to unmark it. |
Project Versions
Section titled “Project Versions”Save and list versions of a project.
list_project_versions
List the versions of a Lesson Studio project, ordered by version_number descending (current/highest first).
list_project_versionsList Project Versions
List the versions of a Lesson Studio project, ordered by version_number descending (current/highest first).
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project. |
bump_project_version
Create a new version of a Lesson Studio project (version_number = max existing + 1, starting at 1).
bump_project_versionBump Project Version
Create a new version of a Lesson Studio project (version_number = max existing + 1, starting at 1). Subsequent comments are stamped to this new version. Returns the created version row.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project to bump. |
label | string | no | Optional human label for this version (e.g. "after canvas review pass 1") |
Generation
Section titled “Generation”Write a new lesson in the style of a specific tutor.
generate_lesson_from_profile
Author a NEW lesson topic in a specific tutor's proven pedagogical style.
generate_lesson_from_profileGenerate Lesson From Tutor Profile
Author a NEW lesson topic in a specific tutor's proven pedagogical style. Loads that tutor's stored pedagogy profile (lms.tutor_profiles) and returns a Studio-importable GENERATION SPEC: a typology-biased slide plan (template picks ranked by the professor's dominant lesson-type via search_templates' ranker), the professor's SIGNATURE as few-shot exemplars (verbatim metaphors/openers/cotidiano anchors), and amplify/reduce directives (amplify high-consistency moves, reduce vícios). Deterministic — the tool does NOT call an LLM; the calling agent expands each suggested slide into content, few-shotting the exemplars and honoring the directives. The rubric (HALF A) is never used for generation (de-circularization).
| Parameter | Type | Required | Description |
|---|---|---|---|
tutor_id | string | yes | work.members.id of the tutor whose stored pedagogy profile to author in (matches lms.tutor_profiles.member_id). |
topic | string | yes | The new lesson topic to author in that tutor's style, e.g. "Introdução a APIs REST". |
version | string | no | Profile version to load. Defaults to the newest (most recently updated) profile for the tutor. |
n_slides | number | no | Target slide count / number of template picks (default 8). |
canvas | string | no | Only pick templates that declare this design canvas (e.g. "social-portrait"). |
project_id | string | no | Pick only templates that fit this project's canvas. Ignored when canvas is given. |
Render
Section titled “Render”Turn stored slides into PNGs through the deterministic headless capture in
dfl-render. Both tools run with the calling user’s own credentials, and both
answer with the reproduction key rather than only a URL: the dfl-slide-templates
commit revision the template was read at, plus a composition fingerprint of the
deck state the derived slide index came from. Two renders are comparable only when
both match — the slide index is derived, so inserting a sibling changes an
already-rendered slide’s pixels on purpose.
canvas is optional. A canvas the template does not declare is refused and no
image is produced; there is deliberately no fallback to the nearest canvas.
Turn slides into PNG images, including the cover image.
render_slide_image
Render one stored slide to a PNG and return its URL.
render_slide_imageRender Slide Image
Render one stored slide to a PNG and return its URL. Wraps the deterministic headless capture in dfl-render with the CALLING user's own credentials. Returns the dfl-slide-templates commit revision the template was read at AND a composition fingerprint of the deck state the derived slide index came from — together those two reproduce the render. canvas is optional; a canvas the template does not declare is REFUSED and no image is produced (never a fallback to the nearest canvas).
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide to render. |
canvas | string | no | Design canvas id, e.g. "landscape", "portrait", "social-portrait". Omit to use the parent project's orientation. Validated against the declared canvas contract AND against the canvases the slide's template ships; an undeclared canvas errors and produces no image. |
render_composition_images
Render every surviving slide of a composition to a PNG and return the URLs in order_index order — the batch form of render_slide_image, and the tool a carousel needs.
render_composition_imagesRender Composition Images
Render every surviving slide of a composition to a PNG and return the URLs in order_index order — the batch form of render_slide_image, and the tool a carousel needs. Soft-deleted slides are skipped. Captures run with bounded concurrency. Returns the dfl-slide-templates commit revision and ONE deck-level composition fingerprint shared by every image. canvas is optional; a canvas any template in the deck does not declare is REFUSED before anything is captured.
| Parameter | Type | Required | Description |
|---|---|---|---|
composition_id | string | yes | UUID of the composition to render. |
canvas | string | no | Design canvas id, e.g. "landscape", "portrait", "social-portrait". Omit to use each slide's parent project orientation. Every distinct template in the deck must declare it, or the whole call errors and no image is produced. |
capture_cover_slide
Render the composition's cover slide (template_data.is_cover, set by set_cover_slide) to a PNG for the deck's video thumbnail, and return its image_url.
capture_cover_slideCapture Cover Slide
Render the composition's cover slide (template_data.is_cover, set by set_cover_slide) to a PNG for the deck's video thumbnail, and return its image_url. Errors clearly when the composition has no cover slide. This does NOT write projects.thumbnail_url — no tool in dfl-mcp-studio does yet — so persist the returned image_url yourself once such a tool exists.
| Parameter | Type | Required | Description |
|---|---|---|---|
composition_id | string | yes | UUID of the composition whose cover slide to capture. |
Thumbnail
Section titled “Thumbnail”Render one YouTube thumbnail (1280x720, youtube_gold layout) through the Thumbify
renderer on dfl-services. The tool stores nothing: to change a thumbnail, call it
again with changed inputs. Every image input must be a public URL.
Make a YouTube thumbnail image.
generate_thumbnail
Render a gold-standard DevFellowship YouTube thumbnail (1280x720) via the youtube_gold layout.
generate_thumbnailGenerate YouTube Thumbnail
Render a gold-standard DevFellowship YouTube thumbnail (1280x720) via the youtube_gold layout. Layers, back to front: background_image -> behind_object (behind the character) -> character (alpha-cut photo on the right) -> tech logos (top-left) -> condensed bold title with green highlight (bottom-left). character_url MUST be a background-removed (alpha) photo; all image inputs must be public URLs the renderer can fetch (upload local files first). Returns the rendered PNG URL.
| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | yes | Headline / SEO keyword. Rendered UPPERCASE, condensed bold, auto-fit. |
character_url | string | yes | Character photo URL with background ALREADY removed (alpha PNG). Placed large on the right. |
background_image | string | no | Full-bleed background image URL. Omit for the brand teal gradient. |
behind_object | string | no | No-bg object/mockup PNG URL, layered ABOVE the background but BEHIND the character. |
behind_object_box | object | no | Placement box for behind_object in 1280x720 reference px. Omit for full-canvas centered contain. |
screenshot_url | string | no | Optional legacy center screenshot layer (above background, below character). |
logos | enum[] | no | Tech-stack logo keys (top-left tiles). Allowed: react, tailwind, vite, typescript, javascript, nextjs, node, python, supabase, postgresql, docker, go. Inferred from title when omitted. |
highlight | string[] | no | Words to paint green in the title. Heuristic (longest content words) when omitted. |
character_brightness | number | no | Brightness multiplier for the character/person layer (CSS brightness()). Default 1.1. Range ~0.5-2.0; >1 brightens the person to pop against dark backgrounds. |
character_contrast | number | no | Contrast multiplier for the character/person layer (CSS contrast()). Default 1.15. Range ~0.5-2.0. |
width | number | no | Output width. Default 1280. |
height | number | no | Output height. Default 720. |
Templates
Section titled “Templates”Find, read and change slide templates.
list_templates
List all slide templates available in dfl-slide-templates (reads registry.json from GitHub).
list_templatesList Templates
List all slide templates available in dfl-slide-templates (reads registry.json from GitHub).
Takes no parameters.
search_templates
Find the best slide templates for an intent.
search_templatesSearch Templates
Find the best slide templates for an intent. Returns a RANKED shortlist (top-N) instead of the full list — give a natural-language intent (e.g. "compare two options head-to-head", "mostrar uma captura de tela", "one big hero number") and get back the templates whose registry "when_to_use"/tags/media_profile best match, each with a relevance score and a one-line reason. Use this to PICK a template, then call get_template(id) for its slots. Deterministic + read-only (no auth needed).
| Parameter | Type | Required | Description |
|---|---|---|---|
intent | string | yes | Natural-language description of what the slide should do (English or Portuguese), e.g. "comparar duas opções", "show a screenshot", "single big statistic". |
limit | number | no | Max templates to return in the shortlist (default 5). |
canvas | string | no | Only templates that declare this design canvas (e.g. "social-portrait"). Omit for no canvas filter. |
project_id | string | no | Filter by this project's canvas (its orientation). Ignored when canvas is given. |
get_template
Fetch all source files (HTML landscape, HTML portrait, CSS landscape, CSS portrait, config YAML) for a specific template.
get_templateGet Template
Fetch all source files (HTML landscape, HTML portrait, CSS landscape, CSS portrait, config YAML) for a specific template.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | Template ID (e.g. "title", "bullet-list", "two-column") |
update_template
Open a ready-to-merge PR in dfl-slide-templates with updated HTML/CSS and optional config YAML for a template.
update_templateUpdate Template
Open a ready-to-merge PR in dfl-slide-templates with updated HTML/CSS and optional config YAML for a template. All provided fields are written in a single commit. Returns the PR URL. registry.json is edited as a MERGE: any registry_* field you omit keeps its current value, so a version bump can no longer erase the discoverability metadata search_templates ranks on. Pass null to delete a field. CREATING a template (an id not yet in registry.json) requires registry_version, registry_name, registry_when_to_use, registry_media_profile and registry_tags, so the new template is findable from the moment it merges.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | Template ID to update (e.g. "title") |
description | string | yes | Human-readable description of the change (used as PR/commit title suffix) |
html_landscape | string | yes | New landscape.html content. |
css_landscape | string | yes | New landscape.css content. |
html_portrait | string | yes | New portrait.html content. |
css_portrait | string | yes | New portrait.css content. |
config_yaml | string | no | New config.yaml content (optional — skip to leave unchanged) |
registry_version | string | no | Bump the registry version (e.g. "1.1.0") — omit to leave unchanged. REQUIRED when creating a new template. |
registry_category | string | no | Update the registry category (content | layout | data | …) — omit to leave unchanged. Defaults to "content" on create only. |
registry_name | string | no | Human display name, e.g. "Title Slide". search_templates ranks on it. Omit to leave unchanged; null to delete. REQUIRED when creating. |
registry_when_to_use | string | no | When an authoring agent SHOULD reach for this template — the highest-signal ranking field. Omit to leave unchanged; null to delete. REQUIRED when creating. |
registry_avoid_when | string | no | Anti-patterns: when NOT to use this template. Omit to leave unchanged; null to delete. |
registry_media_profile | enum | no | What kind of content this template is shaped for. Omit to leave unchanged; null to delete. REQUIRED when creating. One of: text-heavy, balanced, image-first, image-only, video, data, code, diagram. |
registry_text_density | enum | no | How much copy the layout carries. Omit to leave unchanged; null to delete. One of: none, low, medium, high. |
registry_layout | string | no | One-line shape description, e.g. "centered big headline + subtitle, no media". Omit to leave unchanged; null to delete. |
registry_tags | string[] | no | Free keywords for matching — the heaviest-weighted ranking field. Replaces the whole list (never merged element-wise). Omit to leave unchanged; null to delete. REQUIRED when creating. |
Themes
Section titled “Themes”Read and change the CSS themes of slides.
list_themes
List the CSS themes that actually exist in dfl-slide-templates at the pinned revision.
list_themesList Themes
List the CSS themes that actually exist in dfl-slide-templates at the pinned revision. The set is DERIVED from the repo (registry.json themes[]), so a theme added upstream appears here with no code change. Each entry reports whether its stylesheet resolved, plus its display name and light/dark mode when declared. The response names the commit it was read from and flags a degraded read.
Takes no parameters.
get_theme
Fetch the CSS source for a specific theme.
get_themeGet Theme
Fetch the CSS source for a specific theme.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | Theme ID as reported by list_themes (e.g. "default", "devfellowship", "itera", "revera") |
update_theme
Open a ready-to-merge PR in dfl-slide-templates with updated CSS for a theme.
update_themeUpdate Theme
Open a ready-to-merge PR in dfl-slide-templates with updated CSS for a theme. Returns the PR URL. The PR also REGISTERS the theme in registry.json's themes[] — the array themes_doc calls the source of truth and list_themes reads — so a theme published here is discoverable instead of being a stylesheet nothing lists. Registering a NEW theme requires theme_name and theme_mode: neither a brand's display name nor its light/dark mode can be inferred, and a guess would become the source of truth. A new theme also needs its forbidden-colour contract in scripts/theme.config.json, or lint:css fails — that file is human-gated, so a brand-new theme cannot merge unattended.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | Theme ID to update, as reported by list_themes (e.g. "devfellowship", "itera") |
description | string | yes | Human-readable description of the change (used as PR/commit title suffix) |
css | string | yes | New CSS content for the theme file. |
theme_name | string | no | Display name for the registry themes[] entry, e.g. "Itera". Omit to leave an existing theme unchanged. REQUIRED when registering a new theme. |
theme_mode | enum | no | Advisory light/dark hint for the host chrome around the slide (the slide always paints its own surface). Omit to leave an existing theme unchanged. REQUIRED when registering a new theme. One of: light, dark. |