Skip to main content
A signature declares one LM interaction as a plain Rust struct: typed inputs, typed outputs, and an instruction. DSRs compiles the declaration into a prompt and parses the model’s response back into your types, which removes prompt strings and response parsing from your code entirely. A signature holds no state and makes no calls; it is the contract that predictors execute.

Declaring a signature

Three rules cover most of what there is to know:
  • The struct doc comment becomes the instruction the model receives.
  • Field doc comments become field descriptions in the prompt. Write one only when it adds something the field name does not; /// The question on a field named question adds nothing.
  • The #[output] field’s type is part of the prompt. Declaring answer: String versus answer: Vec<String> versus a custom enum changes both what the model is told to produce and what the parser will accept.
The derive generates QAInput and QAOutput structs (each with a new constructor) and implements the Signature trait: instruction(), input_shape(), output_shape(), and per-field metadata. Calling the signature is the predictor’s job:

Richer output types

The type system is the main lever for controlling model output. An enum output constrains the model to its variants; an Option makes absence a legal answer instead of an invitation to guess; a nested struct is filled field by field; a constraint attaches a rule the value must satisfy:
If the model returns something that cannot be read as a Sentiment, the call fails with a typed error carrying the raw response; see Predict for error handling and Adapters for how the types are rendered into the prompt and parsed back.

Field attributes

Every field requires exactly one of #[input] or #[output], and the signature requires at least one of each. #[render(jinja = ...)] templates see this (the field value), input (the full input object with alias overlays), field (name, rust_name, type), and vars (currently empty). Available filters: minijinja builtins plus regex_match, sum, and truncate.

Supported field types

Rejected shapes (compile errors)

Custom types with #[Schema]

#[Schema] marks a struct or enum as usable inside signature fields. It accepts no arguments. It expands to #[derive(facet::Facet, serde::Serialize, serde::Deserialize)] with crate-path attributes; enums additionally receive #[repr(u8)] when no explicit repr is present.
#[BamlType] survives as a backwards-compatible alias with identical expansion. The old vendored BAML stack, including its #[baml(...)] attribute grammar, was removed. The schema layer now reads facet metadata instead.
What the schema builder honors on #[Schema] types: Enums must be unit-only. Variant matching at parse time is case-insensitive and strips quotes. Data-carrying enums are rejected: schema construction panics with data-carrying enums are not supported; use a struct. Model union-shaped data as a struct with optional fields.

The typesys pipeline

The in-house typesys module implements the type system in four parts: schema (the FieldType/OutputSchema model, derived from facet Shape metadata), render (the schema text the model sees), coerce (tolerant parsing of model output), and constraint (check/assert evaluation). Order per call: render the schema into the prompt, coerce the response text per field, evaluate constraints, then deserialize into the output struct via serde.

render

typesys::type_name produces the inline label shown in field descriptions and the should be of type: line (unions render as A or B, literals as "value"). typesys::schema_block produces the expanded block for structured types: classes render as { field: type, ... } with doc comments, enums as a one of: value list, lists of classes as a bracketed block. Primitives return their label so the adapter skips a redundant block.

coerce

typesys::coerce(raw, field_type, types) returns a Coerced { value, flags }. Non-fatal observations are recorded as Flag values: StrippedCodeFence, ParsedListFromText, CoercedFromString, ExtraTextIgnored. Per-field results land in CallMetadata::field_meta, an IndexMap<String, FieldMeta> where FieldMeta carries raw_text, flags, and checks.

Constraints

Constraints attach rules to output fields. Use #[assert] for rules whose violation should fail the call, and #[check] for rules you want recorded without failing.

Expression language

Expressions are minijinja expressions evaluated with the parsed field value bound as this. Derive-emitted expressions compile once per process.
Python-style method calls do not evaluate: this.len(), this.startswith(...), and this.contains(...) always come out false. A failed evaluation counts as not passing, so an assert written that way fails every call. Use this|length and in instead.

Inspecting results

Compile-time enforcement

A #[check] without a label is a compile error:

See also