Harn language specification
Version: tracks the workspace 0.8.x series; derived from the
implementation and updated alongside it. The language is still
pre-1.0 — surface-level breaking changes are possible between minor
releases. See the
changelog for
what changed and when, and the Stability column in subsections below for
per-feature guarantees.
Harn is a pipeline-oriented programming language for orchestrating AI agents. It is implemented as a Rust workspace with a lexer, parser, type checker, tree-walking VM, tree-sitter grammar, and CLI/runtime tooling. Programs consist of named pipelines containing imperative statements, expressions, and calls to registered builtins that perform I/O, LLM calls, and tool execution.
This file is the canonical language specification. The hosted docs page
docs/src/language-spec.md is generated from it by
scripts/sync_language_spec.harn.
Lexical rules
Whitespace
Spaces (' '), tabs ('\t'), and carriage returns ('\r') are insignificant and skipped
between tokens. Newlines ('\n') are significant tokens used as statement separators.
The parser skips newlines between statements but they are preserved in the token stream.
Semicolons (';') are also accepted as optional statement separators in statement-list
contexts (top-level items, block statements, tool bodies, and skill fields), but they
are non-canonical input syntax. harn fmt normalizes them back to newline-separated form.
Backslash line continuation
A backslash (\) immediately before a newline joins the current line with the next.
Both the backslash and the newline are removed from the token stream, so the two
physical lines are treated as a single logical line by the lexer.
let total = 1 + 2 \
+ 3 + 4
// equivalent to: let total = 1 + 2 + 3 + 4
This is useful for breaking long expressions that do not involve a binary operator eligible for multiline continuation (see "Multiline expressions").
Comments
// Line comment: everything until the next newline is ignored.
/* Block comment: can span multiple lines.
/* Nesting is supported. */
Still inside the outer comment. */
Block comments track nesting depth, so /* /* */ */ is valid. An unterminated block comment produces a lexer error.
Keywords
The following identifiers are reserved:
| Keyword | Token |
|---|---|
pipeline | .pipeline |
extends | .extends |
override | .overrideKw |
let | .letKw |
const | .constKw |
var | .varKw |
if | .ifKw |
else | .elseKw |
for | .forKw |
in | .inKw |
match | .matchKw |
retry | .retry |
parallel | .parallel |
defer | .defer |
return | .returnKw |
import | .importKw |
true | .trueKw |
false | .falseKw |
nil | .nilKw |
try | .tryKw |
catch | .catchKw |
throw | .throwKw |
finally | .finally |
fn | .fnKw |
emit | .emit |
spawn | .spawnKw |
while | .whileKw |
type | .typeKw |
enum | .enum |
eval_pack | .evalPack |
struct | .struct |
interface | .interface |
pub | .pub |
from | .from |
to | .to |
tool | .tool |
skill | .skill |
exclusive | .exclusive |
guard | .guard |
require | .require |
deadline | .deadline |
yield | .yield |
mutex | .mutex |
break | .break |
continue | .continue |
select | .select |
impl | .impl |
request_approval | .requestApproval |
dual_control | .dualControl |
ask_user | .askUser |
escalate_to | .escalateTo |
Identifiers
An identifier starts with a letter or underscore, followed by zero or more letters, digits, or underscores:
identifier ::= [a-zA-Z_][a-zA-Z0-9_]*
Number literals
int_literal ::= digit+
float_literal ::= digit+ '.' digit+
A number followed by . where the next character is not a digit is lexed as an integer
followed by the . operator (enabling 42.method).
Duration literals
A duration literal is an integer followed immediately (no whitespace) by a time-unit suffix:
duration_literal ::= digit+ ('ms' | 's' | 'm' | 'h' | 'd' | 'w')
| Suffix | Unit | Equivalent |
|---|---|---|
ms | milliseconds | -- |
s | seconds | 1000 ms |
m | minutes | 60 s |
h | hours | 60 m |
d | days | 24 h |
w | weeks | 7 d |
Duration literals evaluate to an integer number of milliseconds. They can be used anywhere an expression is expected:
sleep(500ms)
deadline 30s { /* ... */ }
let one_day = 1d // 86400000
let two_weeks = 2w // 1209600000
String literals
Single-line strings
string_literal ::= '"' (char | escape | interpolation)* '"'
escape ::= '\' ('n' | 'r' | 't' | '0' | '\\' | '"' | '