Mustache extensions

The @type, @doc, @optional, and @outputSchema additions on top of Mustache.

Sufleur templates are Mustache — the full spec, no subset, no surprises. We assume you already know variables, sections, inverted sections, and partials; if you don't, the Mustache manual is the right place to start.

On top of plain Mustache, Sufleur adds four optional directives that turn templates into self-describing, type-safe interfaces. None of them appear in the rendered prompt — they're parsed out before your prompt ever reaches an LLM.

{{@type}} — annotate a variable

{{@type X}} declares the JSON Schema type of the variable that immediately precedes it. The annotation flows through to the generated code, where it becomes the input type for that field.

Hi {{user.name}}{{@type string}}, you're {{user.age}}{{@type integer}} today.

When this template renders, the {{@type ...}} directives are dropped, so the output is just:

Hi Ada, you're 32 today.

Six types are accepted:

NameDescription
stringAny text. Maps to string (TS) / str (Python).
integerWhole numbers. Maps to number (TS) / int (Python).
numberReal numbers, including decimals. Maps to number (TS) / float (Python).
booleanTrue/false. Used by Mustache section/inverted-section truthiness too.
objectNested object. Field types are inferred from the dotted variables you reference (e.g. {{user.email}}).
arrayList of items. Use Mustache section blocks ({{#items}}…{{/items}}) to iterate.

If you don't annotate a variable, Sufleur infers the loosest type that fits — usually string, sometimes unknown / Any when ambiguous. Explicit annotations are strongly recommended because they sharpen the generated input types and document your intent for teammates.

{{@doc}} — document a variable

{{@doc Description text...}} attaches a human description to the variable that immediately precedes it. The description becomes a JSDoc comment in TypeScript and a """docstring""" field comment in Python.

Hi {{user.name}}{{@type string}}{{@doc User's first name as they prefer to be addressed}}!

Like {{@type}}, the {{@doc ...}} directive is dropped at render time. The example above produces just Hi Ada! — your model never sees the description, only your code does.

In the generated code:

export type WelcomeMessage_UserPromptInput = {
  user: {
    /** User's first name as they prefer to be addressed */
    name: string
  }
}

@doc and @type are independent — you can use either, both, or neither, in either order. The convention in the editor is {{var}}{{@type T}}{{@doc Description}}.

{{@optional}} — mark a variable as optional

{{@optional}} declares that a variable or section is not required — callers may omit it (or pass undefined). It takes no argument; presence alone marks the target optional.

By default every variable Sufleur sees in a template is required. The inferred input schema lists it in required, and the generated code reflects that: name: string in TypeScript, name: str in Python. Adding {{@optional}} drops the field from required, which translates to name?: string / Optional[str] in your generated SDK.

On a variable

Place {{@optional}} directly after the variable, alongside any other directives:

Hi {{name}}{{@optional}}!
You are {{age}}{{@type integer}}{{@optional}} years old.

Order between @type, @doc, and @optional doesn't matter; they all attach to the same preceding variable.

On a section

Place {{@optional}} inside the section's opening tag, alongside other section directives:

{{#user}}{{@optional}}
  Name: {{name}}
  Email: {{email}}
{{/user}}

This marks user itself as optional — callers can omit the entire object/array. Inner fields keep their own required/optional status.

Auto-detection: the same-name conditional idiom

The canonical Mustache pattern for "render this only if the variable is set" already implies optionality, and Sufleur detects it automatically — you don't need to add {{@optional}}:

{{#dueAt}}Due at {{dueAt}}{{/dueAt}}

A section whose only effective child references the same name as the section is treated as an optional variable. The type comes from the inner usage:

{{#dueAt}}{{dueAt}}{{@type integer}}{{/dueAt}}

dueAt?: number in the generated input type.

This rule only fires when the section has exactly one child (whitespace and directives ignored) referencing the same name. Multi-child sections still need an explicit {{@optional}} to be considered optional.

What the playground does

When you leave an optional field blank in the playground, it's passed to Mustache as undefined (not "" / 0 / false). Mustache treats undefined as falsy, so {{#x}}…{{/x}} correctly skips and {{^x}}…{{/x}} renders. The schema view marks optional keys with a trailing ? (cosmetic only).

The CLI should follow the same convention: callers omit the field entirely, or pass the target language's "absent" sentinel.

{{@outputSchema}} — inline the output schema

{{@outputSchema}} is a directive you place inside a template. When the CLI generates your code, it replaces every {{@outputSchema}} with the pretty-printed JSON of your prompt's output schema.

Reply with JSON matching this schema:
 
{{@outputSchema}}
 
Do not include any text outside the JSON.

After codegen, the template embedded in your generated SDK looks like:

Reply with JSON matching this schema:
 
{
  "type": "object",
  "required": ["id", "name", "email"],
  "properties": {
    "id":    { "type": "integer" },
    "name":  { "type": "string", "minLength": 1 },
    "email": { "type": "string", "format": "email" }
  }
}
 
Do not include any text outside the JSON.

This is a build-time substitution, not a runtime render. By the time Mustache renders the template, {{@outputSchema}} is already gone — replaced with the literal JSON of the schema you set on the version.

The schema itself is set per-version through the web app. It's standard JSON Schema:

{
  "type": "object",
  "title": "User",
  "required": ["id", "name", "email"],
  "properties": {
    "id":       { "type": "integer", "description": "Unique user ID" },
    "name":     { "type": "string", "minLength": 1 },
    "email":    { "type": "string", "format": "email" },
    "age":      { "type": "integer", "minimum": 0 },
    "isActive": { "type": "boolean", "default": true }
  }
}

The same schema also drives a parseOutput() function in your generated code, which validates the model's response and returns a typed object:

const result = getPrompt('@acme/extract-user').parseOutput(llmResponse)
if (result.success) {
  console.log(result.data.email) // typed as `string`
} else {
  console.error(result.error)
}