Authoring in TSX

Author one default-exported SML template per TSX file with typed variables, predicate builders, and clear send-time semantics.

SML (the primitives + class-subset language documented on the rest of this site) is deliberately a small, flat, closed grammar, no components, no props, no composition. That's a good property for a compiled document format: it keeps per-keystroke compile fast, keeps the editor's canvas round-trip lossless, and keeps the built-in agent's grammar small enough to hold in a system prompt. It's a worse property for hand-authoring anything with real structure, reusable sections, typed inputs, composition.

TSX is the answer to that: a typed authoring layer that compiles down to SML, not a second runtime format. You write idiomatic TypeScript/JSX functions; a build step evaluates them once, on your machine, and emits plain .samva source. The platform never sees your TSX, never runs your build tool, and never executes your code. It only ever receives and recompiles SML, the same closed grammar this whole reference documents.

One default export per file

Each .tsx file owns at most one template, and that template is its default export:

export default function WelcomeEmail(v: Variables<{ firstName: string }>) {
  return (
    <Email class="bg-stone-50">
      <Preview>Welcome aboard, {v.firstName}</Preview>
      <Container class="bg-white px-8 py-6">
        <Heading level={1} class="text-2xl font-bold">
          Welcome, {v.firstName}
        </Heading>
      </Container>
    </Email>
  );
}

The file is the template identity. Named exports are ignored by template discovery, so you can use them for helpers, constants, and ordinary composition without accidentally creating additional templates. A TSX file without a default export is excluded with a warning; a default export that does not return <Email>, <Sms>, or <WhatsApp> is a build diagnostic.

The components (<Email>, <Container>, <Heading>, ...) mirror the SML primitives one for one, same tags, same attributes, same content model. There's no new vocabulary to learn beyond what's already on the primitives reference; TSX just gives that vocabulary TypeScript's type-checking, editor autocomplete, and composition via ordinary functions and imports.

Evaluation, not parsing

This is the core mechanical difference from SML: TSX is evaluated, never statically analyzed. The build step runs your function with a real JavaScript engine, any TypeScript that's valid to run at build time is accepted (loops, conditionals over build-time data, helper functions, imports from your own codebase, npm packages). The output of running that function (the JSX tree it returns) is what gets validated against SML's closed grammar The same schemas and checks validate hand-written .samva source.

Because it's evaluation and not parsing, there's nothing to sandbox: your code runs on your machine, in your build, the same trust boundary as the rest of your toolchain. Nothing executes on Samva's infrastructure, the platform's only input is the emitted SML.

Variables<T> typed send-time holes

export default function WelcomeEmail(v: Variables<{firstName: string}>) declares the template's variable schema as an ordinary TypeScript type, and the emitter invokes your function with a recording proxy in place of real data. Reading v.firstName doesn't return a string, it returns a reference that, when it ends up somewhere in the JSX tree, prints as {{firstName}} in the emitted SML. The declared type doubles as the variable schema the platform validates sends against, so there's exactly one place variables are declared, not a TypeScript type and a separate JSON schema that can drift apart.

The build-time-vs-send-time rule

Variables are send-time holes, real values don't exist until a specific email is being sent to a specific recipient, which is long after your build ran. v.firstName is a placeholder, not a string, at the moment your function executes.

This means ordinary JavaScript coercion of a variable reference is a build-time error, not a build-time computation: comparing it (v.plan === "pro"), concatenating it (`Hi ${v.firstName}!` outside JSX text position), or doing arithmetic on it throws immediately when the emitter runs your function. There is no value yet to compare, concatenate, or compute with, only a reference to where a value will eventually go. Letting plain JS operators silently coerce that reference to "[object Object]" or NaN would silently emit a broken template instead of failing loudly at the one point (your local build) where you can actually see the mistake.

The fix is the same variable reference's typed predicate builders, which construct a predicate reference instead of coercing to a real value:

// Throws at build time, plain coercion, not a builder call:
if (v.plan === "pro") { /* ... */ }

// Correct, builder methods construct a PredicateRef, never coerce:
<If test={v.plan.eq("pro").and(v.seats.gt(5))}>
  <Text>Your team plan covers {v.seats} seats.</Text>
  <Else>
    <Text>Thanks for being a pro customer, {v.firstName}.</Text>
  </Else>
</If>

VariableRef grows typed comparison methods per field (eq/ne/gt/gte/ lt/lte, typed by the field's declared type), presence checks (exists/notExists), and boolean chaining (and/or/not) that compose PredicateRefs. The emitter prints the resulting PredicateRef as an SML test="..." string using the exact same predicate grammar documented for hand-written SML, comparisons over flat names and literals, and/or/not, no arithmetic, no paths. The builders exist specifically so that grammar is reachable through typed, autocompletable TypeScript instead of string-templating a predicate by hand.

This is the same shape as {{}}/${} interpolation always had, variables were never usable in real if statements or template-string logic, because "real" values don't exist at build time. <If>/predicate builders are the one expressiveness upgrade over plain interpolation: a typed, send-time-safe way to branch, without ever pretending a variable is a build-time value.

The pipeline

template.tsx (default export)
  │  (Vite JSX transform, evaluated at YOUR build time)

SmlDocument AST (in memory)
  │  print

canonical .samva source
  │  samva push  →  sends SOURCE ONLY, never compiled HTML

platform: parses + compiles with the org's theme layers, server-side

artifact HTML (+ plain-text alt), stored content-addressed

Two things worth being precise about:

  • Local preview runs the full chain, including the SML → HTML compiler. the same pure library the platform Worker, the editor's browser canvas, and the test suite all call. A local dev server can show you real compiled output, with hot reload, entirely offline.
  • Push sends SML, never HTML. The platform always recompiles server-side, against the organization's current brand-kit theme and target-client profile. This is what keeps a template re-themeable after the fact and keeps "what compiled this" a server-side, auditable fact, a local toolchain that's a version behind can't ship a stale-looking artifact, because it never ships an artifact at all.

Composition lives here, not in SML

SML's primitive set is permanently flat and closed, no <component>, no <slot>, no user-defined macros, ever. Composition (a reusable "email footer" block, a shared header with props, a library of on-brand sections) is a TSX-authoring-layer concern: it's ordinary function composition, the same way you'd share a React component. The emitter inlines the result, a composed TSX template still emits one flat SML document, because that's the only shape the platform (and the built-in agent, and the canvas) ever has to understand.

Determinism

The emitter runs your template function twice and diffs the two emitted SML strings. Anything that makes those two runs differ, Date.now(), Math.random(), reading mutable external state, is flagged as a build-time warning, because a non-deterministic template makes diffing pushed versions meaningless and breaks the emission-determinism guarantee the whole pipeline relies on.

Managed-from-code templates in the dashboard

A template pushed from TSX carries origin: tsx on its stored version. The dashboard canvas stays fully editable, dashboard edits aren't blocked, but the editor shows a "managed from code" banner, and samva push warns (or, in stricter setups, refuses) if the repo's version has diverged from what's currently published, the same posture git push --force-with-lease takes toward a moved remote. Whichever side is the source of truth for a given template, the repo for a code-managed team, the dashboard for a template authored natively there, the underlying version history (every save is a commit, with rollback) makes the divergence visible either way.

Who writes what

  • The built-in template agent always writes SML directly, the closed grammar and span-precise diagnostics are exactly what makes its write → validate → retry loop converge quickly. It has no reason to go through a TSX layer.
  • A customer's own agent, working in their repo, writes TSX idiomatically , its iteration loop is tsc, the emitter's determinism/coercion checks, and ultimately the same SML diagnostics, all through standard tooling a code-focused agent already knows how to drive.
  • Conditionals, the predicate grammar the typed builders emit into, and the full send-time-vs-build-time explanation from the SML side.
  • Variables, {{name}}/${name} interpolation semantics, unchanged underneath the typed Variables<T> layer.
  • Primitives, the tag vocabulary TSX components mirror one for one.

On this page