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 SeverityThe 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:
- Lowers the type to a JSON schema. Your
Ticketbecomes an object schema withtitle,severity(constrained to the enum's variants), andsummary. Money and duration fields lower to their string forms. - Instructs the model to emit JSON matching that schema, alongside your prompt.
- Parses and coerces the model's output into a real
Ticketvalue, checking every field's type. - 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.
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.
