Language Reference

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

The 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
true and false.
Null
null, the single value of the null type 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
true or false.
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, and json is what you reach for when the shape is genuinely unknown at compile time.

Composite types

list<T>
An ordered, homogeneous list of T, for example list<str>.
map<K, V>
A mapping from keys of type K to values of type V, for example map<str, int>.
T?
An optional T: either a T or null. 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 calculator exposes calculate) 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? }. The provider is one of anthropic, openai, ollama, groq, together, openrouter, or mock.
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 bare gen sees.
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 use gen, delegate, tools, or state; the checker enforces this, so a fn is 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 call gen and other tools. It is exposed to the loop as skill_<name>.
on start / message / schedule / file
Event handlers. on start runs once at session start, on message handles an incoming message, on schedule fires on a timer, and on file fires 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, call

The 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 == 3

budget

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 *= 2

Conditionals

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.orch
Offline by default. With model { 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.