# Templates

> Versioned templates that create records, such as an invoice or a project with its tasks, system starters each organization forks, merge fields for text and email, and the workflows each new record starts.

Source: https://bettersupabase.com/docs/blocks/templates

`templates` stores templates of a type you define, such as `invoice`, `project` or `onboarding-email`. A template has settings, a versioned body and a language, and applying it creates a record from the newest published version through your type's `apply`. The module records which version made which record, and starts the [workflow-builder](/docs/blocks/workflow-builder) workflows attached to the template on the new record.

System templates (no organization) are starters your app ships and keeps by key with `sync`. An organization forks one to change it, and the fork tracks the version it copied, so you can tell when the starter moved on.

Reading templates needs `templates.read`, applying one needs `templates.apply`, and creating, editing, publishing, forking and attaching workflows needs `templates.manage`. Admins get every permission, and members get `templates.read` and `templates.apply`. Only the service role changes system templates.

```bash
better-supabase sql add templates
```

The module needs `tenant` and `access`. With `outbox` it records events, with `audit` it audits publishes and applications, with `workflow-builder` it attaches workflows, and with `jsonb-schemas` it checks a workflow's inputs against its whole input schema.

## Template types [#template-types]

A type gives a template its Standard Schemas and creates the record. `settings` and `body` are checked on every write and parsed again before `apply`. `apply` returns the record it created as a subject.

```ts title="lib/templates.ts"
import "server-only";

import * as v from "valibot";
import {
  createTemplates,
  defineMergeFields,
  defineTemplateType,
  sqlTransport,
} from "better-supabase/blocks/templates";

const project = defineTemplateType({
  settings: v.object({ color: v.string() }),
  body: v.object({ tasks: v.array(v.string()) }),
  apply: async ({ body, settings, organizationId, key }) => {
    const created = await projects
      .create({ organizationId, color: settings.color, tasks: body.tasks, key })
      .orThrow();
    return { subject: { type: "project", id: created.id } };
  },
});

export const invoiceFields = defineMergeFields("invoice", {
  "customer.name": { label: "Customer", example: "Ada Lovelace" },
  total: { label: "Total", example: "€120.00" },
});

export const templatesFor = (sql: Sql) =>
  createTemplates({
    transport: sqlTransport(sql),
    types: { project },
    workflows: builderFor(sql),
  });
```

A type without `apply` stores templates you render yourself, such as emails. Applying one returns an `invalid_request` error.

## Starters, forks and defaults [#starters-forks-and-defaults]

```ts
await service
  .sync([
    {
      key: "launch",
      type: "project",
      name: "Product launch",
      body: { tasks: ["Kickoff", "Plan", "Retro"] },
    },
  ])
  .orThrow();

const [starter] = await templates.list(null, { type: "project" }).orThrow();
const fork = await templates.fork(starter.id, organizationId).orThrow();
await templates.setDefault(fork.id).orThrow();
```

`sync` upserts system templates by `key` as the service role, and publishes a new version of each starter whose body changed (it compares the SHA-256 of the body). It returns how many versions it published, so running it on every deploy is cheap.

`fork(source, organizationId)` copies the source's newest published version into a new template in that organization, published as version 1, with its workflow attachments. The fork keeps `forkedFrom` and `forkedFromVersion`. `upstreamVersion` is the source's newest published version, and `behind` is true when it is newer than the one the fork copied.

`list(organizationId)` returns the organization's templates and the system ones, defaults first; `list(null)` returns only the system ones. `setDefault` makes a template the default for its organization, type and language; a unique index keeps one.

## Versions [#versions]

```ts
await templates.versions
  .save(fork.id, { tasks: ["Kickoff", "Plan"] })
  .orThrow();
await templates.versions.publish(fork.id).orThrow();
```

`versions.save` updates the open draft, or opens the next version when the last one is published; a template has at most one draft. `versions.publish` publishes the draft (`TEMPLATE_NO_DRAFT` when there is none) and records `template.published`. Published versions don't change, so an application always points at the exact body it used. `get` returns the template with its newest published version and its draft, bodies included; `versions.list` lists them without bodies.

## Applying a template [#applying-a-template]

```ts
const { subject, runs } = await templates
  .apply(templateId, { into: organizationId, values: { month: "October" } })
  .orThrow();
```

`apply` reads the newest published version (`TEMPLATE_NOT_PUBLISHED` without one), calls the type's `apply` with the parsed settings and body, the organization, your `values` and your `key`, then records the application and its `template.applied` event. A throw from `apply` returns as an error and records nothing. `applications.list(organizationId, { template, subject })` lists what each template made.

Then it starts each enabled attached workflow on the new record through `workflows.run`, with the attachment's inputs as the run's input, the record as its subject and `template-application:<application>:<definition>` as its idempotency key. A run starts as the caller, so a member who may apply a template but not run the workflow gets the record and an `error` in `runs` for that workflow; `apply` doesn't fail. Pass a builder with a service transport when the workflows should run anyway.

## Workflows on new records [#workflows-on-new-records]

```ts
const version = await builder.versions
  .save(definition.id, graph, {
    inputSchema: {
      type: "object",
      required: ["owner"],
      properties: { owner: { type: "string" } },
    },
  })
  .orThrow();
await builder.versions.publish(version.id).orThrow();

await templates.workflows
  .attach({
    template: fork.id,
    definition: definition.id,
    inputs: { owner: userId },
  })
  .orThrow();
```

`workflows.attach` checks `inputs` against the `inputSchema` of the definition's published version: its required keys always, and the whole schema with `jsonb-schemas` installed (`TEMPLATE_WORKFLOW_INPUT_INVALID`). `apply` checks them again, because the definition may have published a new schema since. The definition has to belong to the template's organization or be a system one (`TEMPLATE_WORKFLOW_NOT_FOUND`), and it has to run on the subject type `apply` returns. Attaching it again replaces the inputs; `detach` removes it.

## Recurring records [#recurring-records]

The `templates.apply` step creates a record from a workflow, so a scheduled workflow builds the monthly invoice or the weekly checklist.

```ts title="workflows/steps.ts"
import {
  TEMPLATES_APPLY_STEP,
  templatesApplyStep,
} from "better-supabase/blocks/templates";

export const stepLibrary = [TEMPLATES_APPLY_STEP, ...otherSteps];
export const steps = {
  "templates.apply": templatesApplyStep(templatesFor(postgres.admin)),
  ...otherHandlers,
};
```

A node's config takes `template` and optional `values`, and the step applies the template in the run's organization. It passes `<runKey>:<node>` to your type's `apply` as `key`, so a retried step can find the record it already created instead of creating a second one. It returns `{ applicationId, subject, runs }`. Publishing a graph with the node checks `templates.apply`.

## Merge fields [#merge-fields]

```ts
import {
  composeTemplateEmail,
  renderMergeFields,
} from "better-supabase/blocks/templates";

renderMergeFields("Hi {{ customer.name }}", { customer: { name: "Ada" } });

invoiceFields.unknown("Total: {{ total }} {{ vat }}"); // ["vat"]

const email = composeTemplateEmail(
  { subject: "Invoice {{ number }}", html: "<p>Hi {{ customer.name }}</p>" },
  values,
  { category: "notification", templateKey: "invoice-v3" },
);
```

`renderMergeFields` replaces `{{ name }}` with its value and `{{{ name }}}` with its value unescaped. With `escape: "html"`, `{{ name }}` is escaped for HTML. A dotted name reads that key first and then the nested path, never an inherited property. A missing value renders as empty, or as the field itself with `missing: "keep"`. Values print as text: dates as ISO 8601, Temporal values with their `toString`, arrays joined with commas and plain objects as JSON; pass `format` to change that. It has no dependencies and runs on every WinterTC runtime.

`defineMergeFields(subjectType, fields)` declares the fields a record type offers, for an editor's field picker: `names`, `examples()` for a preview, `unknown(text)` for the fields a template uses that the catalog doesn't define, and a typed `render`. `composeTemplateEmail` renders an email body for the [notifications](/docs/blocks/notifications) email channel's `compose`, with the HTML escaped and the subject and text left as text.

## Events and hooks [#events-and-hooks]

The module records `template.published` with the `versionId`, `version` and `type`, and `template.applied` with the `templateId`, `versionId` and the new record as its subject, so the record's activity feed shows which template made it. It calls `before_template_save`, `after_template_save`, `before_template_apply` and `after_template_apply`; `hooks` in `createTemplates` wraps the client's methods.

Errors carry these hints: `TEMPLATE_INVALID`, `TEMPLATE_NOT_FOUND`, `TEMPLATE_FORBIDDEN`, `TEMPLATE_NO_DRAFT`, `TEMPLATE_NOT_PUBLISHED`, `TEMPLATE_WORKFLOW_NOT_FOUND` and `TEMPLATE_WORKFLOW_INPUT_INVALID`.

## Tables [#tables]

| Table                   | What it holds                                                                                                          |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `templates`             | One row per template: organization (none for a system one), type, name, language, default, settings, the fork's source |
| `template_versions`     | The versions of each template, with the body and its SHA-256; the draft has no `published_at`                          |
| `template_applications` | Which version of which template created which record, by whom                                                          |
| `template_workflows`    | The workflow definitions each template starts on a new record, with their inputs (with `workflow-builder`)             |