---
name: fablyn
description: Write, preview and render narrated explainer videos with Fablyn. Use when someone asks for a product video, a feature launch, an explainer or a narrated walkthrough, or asks which Fablyn account is connected, and Fablyn is connected. Videos are compositions, JSON documents in Remotion's names, validated and rendered by Fablyn.
---

# Fablyn

Fablyn turns a **composition** into a finished, narrated MP4. A composition is a JSON document that uses Remotion's names: `Sequence`, `Series`, `AbsoluteFill`, `interpolate`, `spring`. It is data, never code. Fablyn validates it, speaks each narration line, times the video to the speech, captures every frame in Chromium, and encodes the cut. You write the document; Fablyn does the rest.

This file is generated from the same registry the connector's `get_creation_options` tool returns (registry 1.3.2). When the two disagree, the tool is current.

## Connect

Add Fablyn as a connector by URL: `https://fablyn.app/mcp`. Your client opens a Fablyn sign-in page; the workspace owner chooses what you may do. The scopes, in plain words, are **Write compositions**, **Generate narration**, **Render** and **Approve**. Approve is never granted when you connect; the owner adds it later if they want you to approve finished videos.

Call `whoami` with no arguments when asked who is connected or which workspace the agent uses. Report its `account` email and user ID, `workspace` name and ID, `connectedAgent` name and `grant` permissions. These describe the MCP connection; the account open in Studio may differ. It works with a read-only Grant and uses no narration or Video minutes. `get_creation_options` returns the same information under `connection`; check that destination before creating a Video.

`whoami` also reports the workspace's Video minutes under `minutes`: `funding` is what the next render uses (`paid`, `welcome`, `none`, or `unmetered` while minutes are not checked, with `enforced` false), `remainingMs` is what is left of it, and `paidRemainingMs` and `welcomeRemainingMs` are each balance. A render is paid when the paid minutes cover it with at least a minute to spare; otherwise welcome minutes must cover its whole estimated length. Welcome minutes are used by every finished render, approved or not; paid minutes by each approved Video.

## The workflow

1. `get_creation_options`: the index. Nodes, the CSS property count (`get_schema` with part `style` lists the properties and their formats), easings, transitions, fonts, this workspace's assets, Projects, videos (under `videos`) and pins, the component library by name with each component's props, the names of the fourteen templates and of the schema parts. Read it first.
2. `get_example` with a name (`product-launch`, `hold-across-lines`, `loop`, `slides`, `steps-coach`, `numbers-real-estate`, `question-clinic`, `announcement-organiser`, `quote-podcaster`, `recap-nonprofit`, `list-food`, `myth-finance`, `beforeafter-trade` or `countdown-conference`): a template, a complete composition to start from. Templates and library components are known to render well.
3. `get_component` with a name before you use a library component: its full definition, to copy into your composition's `components`.
4. Write the composition. When a diagnostic needs the exact shape of something, `get_schema` with the part that holds it (`style`, `animation`, `node:Sequence` and so on); without a part it lists them all.
5. `validate_video`: every error with its path, and the timeline at the estimated speaking rate. Fix errors until `valid` is true.
6. `create_video` (a new video, in a Project by `campaignId` or a new one by `campaignName`) or `update_video` (the next revision; pass the revision you read as `expectedRevision`). A video is named by its `campaignId` and `cutId`, as `create_video` returns them. For a small change to a revision you have read, send `patch_video` instead: the same `expectedRevision`, `pins` and `note`, and `ops`, a list of edits `{ "op", "path", "value" }` with `op` one of `replace`, `add`, `remove` and `test` (JSON Patch, RFC 6902) and `path` a JSON Pointer into the composition (RFC 6901), such as `/children/2/children/0/props/x`. Positions in a list are numbers, `add` at `/-` at the end of a list appends, and `test` checks a value before the edits after it. The edits apply in order to the revision you read, at most 200 and 64 KB. If one fails, or the result has errors, nothing is saved and the answer names the operation and its path; the save is otherwise `update_video`'s, with the same diagnostics. Send `update_video` for a rewrite. To have `create_video` or `update_video` drop two fields that validation refuses and that change nothing, pass `normalize: true` (default false): it removes `name` from any node that has no `name` field and `from` from a Sequence directly inside a Series, before validating, and the answer lists what it removed under `normalized` as `[{ path, removed }]`, an empty list when nothing was. Without it the document is validated exactly as sent, and those two fields are refused as `field_unknown` and `series_from`.

   **A large composition, as a file.** If you build the composition in a file (a program writing JSON, say), or it is over about 200 KB, send the file instead of reproducing it as tool-call text; the cap below is the same on both paths: call `register_asset` with `kind: "composition"`, `contentType: "application/json"`, no `id`, and the file's `size` and `sha256`. It returns `upload: { id, uploadUrl, method: "PUT", headers, maxBytes, expiresAt }`. PUT the file's exact bytes to `uploadUrl` with those headers (for example `curl -X PUT -H 'content-type: application/json' --data-binary @video.json "$uploadUrl"`); a file that is not the size or digest you declared, is not JSON, or is over 1048576 bytes is refused and nothing is kept. Then call `create_video` or `update_video` with `compositionAssetId` set to that `id`, in place of `composition`; send one or the other, never both. The save runs exactly as it does inline (validation, diagnostics, `expectedRevision`, `withStills`, `pins`, `note`), and the answer adds `receivedComposition: { compositionAssetId, sha256, bytes }` for what Fablyn received. An upload lasts 30 minutes from the moment its URL is issued and one successful save uses it; a refused save leaves it usable until then. It is not an asset: it never appears in the Library or counts as stored files, but each PUT counts as a file added this hour. To compare a saved revision with your file, `get_video` with `compositionUrl: true` returns `compositionDownload`: a link valid for fifteen minutes to the composition as the compact JSON Fablyn keeps (`JSON.stringify` of what you sent, without indentation), with its `sha256` and `bytes`.
7. `get_video_stills`: one still at the middle of each narration line and each silent scene, with warnings: `text_overflow`, `off_canvas`, `overlap`, `low_contrast`, `min_text_size`. Each still also follows the answer as a small picture (image content): show the person the scenes rather than describing them, and never illustrate them with other images. Look at them. Warnings never block a render, but they are usually right. To check particular moments, such as the middle of a long scene, pass `frames` with up to 12 frame numbers (`[0, 120, 630]`): you get a still with its warnings at each, with `kind: "frame"`. Sixty requests an hour per workspace, with or without `frames`. To save and check in one call, pass `withStills: true` to `create_video`, `update_video` or `patch_video`: a successful save then returns the saved revision's stills under `stills`, exactly as `get_video_stills` would, and counts as one stills request. If the stills cannot be made, the save still stands and `stillsError` says why; call `get_video_stills` to try again. Warnings never make the stills fail for their length: when a full answer would be too long, each still carries its first 10 warnings and `warningCounts` (a count per code, for all of them), and the answer says `warningsTrimmed: true` with a `note`; if even that is too long the warnings are left out and only `warningCounts` remain, and you ask for fewer `frames`. Pass `warnings: "summary"` to `get_video_stills` to ask for that shape (counts, and at most 10 warnings per still) at any size; the default is `"full"`.
8. `get_video_quote`: show the person the maximum AI credits for new narration and estimated Video minutes. Unchanged narration is reused for zero AI credits. A video with narration needs a quote before `render_video`, and its quote lasts fifteen minutes. A video with nothing to narrate costs nothing: its quote does not expire while the video is unchanged, and `render_video` may be called without a `quoteId` for it. Its `minutes` are `whoami`'s with this revision's `estimatedMs` and `fits`: whether `render_video` would find enough minutes for it now. When `fits` is false, shorten the video or ask the person to add minutes before rendering. When enabled, pass the returned `requestId` as `quoteId` to `render_video`: narration, capture and encode. Rendering uses rendered minutes; approval later uses approved minutes. A Connected agent cannot confirm spending above the owner's limit: give the person the returned Studio link. If AI credits are disabled, render without a quote. Minute exhaustion returns `credits_exhausted`; AI exhaustion returns `ai_credits_exhausted`. One render runs at a time in a workspace: while one is running, `render_video` answers `job_failed` with the message "Another render is still running in this workspace"; nothing is wrong with the Video, so wait for the running render with `get_render_status` and try again. Poll `get_render_status` until `done`; its AI credit receipt also appears in `get_render_result`.
9. `get_render_result`: links to the video and line stills (valid for fifteen minutes), captions, transcript and the receipt.

   **A cover image.** To get one frame of the video as a full-size PNG (the composition's width by height, 1080 by 1920 for a vertical Short), pass `coverFrame` to `render_video`: a whole number from 0 to the last frame of the timeline `validate_video` reports. The video itself is unchanged. `get_render_result` then has `cover: { frame, width, height, url }`, the link valid for fifteen minutes like the others. A frame outside the video is refused as `invalid_cover_frame` with the frames it may be, and nothing starts. For a narrated video the rendered timeline follows the measured narration; if that makes the video end before the frame, `cover` is `null` and `coverMessage` says which frames to use next time.
10. A person approves the video in Fablyn or leaves a note on it asking for a change; you approve with `approve_render` only if the owner allowed it.

A person approves the slides in Fablyn, or you do with `approve_slides` if the owner allowed it; `get_slides` returns the PDF and PNGs with the receipt. Slides are a video's stills approved as a file: one PDF page per still at the canvas size, plus each still as a PNG. They need no render and use no minutes; make the stills first with `get_video_stills`.

A video with no narration is fine: leave `narrator` and `language` out when no Sequence carries narration, and it renders without narration. Original sound from Audio or unmuted Video nodes can still play. Both are required as soon as one Sequence is narrated (`narrator_required_for_narration`). The `slides` example is that shape: three silent scenes in a Series.

The earlier names still work and do exactly the same: `validate_cut`, `create_cut`, `update_cut`, `get_cut`, `get_cut_stills` and `render_cut` (`get_video` reads a video's composition at a revision).

Each tool result is kept small enough for an agent to read whole, which is why the schema, the templates and the component definitions come one at a time. Agents that fetch URLs can also read them whole: `https://fablyn.app/connector/composition.schema.json` and `https://fablyn.app/connector/examples/<name>.json`.

**Notes from the person.** A person can leave a note on a video's revision, for you. A note on the latest revision is open: it is a request for a change. Read a video's notes before you save: `get_video` returns every note on the video, newest first, each with its `revision`, `text`, who wrote it and whether it is `open`, and `get_creation_options` counts each video's `openNotes`. Answer by saving a new revision with `update_video` and a `note` (1 to 500 characters) saying what you changed; that closes the open notes. You never delete or edit a note.

**Projects** group videos on the person's wall; every video is in exactly one. `create_video` puts a new video in a Project by `campaignId`, or starts one by `campaignName`; `move_video` moves a video to another Project: `to: { campaignId }` or `to: { campaignName }` (an existing name, else a new Project). A moved video keeps its id and everything it has, and the `campaignId` you held still finds it; read the new one from the answer. The person moves videos by dragging them on the wall.

**Pins** are the person's tags on videos, shown as coloured dots on their wall. `create_video` and `update_video` take an optional `pins` list of names: reuse the names `get_creation_options` lists under `pins` (each with its colour and how many videos carry it), and a name that does not exist yet becomes a pin with the next colour. The Project named in `create_video` becomes a pin too. Each name is 1 to 40 characters and a video carries at most 20; a bad name is refused with `invalid_pins` and a path, and nothing is saved. You add pins; you never rename, recolour, remove or delete one.


**Photos from the person.** Use `list_images` to find a named image already in the workspace. If more than one matches, ask which one. Otherwise call `request_images` with a unique `requestKey`, one to six recognisable image names and brief instructions. Include `campaignId` and `videoId` for an existing Video; omit both for a new one. Reuse the same key only when retrying the same request.

If your client can transfer the actual file bytes, call `register_asset` using the reserved image id. Otherwise give the person the returned Fablyn link, where they sign in to the connected account, choose their photos and add them. Wait for them to return and say done. Then call `get_image_request` with the returned request id. Use its received image ids in `Img.src`; if photos are missing, name what is still needed. Validate, save a new revision and check the Stills. Uploading alone does not place an image or render a Video. Links expire after 24 hours; request a new link with a new key if needed. Keep base64 and internal ids out of the person's instructions.

Images, audio and video files are workspace assets referenced by id, never external URLs. `register_asset` accepts PNG, JPEG, WebP, WAV, MP3 and MP4; up to 2 MB can be sent as `bytesBase64`, and larger files use its PUT URL (up to 25 MB). Pass the file's `size` and `sha256` either way: base64 that loses characters on the way can still decode, to a shorter file, and with them Fablyn refuses a file that arrives different, stores nothing and leaves the id free for the right file. Prefer the PUT URL whenever your client can send a file, since an id, once registered, always names the file it received. Publish reusable components to the workspace Library with `publish_component`, passing the `videoId` it came from.

**Video files from the person.** Read workspace assets from `get_creation_options`; video records include measured `width`, `height`, `durationMs` and `hasAudio`. If your client can send file bytes, register with `kind: "video"` and `contentType: "video/mp4"`, inline or by PUT as above. Otherwise ask the person to choose **Add video** in the Video's Design tab, or add the file in **Library → Assets**, then read the updated composition and assets. Browser control is optional; an MCP tool cannot read a local file that its client has not supplied.

Use an H.264 MP4 with optional AAC sound, up to 120 seconds, 25 MB and 4096 source pixels on either side. Read `get_schema("node:Video")` and use its workspace id in `Video.src`. Set `trimBefore` and `trimAfter` in composition-fps frames (end exclusive), `muted: true` for no original sound, `volume` from zero to one, and `loop: true` to repeat the trimmed range. Source time starts at the enclosing Sequence's local frame zero. Without looping, the last picture holds after the range ends and source sound stops; Freeze holds the picture and suppresses source sound. Put the video behind text in an AbsoluteFill, using `style.objectFit` `contain` or `cover`. Validate, save and check Stills before rendering. Uploading and editing uses no AI credits; rendering uses available Video minutes.

**Fading sound.** `Audio` and `Video` take `fadeInFrames` and `fadeOutFrames`, whole frames from zero. The sound rises linearly from silence to its `volume` over the first `fadeInFrames` frames it plays and falls back to silence over its last `fadeOutFrames`, counted from where its enclosing Sequence (or each Loop repetition) starts and ends, after trims, or where the file runs out first. On a Video only the sound fades. The two together must fit the frames the sound plays (`audio_fade_too_long`). One node replaces a chain of short clips with stepped volumes. A music bed under a 20-second video at 30 fps, fading in over half a second and out over its last two seconds:

```json
{ "type": "Sequence", "durationInFrames": 600, "children": [{ "type": "Audio", "src": "music_bed", "volume": 0.3, "loop": true, "fadeInFrames": 15, "fadeOutFrames": 60 }] }
```

## The document

```json
{ "documentType": "composition", "width": 1080, "height": 1920, "fps": 24, "narrator": "Kore", "language": "en", "components": { }, "children": [ ] }
```

- Canvas: 64 to 3840 pixels a side; 1 to 60 frames a second; at most 120 seconds.
- `narrator` is a voice name such as Kore. `language` is the narration language, for example `en`.
- `pronunciations`: `[{ "from": "SQL", "to": "sequel" }]` replaces whole words in what is spoken, never in captions.
- Every node has a `type`. Children go in `children`. Text goes in `text`. Styles are CSS in camelCase in `style`, numbers in pixels.

### Nodes

| Node | Kind | What it does |
|---|---|---|
| `Sequence` | timing | Shows its children from a start frame for a number of frames, with frames local to it. A narrated Sequence takes both from its narration line. |
| `Series` | timing | Plays its Sequences one after another. Each needs durationInFrames or narration. |
| `Loop` | timing | Repeats its children every durationInFrames, restarting their local frames each time, for times repetitions or as long as its parent lasts. |
| `Freeze` | timing | Shows its children held at one local frame. |
| `TransitionSeries` | timing | Scenes played in order, with a transition between neighbours that overlaps them. |
| `TransitionSeries.Sequence` | timing | One scene in a TransitionSeries. |
| `TransitionSeries.Transition` | timing | The transition between the scenes either side of it. |
| `AbsoluteFill` | content | A div that fills its parent: absolute position, inset 0, a flex column. |
| `div` | content | A block element. |
| `span` | content | An inline element. |
| `p` | content | A paragraph. |
| `h1` | content | A first-level heading. |
| `h2` | content | A second-level heading. |
| `h3` | content | A third-level heading. |
| `Img` | media | An image asset. The export waits for it to load. |
| `Video` | media | A workspace MP4 on the enclosing Sequence clock. Source trims are in composition frames; holds the final frame after the source ends, or loops the trimmed range. Original sound is mixed at export unless muted. fadeInFrames and fadeOutFrames fade that sound only, linearly from silence to volume over the first frames it plays and back to silence over its last. |
| `Audio` | media | Background audio mixed under the narration at export. Shows nothing. fadeInFrames ramps it linearly from silence to volume over the first frames it plays; fadeOutFrames ramps it back to silence over the last frames it plays, where its Sequence ends or its sound runs out. |
| `svg` | svg | An SVG canvas. |
| `g` | svg | A group of SVG shapes. |
| `rect` | svg | A rectangle. |
| `circle` | svg | A circle. |
| `ellipse` | svg | An ellipse. |
| `line` | svg | A straight line. |
| `polyline` | svg | Connected straight segments. |
| `polygon` | svg | A closed shape of straight segments. |
| `path` | svg | A path from SVG path data. |
| `text` | svg | SVG text. Its words go in the text field. |

### Narration and timing

- A narrated `Sequence` has an `id` and `narration: { "text": "...", "pauseAfterFrames": 12 }`. Its start and length come from the speech; never give it `from` or `durationInFrames`.
- Inside a `Series`, children play one after another. Silent scenes give `durationInFrames`.
- Frames inside a Sequence count from its own start. Everything is in frames, never seconds.
- **Holding across lines.** A Sequence that is not narrated can be anchored to lines: `"from": { "line": "launch" }` and `"durationInFrames": { "untilLine": "result" }` (or `{ "untilEnd": true }`). It then follows the speech however long it takes. Interpolation inputs can be anchors too: `"input": [{ "line": "result" }, { "line": "result", "offset": 12 }]`.
- **Loop** repeats its children every `durationInFrames`; `times` limits the repeats.
- At most 40 narration lines, 500 characters each.

### Animated values

Any numeric style value, and colours, can be animated instead of fixed:

- `{ "interpolate": { "input": [0, 15], "output": [0, 1], "easing": "easeOut", "extrapolateRight": "clamp" } }`. Extrapolation defaults to extend, as in Remotion.
- `{ "interpolateColors": { "input": [0, 30], "output": ["#9AA7B8", "#3DDC84"] } }`
- `{ "spring": { "from": 40, "to": 0, "delay": 5, "config": { "damping": 12 } } }`. The defaults are damping 10, mass 1, stiffness 100.
- `transform` is a list: `[{ "translateY": { "spring": { "from": 40, "to": 0 } } }, { "scale": 1.1 }]`.

Easings: `linear`, `ease`, `easeIn`, `easeOut`, `easeInOut`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeInSin`, `easeOutSin`, `easeInOutSin`, `easeInCircle`, `easeOutCircle`, `easeInOutCircle`, `easeInExp`, `easeOutExp`, `easeInOutExp`, `easeInBack`, `easeOutBack`, `easeInOutBack`, `easeInBounce`, `easeOutBounce`, `easeInOutBounce`, `easeInElastic`, `easeOutElastic`, `easeInOutElastic`, or a cubic bezier `[x1, y1, x2, y2]`.

Transitions in a `TransitionSeries`: `fade`, `slide` (from-left, from-right, from-top, from-bottom), `wipe` (from-left, from-top-left, from-top, from-top-right, from-right, from-bottom-right, from-bottom, from-bottom-left), `none`; timings `linear`, `spring`.

### Properties

221 CSS properties are accepted. Unknown properties are errors, never ignored:

`WebkitBoxOrient`, `WebkitLineClamp`, `alignContent`, `alignItems`, `alignSelf`, `aspectRatio`, `backdropFilter`, `backfaceVisibility`, `background`, `backgroundBlendMode`, `backgroundClip`, `backgroundColor`, `backgroundImage`, `backgroundOrigin`, `backgroundPosition`, `backgroundPositionX`, `backgroundPositionY`, `backgroundRepeat`, `backgroundSize`, `blockSize`, `border`, `borderBottom`, `borderBottomColor`, `borderBottomLeftRadius`, `borderBottomRightRadius`, `borderBottomStyle`, `borderBottomWidth`, `borderColor`, `borderImage`, `borderImageOutset`, `borderImageRepeat`, `borderImageSlice`, `borderImageSource`, `borderImageWidth`, `borderLeft`, `borderLeftColor`, `borderLeftStyle`, `borderLeftWidth`, `borderRadius`, `borderRight`, `borderRightColor`, `borderRightStyle`, `borderRightWidth`, `borderStyle`, `borderTop`, `borderTopColor`, `borderTopLeftRadius`, `borderTopRightRadius`, `borderTopStyle`, `borderTopWidth`, `borderWidth`, `bottom`, `boxDecorationBreak`, `boxShadow`, `boxSizing`, `clear`, `clipPath`, `clipRule`, `color`, `columnCount`, `columnFill`, `columnGap`, `columnRule`, `columnRuleColor`, `columnRuleStyle`, `columnRuleWidth`, `columnSpan`, `columnWidth`, `columns`, `direction`, `display`, `dominantBaseline`, `fill`, `fillOpacity`, `fillRule`, `filter`, `flex`, `flexBasis`, `flexDirection`, `flexFlow`, `flexGrow`, `flexShrink`, `flexWrap`, `float`, `fontFamily`, `fontFeatureSettings`, `fontKerning`, `fontSize`, `fontStretch`, `fontStyle`, `fontVariant`, `fontVariantCaps`, `fontVariantLigatures`, `fontVariantNumeric`, `fontVariantPosition`, `fontWeight`, `gap`, `grid`, `gridArea`, `gridAutoColumns`, `gridAutoFlow`, `gridAutoRows`, `gridColumn`, `gridColumnEnd`, `gridColumnStart`, `gridRow`, `gridRowEnd`, `gridRowStart`, `gridTemplate`, `gridTemplateAreas`, `gridTemplateColumns`, `gridTemplateRows`, `height`, `hyphens`, `imageRendering`, `inlineSize`, `inset`, `insetBlock`, `insetInline`, `isolation`, `justifyContent`, `justifyItems`, `justifySelf`, `left`, `letterSpacing`, `lineBreak`, `lineHeight`, `margin`, `marginBlock`, `marginBottom`, `marginInline`, `marginLeft`, `marginRight`, `marginTop`, `mask`, `maskComposite`, `maskImage`, `maskPosition`, `maskRepeat`, `maskSize`, `maxHeight`, `maxWidth`, `minHeight`, `minWidth`, `mixBlendMode`, `objectFit`, `objectPosition`, `opacity`, `order`, `outline`, `outlineColor`, `outlineOffset`, `outlineStyle`, `outlineWidth`, `overflow`, `overflowWrap`, `overflowX`, `overflowY`, `padding`, `paddingBlock`, `paddingBottom`, `paddingInline`, `paddingLeft`, `paddingRight`, `paddingTop`, `paintOrder`, `perspective`, `perspectiveOrigin`, `placeContent`, `placeItems`, `placeSelf`, `position`, `right`, `rotate`, `rowGap`, `scale`, `shapeRendering`, `stroke`, `strokeDasharray`, `strokeDashoffset`, `strokeLinecap`, `strokeLinejoin`, `strokeMiterlimit`, `strokeOpacity`, `strokeWidth`, `tabSize`, `textAlign`, `textAlignLast`, `textAnchor`, `textDecoration`, `textDecorationColor`, `textDecorationLine`, `textDecorationStyle`, `textDecorationThickness`, `textIndent`, `textOrientation`, `textOverflow`, `textRendering`, `textShadow`, `textTransform`, `textUnderlineOffset`, `textWrap`, `textWrapMode`, `textWrapStyle`, `top`, `transform`, `transformBox`, `transformOrigin`, `transformStyle`, `translate`, `unicodeBidi`, `vectorEffect`, `verticalAlign`, `visibility`, `whiteSpace`, `whiteSpaceCollapse`, `width`, `wordBreak`, `wordSpacing`, `writingMode`, `zIndex`.

Elements are `box-sizing: border-box` unless you set `boxSizing`.

### Fonts

Only these families, served by Fablyn. Unknown families and unavailable weights are errors. Text with no font set is in Inter.

| Family | Weights | Italic |
|---|---|---|
| Bricolage Grotesque | 400, 700 | no |
| Caveat | 400, 700 | no |
| DejaVu Sans | 400, 700 | no |
| DejaVu Sans Mono | 400, 700 | no |
| DejaVu Serif | 400, 700 | no |
| Fraunces | 400, 700 | yes |
| IBM Plex Sans | 400, 700 | yes |
| Instrument Serif | 400 | yes |
| Inter | 400, 700 | no |
| JetBrains Mono | 400, 700 | yes |
| Lora | 400, 700 | yes |
| Merriweather | 400, 700 | yes |
| Montserrat | 400, 700 | no |
| Noto Sans | 400, 700 | no |
| Noto Sans Arabic | 400, 700 | no |
| Noto Sans Devanagari | 400, 700 | no |
| Noto Sans Gurmukhi | 400, 700 | no |
| Noto Sans Hebrew | 400, 700 | no |
| Noto Sans JP | 400, 700 | no |
| Noto Sans KR | 400, 700 | no |
| Noto Sans SC | 400, 700 | no |
| Noto Sans TC | 400, 700 | no |
| Noto Sans Thai | 400, 700 | no |
| Nunito | 400, 700 | no |
| Open Sans | 400, 700 | no |
| Roboto | 400, 700 | no |
| Syne | 400, 700 | no |
| Unbounded | 400, 700 | no |

### Components

Define reusable pieces under `components` and instantiate them by name with props. Inside a definition, `{"$prop": "title"}` stands in for a value, `{"$children": true}` marks where the instance's children go, and `{"$each": "rows", "render": { ... }}` repeats a template once per item of a list prop. An instance's `style` is merged over the definition's root style. The library ships 9 components: `BarList`, `Caption`, `Card`, `Metric`, `Outro`, `Pill`, `Row`, `Screen`, `ShareBars`. `get_component` returns a definition to copy into your composition's `components`.

### Limits

At most 2000 nodes after components expand, 32 component definitions, nesting 12 deep, and 1048576 bytes of JSON.

## Errors and warnings

An error blocks saving and rendering. Each has a `code`, a `path` in your document, a `message` saying what to change, and `alternatives` when there is a list to choose from. Inside a component, `definition` names the component and the path in it. Fix exactly what the path points at.

A warning comes from stills and never blocks anything: text that overflows its box, a node off the canvas, text or images overlapping their siblings, text below 3:1 contrast, or text too small to read on a phone (`min_text_size`: the composition's shorter side shown at 360 pixels, text under 9 pixels there; at 1080 wide, a font size under 27). Text or images under 0.35 opacity are fading in or out, or are deliberate ghosts: they are left out of overlap, contrast and size, but not out of overflow or off canvas.

## Templates

- `examples/product-launch.json`, **Product launch**: Five scenes in a Series with two narrated lines, library components, a badge held across lines and a Loop. The fuller reference.
- `examples/hold-across-lines.json`, **Hold across lines**: A label that enters with the first narration line and stays until the second ends, with no frame numbers written.
- `examples/loop.json`, **Loop**: A pulse that repeats every second inside a narrated Sequence.
- `examples/slides.json`, **Slides**: Three silent scenes in a Series, no narrator: the shape of a slide deck.
- `examples/steps-coach.json`, **3 steps to a morning that sticks**: Three numbered steps, each with a why-it-works card, and a progress bar that fills as each step lands. Narrated, for a vertical feed.
- `examples/numbers-real-estate.json`, **Northbank this quarter, in four numbers**: Four figures, one to a scene: the number, its change and what it compares with. Narrated, for a portrait feed.
- `examples/question-clinic.json`, **Should I use ice or heat?**: A question people ask, answered in four scenes: the question, the short answer, the two cases side by side and two rules. Narrated, landscape.
- `examples/announcement-organiser.json`, **Makers Night is back**: A community event in four scenes: the news, when and where, what's on and how to save a place, with a ticket stub in every scene. Narrated, for a vertical feed.
- `examples/quote-podcaster.json`, **Slow Growth, episode 42**: One line from a podcast episode, set large, then who said it and where to listen. Narrated, square.
- `examples/recap-nonprofit.json`, **Riverbend's winter appeal**: A season in review for supporters: three outcomes, a month-by-month chart and a thank-you. Narrated, landscape.
- `examples/list-food.json`, **5 pantry swaps that save money**: Five money-saving swaps counted off two to a scene, each row dimming as the list grows, then a call to save it. Narrated, for a vertical feed.
- `examples/myth-finance.json`, **Myth: you need a big salary to invest**: A common belief struck through, then the fact, why it holds with a simple chart, and a follow. Narrated, square.
- `examples/beforeafter-trade.json`, **A bathroom quote in a day**: The hook, then a dimmed Before card beside a dark After card, then how to book. Narrated, for a portrait feed.
- `examples/countdown-conference.json`, **3 days to Launchpad Live**: Days to go, the day's agenda with times, then how to register. Narrated, landscape.

The same fourteen come back from `get_example` by name (`product-launch`, `hold-across-lines`, `loop`, `slides`, `steps-coach`, `numbers-real-estate`, `question-clinic`, `announcement-organiser`, `quote-podcaster`, `recap-nonprofit`, `list-food`, `myth-finance`, `beforeafter-trade`, `countdown-conference`), and from `https://fablyn.app/connector/examples/<name>.json`.
