Data & validation
Hydrate with data; bad data never renders.
hydrate checks a data row against the template's fields and returns a new, filled-in document. The template is not changed. Same template and same data always give the same document.
import { createProject } from "@miraiclip/core";
import { hydrate, type Template } from "@miraiclip/templates";
declare const template: Template; // the "Promo" template from Defining templates
const doc = hydrate(template, { name: "Ada", discount: 20 });
doc.clips.headline; // text: "Hi Ada"
doc.clips.badge; // params: { bg: "#ff8c32", size: 40, label: "20% off" }
const promo = createProject(doc); // preview, edit or export it like any projectaccent and size weren't given, so their defaults apply. size stays a number because its param is exactly "{{size}}".
Asset fields
An asset field's value replaces the asset's src. Pass durationUs too when the new media's length differs.
import { hydrate, type Template } from "@miraiclip/templates";
declare const template: Template;
const doc = hydrate(template, {
name: "Ada",
discount: 20,
footage: { src: "media/ada.mp4", durationUs: 6_000_000 },
});
doc.assets.bg; // { id: "bg", kind: "video", src: "media/ada.mp4", durationUs: 6000000 }Clip timing doesn't change. If the new media is shorter than the clip using it, run fitClipsToMedia on the result.
Errors
hydrate throws TemplateValidationError. tryHydrate returns the problems instead, which suits forms and AI agents.
import { tryHydrate, type Template } from "@miraiclip/templates";
declare const template: Template;
const result = tryHydrate(template, { name: "A very long name that exceeds twenty", discount: 200, extra: 1 });
if (!result.ok) {
result.issues;
// [{ path: "data.extra", message: "unknown field" },
// { path: "data.name", message: "longer than maxLength 20" },
// { path: "data.discount", message: "above max 90" }]
}| Result | Shape |
|---|---|
| Success | { ok: true, doc } |
| Failure | { ok: false, issues: [{ path, message }] } |
Every problem in the row is reported at once. Paths start with data. and name the field.
| Message | Cause |
|---|---|
required field is missing | A required field without a default was left out |
unknown field | The row has a key that isn't a field |
expected a string, got number | Wrong type (also a number, a boolean) |
longer than maxLength 20 | text maxLength |
does not match pattern ^[A-Z] | text pattern |
must be one of: Pro, Team | text enum |
below min 5, above max 90, expected an integer | number limits |
expected a CSS color string | color is not a non-empty string |
expected a source string or { src, durationUs? } | asset value |
Check without hydrating
validateData returns the issues plus the resolved values, with defaults applied.
import { validateData, type Template } from "@miraiclip/templates";
declare const template: Template;
const { resolved, issues } = validateData(template, { name: "Ada", discount: 20 });
Object.fromEntries(resolved); // { name: "Ada", accent: "#ff8c32", size: 40, discount: 20 }
issues; // []Rows from CSV and NDJSON
For batches, turn text into rows. Both functions are pure: pass the file contents, not a path.
import { rowsFromCsv, rowsFromNdjson, type Template } from "@miraiclip/templates";
declare const template: Template;
rowsFromCsv(template, "name,discount,accent\nAda,20,\nGrace,35,#2e8f63\nLinus,abc,\n");
// [{ name: "Ada", discount: 20 },
// { name: "Grace", discount: 35, accent: "#2e8f63" },
// { name: "Linus", discount: NaN }]
rowsFromNdjson('{"name":"Ada","discount":20}\n\n{"name":"Grace","discount":35}\n');
// [{ name: "Ada", discount: 20 }, { name: "Grace", discount: 35 }]CSV cells are strings, so rowsFromCsv converts them using the template's field types:
| Cell | Becomes |
|---|---|
| Empty | Left out, so the field's default applies |
number field | Number(cell). Text that isn't a number becomes NaN, which fails validation |
boolean field | true for true or 1, false for anything else |
| Any other field | The string as-is |
The first line is the header. Cells may be quoted with ", and "" inside quotes is a literal quote. Blank lines are skipped.
rowsFromNdjson parses one JSON object per non-empty line. Its values aren't converted.
Rows aren't validated when parsed. Validation happens per row in hydrate, tryHydrate or batch rendering.
As an AI tool
toFieldToolDefinition turns the fields into one tool definition. The model's tool input is exactly the data hydrate takes.
import { describeTemplate, toFieldToolDefinition, tryHydrate, type Template } from "@miraiclip/templates";
declare const template: Template;
const tool = toFieldToolDefinition(template); // { name, description, input_schema }
const openaiTool = toFieldToolDefinition(template, { style: "openai" }); // { type: "function", function: { … } }
const context = describeTemplate(template); // for the system prompt
// When the model calls the tool:
declare const input: Record<string, string | number | boolean>;
const result = tryHydrate(template, input);
// on failure, send result.issues back as the tool result so the model can fix its inputThe generated schema for the "Promo" template, abridged:
{
"name": "render_promo",
"description": "Promo card Fill the \"Promo\" video template's fields; the values hydrate the template into a renderable document.",
"input_schema": {
"type": "object",
"properties": {
"name": { "type": "string", "maxLength": 20 },
"accent": { "type": "string", "description": "A CSS color (e.g. \"#ff8c32\").", "default": "#ff8c32" },
"size": { "type": "integer", "minimum": 24, "maximum": 96, "default": 40 },
"discount": { "type": "integer", "minimum": 5, "maximum": 90 },
"footage": { "description": "Replaces asset \"bg\": a source URL/path, or { src, durationUs? }.", "anyOf": ["…"] }
},
"required": ["name", "discount"],
"additionalProperties": false
}
}| Option | Default | What it does |
|---|---|---|
style | "anthropic" | "anthropic" or "openai" wire shape |
name | "render_" + the template name, sanitized | Tool name |
For tools that edit any project, see Tools from the command catalog.
Insert into an existing project
To start a new project from a template, use createProject(hydrate(template, data)). To add a filled template to a project that already has content, such as an intro or an end card, use insertDocument.
import { fitClipsToMedia, hydrate, insertDocument, type Template } from "@miraiclip/templates";
declare const template: Template;
const doc = fitClipsToMedia(hydrate(template, { name: "Ada", discount: 20 }));
const plan = insertDocument(project, doc, { atUs: 4_000_000 }); // one undo step
plan.ids.clips.headline; // the inserted clip's id, e.g. "tpl-headline"
plan.startUs; // 4000000
plan.endUs; // where the inserted content ends| Function | What it does |
|---|---|
insertDocument(project, doc, options?) | Adds the document's tracks on top of the existing ones, with clips shifted to atUs, as one transaction. Returns the plan |
insertCommands(targetDoc, doc, options?) | The same commands, without dispatching them |
fitClipsToMedia(doc) | Shortens video and audio clips that would play past the end of their media, and shortens or drops transitions that lose their footage. Returns a new document |
insertDocument option | Default | Meaning |
|---|---|---|
atUs | 0 | Where the inserted document's time 0 lands |
idPrefix | derived to avoid collisions | Prefix for new clip, track and asset ids |
Tracks without clips aren't inserted. The target's settings are not changed: compare doc.settings yourself if the frame size matters.