Docs
Concepts

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.

text
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:

text
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.

text
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:

text
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:

text
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:

text
{{#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}}:

text
{{#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:

text
{{#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.

text
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:

text
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:

text
{
  "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:

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