Typed generation

This is the feature Orchard is built around. A plain gen returns a string you then have to parse. gen as T returns a validated value of a type you declared, so the code after it routes on a real value instead of picking apart prose. It is the move that turns a chat reply into a typed program.

From a string to a value

You have already seen gen "prompt": one model call, returns a str. Add as T and the result is a value of type T:

let reply = gen "Summarize this in one line: {report}"        // str
let sev   = gen as Severity "How severe is this: {report}"    // a Severity

The second call is guaranteed to produce a valid Severity. Not a string that looks like one, an actual Severity you can match on with no parsing and no defensive checks.

Declaring the type

T can be a built-in type, an enum you declared, a record type you declared, or a composite like list<str>. An enum is the simplest constrained target: it forces the model to pick exactly one variant.

enum Severity { low, medium, high, critical }

A record type describes a shape with named, typed fields. gen as Ticket returns JSON coerced to that shape, so every field is present and well-typed:

type Ticket {
    title: str
    severity: Severity
    summary: str
}

What the runtime does

When you write gen as Ticket, the runtime does four things for you:

  1. Lowers the type to a JSON schema. Your Ticket becomes an object schema with title, severity (constrained to the enum's variants), and summary. Money and duration fields lower to their string forms.
  2. Instructs the model to emit JSON matching that schema, alongside your prompt.
  3. Parses and coerces the model's output into a real Ticket value, checking every field's type.
  4. Re-prompts on failure. If the output does not parse or does not validate, the runtime tells the model what was wrong and asks again, bounded by policy.max_gen_retries (default 2).

If it still cannot get a valid value after the allowed retries, it raises a GenError, which you can catch. So typed generation either gives you a value of exactly the type you asked for, or it throws an error you can handle. There is no in-between state where you hold a half-valid string.

Routing on the result

Because gen as Severity is guaranteed to be a valid variant, you can match on it, and the checker proves the match covers every case. Miss a variant without a _ branch and orch check rejects the file:

skill classify(report: str) -> str {
    let s = gen as Severity "How severe is this incident: {report}"
    match s {                       // checker proves this is exhaustive
        critical => "page oncall"
        high     => "assign to triage"
        medium   => "queue for review"
        low      => "archive"
    }
}

This is the central pattern: one constrained model call, then ordinary deterministic code that the checker can reason about. The model handles the judgment; your code handles the routing.

Lists and records

Composite targets work the same way. gen as list<str> returns a real list you can iterate:

let angles = gen as list<str> "Three distinct research angles on: {topic}"
for angle in angles {
    emit "angle: {angle}"
}

And a record target gives you a structured value to read field by field:

skill triage(report: str) -> Ticket {
    let t = gen as Ticket "Extract a ticket from this report:\n{report}"
    match t.severity {
        critical => emit "🚨 {t.title}"
        high     => emit "{t.title}"
        _        => {}                 // low and medium: nothing
    }
    return t
}

Generation options

Add a with { ... } clause to tune a single gen call. The common knobs are temperature and max_tokens:

let sev = gen as Severity with { temperature: 0.0 } "Be decisive: {report}"
let draft = gen with { temperature: 0.9, max_tokens: 800 } "Brainstorm names for: {idea}"

A low temperature makes a classification stable; a higher one makes a brainstorm varied. The options apply to that call only and override the model block's defaults for it.

Handling failure

When the model cannot produce a valid value within the retry budget, gen as T raises a GenError. Wrap the call in try / catch when you want a fallback rather than letting it propagate:

var sev: Severity = Severity.medium
try {
    sev = gen as Severity "How severe: {report}"
} catch (e) {
    emit "could not classify, defaulting to medium: {e}"
}

Why this matters

Without typed generation you would prompt the model, get a paragraph back, and write brittle code to find the severity in it: a regex, a keyword search, a hope. That code breaks the moment the model phrases its answer differently. gen as T pushes the parsing, validation, and retry into the runtime, where it is done once and done correctly. Your code receives a value of a type the checker understands, and you spend your effort on what to do with it.

Note. Keep the target type small and specific. A tight enum or a record with a few clear fields gives the model the least room to wander and the runtime the cleanest schema to validate against.

Next. A skill that calls gen is more useful when it can also act on the world. Continue to tools and skills.