Template inputs
Declare a template's input schema and fixtures, bind fields in the body, format values with fmt, and choose the locale and time zone per send.
The schema is the template's contract with every caller. It is checked in three places: at compile time against every binding in the body, at publish time against every fixture, and at send time against the real payload.
Schema
Write the schema as a literal jsonSchema<Input>({ ... }) from @samva/markup/input-schema. The
type argument gives the body and your editor the input type; the object is the portable JSON Schema
that validates every send.
import { defineTemplate, type TemplateInput } from "@samva/markup";
import { Email, Section } from "@samva/markup/email";
import { fmt } from "@samva/markup/fmt";
import { jsonSchema } from "@samva/markup/input-schema";
const template = defineTemplate({
id: "appointment",
locale: "en-US",
schema: jsonSchema<{
name: string;
startsAt: string;
guests: string[];
balance: number;
currency: string;
}>({
type: "object",
properties: {
name: { type: "string" },
startsAt: { type: "string", description: "ISO 8601 start time" },
guests: { type: "array", items: { type: "string" } },
balance: { type: "number" },
currency: { type: "string" },
},
required: ["name", "startsAt", "guests", "balance", "currency"],
additionalProperties: false,
}),
fixtures: {
default: {
name: "Ada",
startsAt: "2026-10-01T15:30:00Z",
guests: ["Grace", "Alan"],
balance: 1250.5,
currency: "EUR",
},
},
email: {
subject: (input) => `Your appointment on ${fmt.date(input.startsAt, "medium")}`,
body: (input) => (
<Email>
<Section>
<p>
Hi {input.name}, see you at {fmt.time(input.startsAt, "short")} with{" "}
{fmt.list(input.guests)}.
</p>
<p>Balance due: {fmt.money(input.balance, input.currency)}</p>
</Section>
</Email>
),
},
});
export type AppointmentInput = TemplateInput<typeof template>;
export default template;- The schema uses JSON Schema 2020-12 with local references. Remote references,
format, and unknown keywords fail withinvalid-schema. - The payload is validated as sent. Defaults are not inserted, values are not coerced, and extra
properties are not removed. A schema
defaultdocuments intent only. - A payload that fails validation is rejected before render, so a send never renders a blank.
- Strings are escaped as text, never interpreted as markup.
Bindings are checked against the schema
Bindings are checked against the schema, not by running TypeScript, so the hosted editor, the CLI,
and the local editor report the same findings. {input.ordr} fails with unknown-field and
suggests order. Reading an optional field outside a guard fails with unguarded-optional; see
the static profile.
TemplateInput<typeof template> gives your own code the input type. samva templates types writes
typed inputs for every published template, so the application that sends a message is checked
against the same schema.
Fixtures
Fixtures are named payloads that must validate against the schema. Each one is a preview in the
editor, a render target for agents and samva templates render, and a publish check. Cover every
branch of the body: a default, one without each optional field, and one with many list items.
A fixture is data. Write it as literals, or as constants and imported assets assembled from
literals. A fixture that stops matching the schema fails with fixture-invalid and names the
fixture and the field.
Formatters
fmt from @samva/markup/fmt is the only set of functions a body may call. Each formatter is
deterministic: the locale and time zone come from the send, and times come from the input, so a
render reads no clock.
| Formatter | Example | Output |
|---|---|---|
fmt.money(amount, currency, locale?) | fmt.money(84, "EUR", "de") | 84,00 € |
fmt.number(n, options?, locale?) | fmt.number(1234.5, { maximumFractionDigits: 0 }) | 1,235 |
fmt.date(iso, style?, locale?) | fmt.date(input.shippedAt, "medium") | Sep 29, 2026 |
fmt.time(iso, style?, locale?) | fmt.time(input.slot, "short") | 3:30 PM |
fmt.plural(n, forms, locale?) | fmt.plural(n, { one: "item", other: "items" }) | items |
fmt.list(values, locale?) | fmt.list(["Ada", "Grace"]) | Ada and Grace |
date and time take short, medium, long, or full. Dates and times are ISO 8601 strings. A
call with an unknown formatter fails with unknown-formatter.
Locale and time zone
Locale and time zone are options of the send, not fields of the schema, and fmt reads them
implicitly:
localeis a BCP 47 language tag such asde-DE. It defaults to the template'slocale, and then toen-US.timeZoneis an IANA name such asEurope/Berlin. It defaults toUTC.- An explicit locale argument, as in
fmt.money(84, "EUR", "de"), overrides the send's locale.
Pass them on the send: see Format dates and money for the recipient,
or --locale and --time-zone in the CLI. Scheduled messages and
campaigns take the same two fields, and samva templates render accepts both flags for a preview.