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:
| Name | Description |
|---|---|
| string | Any text. Maps to string (TS) / str (Python). |
| integer | Whole numbers. Maps to number (TS) / int (Python). |
| number | Real numbers, including decimals. Maps to number (TS) / float (Python). |
| boolean | True/false. Used by Mustache section/inverted-section truthiness too. |
| object | Nested object. Field types are inferred from the dotted variables you reference (e.g. {{user.email}}). |
| array | List 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)
}