Orchard is a typed, concurrent language for building LLM agents. A single.orch file defines a complete agent: its model, memory, persona, knowledge, policy, state, tools, skills, and behavior. This page is the formal reference for every construct in Orchard 3.0, the current language version (the runtime that executes it ships as release 3.1.0). It is written to be read in order, but each section stands on its own. If you want a gentler path, start with the progressive guide and come back here for the details.
The grammar of Orchard 3.0 is a superset of the 2.0 syntax. A file that opens with #!orchard 2.0 is accepted and treated as 3.0; the only additions over 2.0 are the concurrency primitives, which 2.0 reserved but did not run. Throughout this reference, code is shown in the canonical style that orch fmt produces.
Lexical structure
An Orchard source file is UTF-8 text. The first meaningful line is a version pragma, and the rest is a sequence of declarations.
The pragma
Every file begins with a pragma that pins the language version. The 3.0 pragma is the canonical form, and the 2.0 pragma is accepted for compatibility.
#!orchard 3.0The pragma uses the shebang spelling so a file can also be marked executable on a system where orch is wired in as the interpreter. Minor version bumps within 3.x enable forward-compatibility warnings; they never change the meaning of code that already checks.
Comments
Orchard has line comments and block comments. Block comments nest, so you can comment out a region that already contains a block comment.
// a line comment runs to the end of the line
/* a block comment
/* may nest */
and continues until the matching close */Automatic statement termination
Statements are terminated automatically, Go-style: a newline ends a statement wherever ending one would be legal. You never write a semicolon. A newline does not terminate a statement in the places where the parser is clearly mid-expression, specifically:
- inside an open
(or[; - immediately after a binary or assignment operator, or after one of
,.->=>?:|>??..; - after an open
{; - before
else,catch,until,in, or a closing)]}.
In practice this means you write code with newlines and the language does the right thing, while still letting you wrap long expressions across lines by breaking after an operator.
Literals
Orchard has a rich set of literals so that durations and money read as themselves in source.
- Integers
- Decimal digits with optional underscores for grouping:
0,42,1_000,1_000_000. The underscores are purely visual. - Floats
- A decimal point or an exponent:
3.14,0.5,1e9,6.022e23. - Strings
- Double quoted with interpolation:
"Hello, {name}". Triple-quoted for multi-line text:"""…""". Raw strings use backticks and perform no interpolation or escaping, which is useful for patterns and snippets. - Booleans
trueandfalse.- Null
null, the single value of thenulltype and the absence value for an optional.- Durations
- Integer-valued spans of time:
30s,15m,2h,1d(seconds, minutes, hours, days). - Money
- A currency amount written with a dollar sign:
$0.50,$5.00,$0.10.
String interpolation and escapes
Inside a double-quoted or triple-quoted string, a single brace begins an interpolation: the expression between the braces is evaluated and inserted. Interpolation uses a single brace and never a dollar sign.
let name = "Ivy"
let greeting = "Hello, {name}! You have {count} new messages."
let report = """
Summary for {topic}:
score = {score}
notes = {notes}
"""To write a literal brace, double it: {{ and }} produce a single { and }. The recognized escape sequences are \n, \t, \r, \", \\, \{, \}, and a Unicode escape \u{1F333} that takes a hex code point. Raw strings written with backticks do none of this; every character is literal.
Types
Orchard is statically typed with local inference. You annotate parameters, return types, and state; you usually let let bindings infer. The checker proves the program well typed before anything runs.
Primitive and built-in types
- str
- A UTF-8 string.
- int
- A signed integer.
- float
- A floating-point number.
- bool
trueorfalse.- null
- The unit type whose only value is
null. - duration
- An integer-valued span of time, written
30s,15m, and so on. - money
- A currency amount, written
$0.50. - bytes
- An opaque sequence of bytes, for binary payloads.
- json
- The dynamic supertype. Any value can be viewed as
json, andjsonis what you reach for when the shape is genuinely unknown at compile time.
Composite types
- list<T>
- An ordered, homogeneous list of
T, for examplelist<str>. - map<K, V>
- A mapping from keys of type
Kto values of typeV, for examplemap<str, int>. - T?
- An optional
T: either aTornull. Reach into an optional safely with?.and supply a fallback with??.
Records and enums
A type declares a record: a named product of typed fields. An enum declares a sum: a closed set of named variants, each of which may optionally carry a payload.
type Ticket {
id: int
title: str
assignee: str?
tags: list<str>
}
enum Severity { low, medium, high, critical }
enum Result {
ok(value: str)
err(code: int, message: str)
}Records and enums are first-class types. They can be used as parameter and return types, stored in state, and produced by gen as T. Because an enum is a closed set, the checker can prove a match over it is exhaustive.
Inference and the JSON lowering
Inference is local: the type of a let follows from its initializer, and you annotate at the boundaries where the type would otherwise be ambiguous. When a type is handed to the model as a schema, money lowers to a "$d.dd" string and duration lowers to a compact string such as "30m", so the model emits and receives them in their readable form.
Top level
At the top level of a file you may write agent, type, enum, fn, and use declarations. A file may contain more than one agent.
- agent
- Declares an agent and its members. This is the unit you run.
- type / enum
- Declares a record or enum shared by every agent in the file. A top-level type or enum is visible to all agents; declare it inside an agent to keep it private.
- fn
- Declares a pure helper function shared by every agent in the file. Like a top-level type, it is visible everywhere in the file.
- use
- Brings in a built-in tool pack (for example
use calculatorexposescalculate) or imports another agent so it can be spawned as a sub-agent.
#!orchard 3.0
enum Severity { low, medium, high, critical } // shared by every agent
fn truncate(s: str, n: int) -> str { // a shared pure helper
return s
}
agent Triage {
model { provider: mock, name: "echo" }
on message(text: str) -> Severity {
return gen as Severity "How severe is: {text}"
}
}Agent members
An agent body is a set of members. Each member has a single, well-defined purpose, and most are optional: a one-line agent needs only a model and one handler.
- model
- The model and its fallback chain:
model { provider, name, temperature?, max_tokens?, api_key?, fallback? }. Theprovideris one ofanthropic,openai,ollama,groq,together,openrouter, ormock. - memory
- The three memory layers:
memory { conversation { enabled, window }, facts, semantic { … } }. Conversation is short-term turn history, facts are free-form durable notes, and semantic is the vector-recall layer. - persona
- The system prompt, assembled from
persona { tone, traits, language, instructions, system_prompt }. This is what a baregensees. - knowledge
- Sources indexed into semantic memory:
knowledge { file: …, text: …, url: … }. They are chunked, embedded, and made available to recall. - policy
- The safety envelope:
policy { max_steps, max_tool_calls, max_spend, allow_shell, allowed_domains, … }. Policy caps what the agent may do; it can only ever tighten. - state
- A typed, persistent, transactional variable:
state name: T = default. Writes commit at the end of a turn and roll back if the turn ends with an uncaught error. - type / enum
- A record or enum private to this agent.
- fn
- A pure helper:
fn name(p: T) -> R { … }. A function may not usegen,delegate, tools, or state; the checker enforces this, so afnis deterministic. - tool
- A deterministic effect the model can call:
tool name(p: T) -> R { … }. Tools perform HTTP, shell, or computation. They are advertised to the delegate loop with their parameter schema. - skill
- A model-using procedure:
skill name(p: T) -> R { … }. A skill may itself callgenand other tools. It is exposed to the loop asskill_<name>. - on start / message / schedule / file
- Event handlers.
on startruns once at session start,on messagehandles an incoming message,on schedulefires on a timer, andon filefires on a file trigger.
agent Assistant {
model { provider: anthropic, name: "claude-opus-4-8" }
memory { conversation { enabled: true, window: 40 }, facts: true }
persona {
tone: "warm"
instructions: "Be concise and accurate."
}
policy { max_steps: 25, max_tool_calls: 100, max_spend: $5.00 }
state turn_count: int = 0
tool word_count(text: str) -> int { return text.split(" ").length }
on message(text: str) -> str {
turn_count += 1
return gen "Reply briefly to: {text}"
}
}Annotations
Annotations attach metadata to a member. They sit on the line before the member they decorate.
- @description("…")
- A human-readable description. For a tool or skill, this is the text the model sees when deciding whether to call it, so it is worth writing well.
- @expose(false)
- Hides a skill or tool from the model. It can still be called from your own code, but the delegate loop will not see it.
- @timeout(30s)
- Bounds how long the decorated member may run before it is cut off.
- @unsafe
- Marks a member that performs a sensitive effect, opting it out of the default guard so it can run where policy would otherwise block it.
@description("Look up the current weather for a city.")
@timeout(10s)
tool get_weather(city: str) -> str {
return fetch(url: "https://api.example.com/weather?q={city}")
}Expressions and precedence
Operators bind from loose to tight in the following order. Lower in the list binds tighter.
or // logical or
and // logical and
== != < <= > >= // comparison
.. ..= // range (exclusive, inclusive)
|> // pipe
+ - // additive
* / % // multiplicative
not - // logical not, unary minus
?? // null-coalescing
. ?. [] () // postfix: member, optional member, index, callThe pipe operator threads a value into the first argument of a call: x |> f(2) is exactly f(x, 2). It reads left to right, which makes data-flow pipelines clear.
let titles = tickets
|> filter((t) => t.open)
|> map((t) => t.title)Calls accept labeled arguments, which name the parameter at the call site and may be given in any order:
let r = get_weather(city: "Wellington")
let results = parallel { weather: get_weather(city: c), news: fetch(url: feed) }Lambdas are written with a parenthesized parameter list, an arrow, and an expression body:
let double = (x) => x * 2
let names = users |> map((u) => u.name)Agent-native primitives
These primitives are what make Orchard an agent language rather than a general-purpose one. They are part of the grammar, type-checked, and given precise runtime semantics.
gen
gen [as T] [with { … }] "prompt" is one model call. With no annotation it returns a str. With as T it constrains the output to the type T: the runtime lowers T to a JSON schema, instructs the model to emit matching JSON, parses and coerces the response, and re-prompts on a mismatch (bounded by policy.max_gen_retries). A run of generation that cannot be coerced raises a catchable GenError. The optional with block tunes the call for this one generation. A bare gen sees only the persona and system prompt, with no history and no tools.
let summary = gen "Summarize in one sentence: {text}"
let label = gen as Severity "How severe is: {report}"
let plan = gen as list<str> with { temperature: 0.2 } "List the steps for: {goal}"delegate
delegate goal [with { tools, budget, max_steps }] hands a goal to the autonomous perceive, think, act loop. The loop runs over the agent's tools and exposed skills, calling them as the model decides, until the model produces a final answer. The optional with block narrows the tools available to this delegation and tightens its budget and step count.
on message(text: str) -> str {
return budget(spend: $0.10, steps: 6) { delegate text }
}retry … until
retry(n) { … } until (v) => … runs the block, then evaluates the predicate against the block's value. If the predicate is false it runs the block again, up to n attempts. The attempt number is available as attempts inside the predicate.
let label = retry(3) {
gen as Severity "Be decisive: {issue}"
} until (r) => r != Severity.low or attempts == 3budget
budget(spend, steps, tool_calls) { … } opens a nested policy scope around a block. A budget can only tighten an enclosing limit, never loosen it: budgets compose by taking the minimum at every level. Under concurrency, all branches inside a single budget draw from one shared pool, and the accounting is atomic and pre-reserved so two concurrent tasks cannot both pass a cap they jointly exceed.
budget(spend: $0.50, steps: 12, tool_calls: 20) {
delegate "Research and summarize the latest release notes."
}spawn, await, parallel
spawn e schedules the expression e to run concurrently and immediately returns a future<T>, where T is the static type of e. await f joins a future, blocking the awaiting task until it resolves and returning its value (or re-raising a throw that happened inside the spawned task). Awaiting the same future twice returns the cached result. parallel { a: e1, b: e2 } is a barrier over named branches: it evaluates every branch concurrently and returns only when all of them have settled, producing a record that maps each label to its branch value in declaration order.
let h = spawn delegate "Research angle: {a}"
let report = await h
let results = parallel {
weather: get_weather(city: c)
news: fetch(url: feed)
}
let hs = angles |> map((a) => spawn delegate "Research: {a}")
let findings = hs |> map(await)Spawned futures are bound to the lexical scope that created them. A future that is never awaited is cancelled when its scope exits, and its pre-reserved budget is refunded. A future may not escape the scope that spawned it: returning or storing one past its owner is a checker error, which makes leaks impossible. If any branch of a parallel throws, the barrier raises the first branch's error by source order once all branches have settled.
match
match subject { pattern => body … } dispatches on the shape of a value. Over an enum it is exhaustive: the checker requires every variant to be handled, either explicitly or with a wildcard _.
match severity {
critical => "page oncall"
high => "assign to triage"
_ => "archive"
}Memory primitives
The free-form fact layer is read and written with these primitives. recall(q) returns the matching facts for a query; recall_one(q) returns the single best match (or null); remember k = v stores a keyed fact and remember v stores an unkeyed one; forget k removes a fact.
remember city = "Wellington"
let where = recall_one("city") ?? "unknown"
let notes = recall("open tickets")
forget "city"reply, emit, halt
reply x produces the final value of a handler and ends it. emit x produces an intermediate output without ending the handler, which a host UI can render as it streams. halt "reason" stops the run with a stated reason.
on message(text: str) -> str {
emit "working on it…"
let answer = delegate text
reply answer
}Statements
The body of a handler, tool, skill, or function is a block of statements. A block is also an expression: its value is the value of its trailing expression, which is what lets a gen sit as the last line of a retry block or a budget scope.
Bindings and assignment
let introduces an immutable binding; var introduces a mutable one. The compound assignment operators are =, +=, -=, *=, and /=.
let limit = 5
var count = 0
count += 1
count *= 2Conditionals
if score > 80 {
reply "high"
} else if score > 40 {
reply "medium"
} else {
reply "low"
}Loops
for … in iterates a list or range; while loops while a condition holds; repeat n runs a block a fixed number of times. break leaves a loop and continue skips to the next iteration.
for ticket in tickets {
if ticket.closed { continue }
emit ticket.title
}
for i in 0..10 { emit "step {i}" }
var tries = 0
while tries < 3 {
tries += 1
}
repeat 5 {
emit "tick"
}Returning and error handling
return ends a function, tool, or skill with a value. throw raises an error, and try / catch handles one. A throw inside a spawned task is captured in its future and re-raised at the matching await.
skill classify(report: str) -> Severity {
try {
return gen as Severity "How severe is: {report}"
} catch err {
return Severity.low
}
}The IR and the toolchain
Orchard source lowers to a stable, diffable JSON intermediate representation. Running orch compile writes that IR, and the interpreter runs the IR rather than the source directly. Because compiling and running share the same lowering, a compiled run and an interpreted run of the same program are identical in observable behavior. The IR is stable enough to commit and diff, so you can see exactly how a source change moves the program the runtime will execute.
orch check agent.orch # parse, type-check, lint, no execution
orch compile -o ir.json agent.orch
orch run -t "hello" agent.orchmodel { provider: mock, name: "echo" } an agent runs entirely offline, which is what makes the examples in the progressive guide runnable with no API key. The same program talks to a real model by changing only the model block to a real provider, name, and key. For the complete command set, see the CLI reference.