# Mustache extensions

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

Sufleur templates are [Mustache](https://mustache.github.io/mustache.5.html) — 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](https://mustache.github.io/mustache.5.html) 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.

```mustache
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:

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

```mustache
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:

#### TypeScript

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

#### Python

```python
class WelcomeMessage_UserPromptInput(TypedDict):
    user: _WelcomeMessage_UserPromptInput_User


class _WelcomeMessage_UserPromptInput_User(TypedDict):
    name: str
    """User's first name as they prefer to be addressed"""
```

`@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:

```mustache
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:

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

```mustache
{{#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:

```mustache
{{#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](/docs/concepts#output-schemas).

```mustache
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](https://json-schema.org/):

```json
{
  "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:

#### TypeScript

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

#### Python

```python
result = get_prompt('@acme/extract-user').parse_output(llm_response)
if result['success']:
    print(result['data'].email)  # typed via Pydantic
else:
    print(result['error'])
```

> [!NOTE] Quick recap
>
> - `{{@type}}`, `{{@doc}}`, and `{{@optional}}` annotate variables for the generated code; they're dropped from the rendered prompt.
> - `{{@outputSchema}}` is replaced with the JSON of your output schema at codegen time, so the model sees the schema in its prompt.
> - In all four cases, the directives never reach the LLM as `{{@...}}` tokens — they're either stripped or substituted first.
