Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Builtin functions

Complete reference for all built-in functions available in Harn.

Output

FunctionParametersReturnsDescription
log(msg)msg: anynilPrint with [harn] prefix and newline
print(msg)msg: anynilPrint without prefix or newline
println(msg)msg: anynilPrint with newline, no prefix
progress(phase, message, progress?, total?)phase: string, message: string, optional numeric progressnilEmit standalone progress output. Dict options support mode: "spinner" with step, or mode: "bar" with current, total, and optional width
color(text, name)text: any, name: stringstringWrap text with an ANSI foreground color code
bold(text)text: anystringWrap text with ANSI bold styling
dim(text)text: anystringWrap text with ANSI dim styling

Type conversion

FunctionParametersReturnsDescription
type_of(value)value: anystringReturns type name: "int", "float", "string", "bool", "nil", "list", "dict", "closure", "taskHandle", "duration", "enum", "struct"
to_string(value)value: anystringConvert to string representation
to_int(value)value: anyint or nilParse/convert to integer. Floats truncate, bools become 0/1
to_float(value)value: anyfloat or nilParse/convert to float
unreachable(value?)value: any (optional)neverThrows “unreachable code was reached” at runtime. When the argument is a variable, the type checker verifies it has been narrowed to never (exhaustiveness check)
iter(x)x: list, dict, set, string, generator, channel, or iterIter<T>Lift an iterable source into a lazy, single-pass, fused iterator. No-op on an existing iter. Dict iters yield Pair(key, value); string iters yield chars. See Iterator methods
pair(a, b)a: any, b: anyPairConstruct a two-element Pair value. Access via .first / .second, or destructure in a for-loop: for (k, v) in ...

Runtime shape validation

Function parameters with structural type annotations (shapes) are validated at runtime. If a dict or struct argument is missing a required field or has the wrong field type, a descriptive error is thrown before the function body executes.

fn greet(u: {name: string, age: int}) {
  println("${u.name} is ${u.age}")
}

greet({name: "Alice", age: 30})   // OK
greet({name: "Alice"})            // Error: parameter 'u': missing field 'age' (int)

See Error handling – Runtime shape validation errors for more details.

Result

Harn has a built-in Result type for representing success/failure values without exceptions. Ok and Err create Result.Ok and Result.Err enum variants respectively. When called on a non-Result value, unwrap and unwrap_or pass the value through unchanged.

FunctionParametersReturnsDescription
Ok(value)value: anyResult.OkCreate a Result.Ok value
Err(value)value: anyResult.ErrCreate a Result.Err value
is_ok(result)result: anyboolReturns true if value is Result.Ok
is_err(result)result: anyboolReturns true if value is Result.Err
unwrap(result)result: anyanyExtract Ok value. Throws on Err. Non-Result values pass through
unwrap_or(result, default)result: any, default: anyanyExtract Ok value. Returns default on Err. Non-Result values pass through
unwrap_err(result)result: anyanyExtract Err value. Throws on non-Err

Example:

let good = Ok(42)
let bad = Err("something went wrong")

println(is_ok(good))             // true
println(is_err(bad))             // true

println(unwrap(good))            // 42
println(unwrap_or(bad, 0))       // 0
println(unwrap_err(bad))         // something went wrong

JSON

FunctionParametersReturnsDescription
json_parse(str)str: stringvalueParse JSON string into Harn values. Throws on invalid JSON
json_stringify(value)value: anystringSerialize Harn value to JSON. Closures and handles become null
yaml_parse(str)str: stringvalueParse YAML string into Harn values. Throws on invalid YAML
yaml_stringify(value)value: anystringSerialize Harn value to YAML
toml_parse(str)str: stringvalueParse TOML string into Harn values. Throws on invalid TOML
toml_stringify(value)value: anystringSerialize Harn value to TOML
json_validate(data, schema)data: any, schema: dictboolValidate data against a schema. Returns true if valid, throws with details if not
schema_check(data, schema)data: any, schema: dictResultValidate data against an extended schema and return Result.Ok(data) or Result.Err({message, errors, value?})
schema_parse(data, schema)data: any, schema: dictResultSame as schema_check, but applies default values recursively
schema_is(data, schema)data: any, schema: dictboolValidate data against a schema and return true/false without throwing
schema_expect(data, schema, apply_defaults?)data: any, schema: dict, bool (optional)anyValidate data and return the normalized value, throwing on failure
schema_from_json_schema(schema)schema: dictdictConvert a JSON Schema object into Harn’s canonical schema dict
schema_from_openapi_schema(schema)schema: dictdictConvert an OpenAPI Schema Object into Harn’s canonical schema dict
schema_to_json_schema(schema)schema: dictdictConvert an extended Harn schema into JSON Schema
schema_to_openapi_schema(schema)schema: dictdictConvert an extended Harn schema into an OpenAPI-friendly schema object
schema_extend(base, overrides)base: dict, overrides: dictdictShallow-merge two schema dicts
schema_partial(schema)schema: dictdictRemove required recursively so properties become optional
schema_pick(schema, keys)schema: dict, keys: listdictKeep only selected top-level properties
schema_omit(schema, keys)schema: dict, keys: listdictRemove selected top-level properties
json_extract(text, key?)text: string, key: string (optional)valueExtract JSON from text (strips markdown code fences). If key given, returns that key’s value

Type mapping:

JSONHarn
stringstring
integerint
decimal/exponentfloat
true/falsebool
nullnil
arraylist
objectdict

Canonical schema format

The canonical schema is a plain Harn dict. The validator also accepts compatible JSON Schema / OpenAPI Schema Object spellings such as object, array, integer, number, boolean, oneOf, allOf, minLength, maxLength, minItems, maxItems, and additionalProperties, normalizing them into the same internal form.

Supported canonical keys:

KeyTypeDescription
typestringExpected type: "string", "int", "float", "bool", "list", "dict", "any"
requiredlistList of required key names (for dicts)
propertiesdictDict mapping property names to sub-schemas (for dicts)
itemsdictSchema to validate each item against (for lists)
additional_propertiesbool or dictWhether unknown dict keys are allowed, or which schema they must satisfy

Example:

let schema = {
  type: "dict",
  required: ["name", "age"],
  properties: {
    name: {type: "string"},
    age: {type: "int"},
    tags: {type: "list", items: {type: "string"}}
  }
}
json_validate(data, schema)  // throws if invalid

Extended schema constraints

The schema builtins support these additional keys:

KeyTypeDescription
nullableboolAllow nil
min / maxint or floatNumeric bounds
min_length / max_lengthintString length bounds
patternstringRegex pattern for strings
enumlistAllowed literal values
constanyExact required literal value
min_items / max_itemsintList length bounds
unionlist of schemasValue must match one schema
all_oflist of schemasValue must satisfy every schema
defaultanyDefault value applied by schema_parse

Example:

let user_schema = {
  type: "dict",
  required: ["name", "age"],
  properties: {
    name: {type: "string", min_length: 1},
    age: {type: "int", min: 0},
    role: {type: "string", enum: ["admin", "user"], default: "user"}
  }
}

let parsed = schema_parse({name: "Ada", age: 36}, user_schema)
println(is_ok(parsed))
println(unwrap(parsed).role)
println(schema_to_json_schema(user_schema).type)

schema_is(...) is useful for dynamic checks and can participate in static type refinement when the schema is a literal (or a variable bound from a literal schema).

The lazy std/schema module provides ergonomic builders such as schema_string(), schema_object(...), schema_union(...), get_typed_result(...), get_typed_value(...), and is_type(...).

Composition helpers:

let public_user = schema_pick(user_schema, ["name", "role"])
let patch_schema = schema_partial(user_schema)
let admin_user = schema_extend(user_schema, {
  properties: {
    name: {type: "string", min_length: 1},
    age: {type: "int", min: 0},
    role: {type: "string", enum: ["admin"], default: "admin"}
  }
})

json_extract

Extracts JSON from LLM responses that may contain markdown code fences or surrounding prose. Handles ```json ... ```, ``` ... ```, and bare JSON with surrounding text. Uses balanced bracket matching to correctly extract nested objects and arrays from mixed prose.

let result = llm_call("Return JSON with name and age")
let data = json_extract(result.text)         // parse, stripping fences
let name = json_extract(result.text, "name") // extract just one key

Math

FunctionParametersReturnsDescription
abs(n)n: int or floatint or floatAbsolute value
ceil(n)n: floatintCeiling (rounds up). Ints pass through unchanged
floor(n)n: floatintFloor (rounds down). Ints pass through unchanged
round(n)n: floatintRound to nearest integer. Ints pass through unchanged
sqrt(n)n: int or floatfloatSquare root
pow(base, exp)base: number, exp: numberint or floatExponentiation. Returns int when both args are int and exp is non-negative
min(a, b)a: number, b: numberint or floatMinimum of two values. Returns float if either argument is float
max(a, b)a: number, b: numberint or floatMaximum of two values. Returns float if either argument is float
random()nonefloatRandom float in [0, 1)
random_int(min, max)min: int, max: intintRandom integer in [min, max] inclusive

Trigonometry

FunctionParametersReturnsDescription
sin(n)n: floatfloatSine (radians)
cos(n)n: floatfloatCosine (radians)
tan(n)n: floatfloatTangent (radians)
asin(n)n: floatfloatInverse sine
acos(n)n: floatfloatInverse cosine
atan(n)n: floatfloatInverse tangent
atan2(y, x)y: float, x: floatfloatTwo-argument inverse tangent

Logarithms and exponentials

FunctionParametersReturnsDescription
log2(n)n: floatfloatBase-2 logarithm
log10(n)n: floatfloatBase-10 logarithm
ln(n)n: floatfloatNatural logarithm
exp(n)n: floatfloatEuler’s number raised to the power n

Constants and utilities

FunctionParametersReturnsDescription
pifloatThe constant pi (3.14159…)
efloatEuler’s number (2.71828…)
sign(n)n: int or floatintSign of a number: -1, 0, or 1
is_nan(n)n: floatboolCheck if value is NaN
is_infinite(n)n: floatboolCheck if value is infinite

Sets

FunctionParametersReturnsDescription
set(items?)items: list (optional)setCreate a new set, optionally from a list
set_add(s, value)s: set, value: anysetAdd a value to a set, returns new set
set_remove(s, value)s: set, value: anysetRemove a value from a set, returns new set
set_contains(s, value)s: set, value: anyboolCheck if set contains a value
set_union(a, b)a: set, b: setsetUnion of two sets
set_intersect(a, b)a: set, b: setsetIntersection of two sets
set_difference(a, b)a: set, b: setsetDifference (elements in a but not b)
set_symmetric_difference(a, b)a: set, b: setsetElements in either but not both
set_is_subset(a, b)a: set, b: setboolTrue if all elements of a are in b
set_is_superset(a, b)a: set, b: setboolTrue if a contains all elements of b
set_is_disjoint(a, b)a: set, b: setboolTrue if a and b share no elements
to_list(s)s: setlistConvert a set to a list

Set methods (dot syntax)

Sets also support method syntax: my_set.union(other).

MethodParametersReturnsDescription
.count() / .len()noneintNumber of elements
.empty()noneboolTrue if set is empty
.contains(val)val: anyboolCheck membership
.add(val)val: anysetNew set with val added
.remove(val)val: anysetNew set with val removed
.union(other)other: setsetUnion
.intersect(other)other: setsetIntersection
.difference(other)other: setsetElements in self but not other
.symmetric_difference(other)other: setsetElements in either but not both
.is_subset(other)other: setboolTrue if self is a subset of other
.is_superset(other)other: setboolTrue if self is a superset of other
.is_disjoint(other)other: setboolTrue if no shared elements
.to_list()nonelistConvert to list
.map(fn)fn: closuresetTransform elements (deduplicates)
.filter(fn)fn: closuresetKeep elements matching predicate
.any(fn)fn: closureboolTrue if any element matches
.all(fn) / .every(fn)fn: closureboolTrue if all elements match

String functions

FunctionParametersReturnsDescription
len(value)value: string, list, or dictintLength of string (chars), list (items), or dict (keys)
trim(str)str: stringstringRemove leading and trailing whitespace
lowercase(str)str: stringstringConvert to lowercase
uppercase(str)str: stringstringConvert to uppercase
split(str, sep)str: string, sep: stringlistSplit string by separator
starts_with(str, prefix)str: string, prefix: stringboolCheck if string starts with prefix
ends_with(str, suffix)str: string, suffix: stringboolCheck if string ends with suffix
contains(str, substr)str: string, substr: stringboolCheck if string contains substring. Also works on lists
replace(str, old, new)str: string, old: string, new: stringstringReplace all occurrences
join(list, sep)list: list, sep: stringstringJoin list elements with separator
substring(str, start, len?)str: string, start: int, len: intstringExtract substring from start position
format(template, ...)template: string, args: anystringFormat string with {} placeholders. With a dict as the second arg, supports named {key} placeholders

String methods (dot syntax)

These are called on string values with dot notation: "hello".uppercase().

MethodParametersReturnsDescription
.trim()nonestringRemove leading/trailing whitespace
.trim_start()nonestringRemove leading whitespace only
.trim_end()nonestringRemove trailing whitespace only
.lines()nonelistSplit string by newlines
.char_at(index)index: intstring or nilCharacter at index (nil if out of bounds)
.index_of(substr)substr: stringintFirst character offset of substring (-1 if not found)
.last_index_of(substr)substr: stringintLast character offset of substring (-1 if not found)
.lower() / .to_lower()nonestringLowercase string
.len()noneintCharacter count
.upper() / .to_upper()nonestringUppercase string
.chars()nonelistList of single-character strings
.reverse()nonestringReversed string
.repeat(n)n: intstringRepeat n times
.pad_left(width, char?)width: int, char: stringstringPad to width with char (default space)
.pad_right(width, char?)width: int, char: stringstringPad to width with char (default space)

List methods (dot syntax)

MethodParametersReturnsDescription
.map(fn)fn: closurelistTransform each element
.filter(fn)fn: closurelistKeep elements where fn returns truthy
.reduce(init, fn)init: any, fn: closureanyFold with accumulator
.find(fn)fn: closureany or nilFirst element matching predicate
.find_index(fn)fn: closureintIndex of first match (-1 if not found)
.any(fn)fn: closureboolTrue if any element matches
.all(fn) / .every(fn)fn: closureboolTrue if all elements match
.none(fn?)fn: closureboolTrue if no elements match (no arg: checks emptiness)
.first(n?)n: int (optional)any or listFirst element, or first n elements
.last(n?)n: int (optional)any or listLast element, or last n elements
.partition(fn)fn: closurelistSplit into [[truthy], [falsy]]
.group_by(fn)fn: closuredictGroup into dict keyed by fn result
.sort() / .sort_by(fn)fn: closure (optional)listSort (natural or by key function)
.min() / .max()noneanyMinimum/maximum value
.min_by(fn) / .max_by(fn)fn: closureanyMin/max by key function
.chunk(size)size: intlistSplit into chunks of size
.window(size)size: intlistSliding windows of size
.each_cons(size)size: intlistSliding windows of size
.compact()nonelistRemove nil values
.unique()nonelistRemove duplicates
.flatten()nonelistFlatten one level of nesting
.flat_map(fn)fn: closurelistMap then flatten
.tally()nonedictFrequency count: {value: count}
.zip(other)other: listlistPair elements from two lists
.enumerate()nonelistList of {index, value} dicts
.take(n) / .skip(n)n: intlistFirst/remaining n elements
.sum()noneint or floatSum of numeric values
.join(sep?)sep: stringstringJoin to string
.reverse()nonelistReversed list
.push(item) / .pop()item: anylistNew list with item added/removed (immutable)
.contains(item)item: anyboolCheck if list contains item
.index_of(item)item: anyintIndex of item (-1 if not found)
.slice(start, end?)start: int, end: intlistSlice with negative index support

Iterator methods

Eager list/dict/set/string methods listed above are unchanged — they still return eager collections. Lazy iteration is opt-in via .iter(), which lifts a list, dict, set, string, generator, or channel into an Iter<T> value. Iterators are single-pass, fused, and snapshot — they Rc-clone the backing collection, so mutating the source after .iter() does not affect the iter.

On a dict, .iter() yields Pair(key, value) values (use .first / .second, or destructure in a for-loop). String iteration yields chars (Unicode scalar values).

Printing with log(it) renders <iter> or <iter (exhausted)> and does not drain the iterator.

Lazy combinators (return a new Iter)

MethodParametersReturnsDescription
.iter()noneIter<T>Lift a source into an iter; no-op on an existing iter
.map(fn)fn: closureIter<U>Lazily transform each item
.filter(fn)fn: closureIter<T>Lazily keep items where fn returns truthy
.flat_map(fn)fn: closureIter<U>Map then flatten, lazily
.take(n)n: intIter<T>First n items
.skip(n)n: intIter<T>Drop first n items
.take_while(fn)fn: closureIter<T>Items until predicate first returns falsy
.skip_while(fn)fn: closureIter<T>Drop items while predicate is truthy
.zip(other)other: iterIter<Pair<T, U>>Pair items from two iters, stops at shorter
.enumerate()noneIter<Pair<int, T>>Pair each item with a 0-based index
.chain(other)other: iterIter<T>Yield items from self, then from other
.chunks(n)n: intIter<list<T>>Non-overlapping fixed-size chunks
.windows(n)n: intIter<list<T>>Sliding windows of size n

Sinks (drain the iter, return an eager value)

MethodParametersReturnsDescription
.to_list()nonelistCollect all items into a list
.to_set()nonesetCollect all items into a set
.to_dict()nonedictCollect Pair(key, value) items into a dict
.count()noneintCount remaining items
.sum()noneint or floatSum of numeric items
.min() / .max()noneanyMin/max item
.reduce(init, fn)init: any, fn: closureanyFold with accumulator
.first() / .last()noneany or nilFirst/last item
.any(fn)fn: closureboolTrue if any remaining item matches
.all(fn)fn: closureboolTrue if all remaining items match
.find(fn)fn: closureany or nilFirst item matching predicate
.for_each(fn)fn: closurenilInvoke fn on each remaining item

Path functions

FunctionParametersReturnsDescription
dirname(path)path: stringstringDirectory component of path
basename(path)path: stringstringFile name component of path
extname(path)path: stringstringFile extension including dot (e.g., .harn)
path_join(parts...)parts: stringsstringJoin path components
path_workspace_info(path, workspace_root?)path: string, workspace_root?: stringdictClassify a path as workspace_relative, host_absolute, or invalid, and project both workspace-relative and host-absolute forms when known
path_workspace_normalize(path, workspace_root?)path: string, workspace_root?: stringstring or nilNormalize a path into workspace-relative form when it is safely inside the workspace (including common leading-slash drift like /packages/...)

File I/O

FunctionParametersReturnsDescription
read_file(path)path: stringstringRead entire file as UTF-8 string. Throws on failure. Deprecated in favor of read_file_result for new code; the throwing form remains supported.
read_file_result(path)path: stringResult<string, string>Non-throwing read: returns Result.Ok(content) on success or Result.Err(message) on failure. Shares read_file’s content cache
write_file(path, content)path: string, content: stringnilWrite string to file. Throws on failure
append_file(path, content)path: string, content: stringnilAppend string to file, creating it if it doesn’t exist. Throws on failure
copy_file(src, dst)src: string, dst: stringnilCopy a file. Throws on failure
delete_file(path)path: stringnilDelete a file or directory (recursive). Throws on failure
file_exists(path)path: stringboolCheck if a file or directory exists
list_dir(path?)path: string (default ".")listList directory contents as sorted list of file names. Throws on failure
mkdir(path)path: stringnilCreate directory and all parent directories. Throws on failure
stat(path)path: stringdictFile metadata: {size, is_file, is_dir, readonly, modified}. Throws on failure
temp_dir()nonestringSystem temporary directory path
render(path, bindings?)path: string, bindings: dictstringRead a template file relative to the current module’s asset root and render it. The template language supports {{ name }} interpolation (with nested paths and filters), {{ if }} / {{ elif }} / {{ else }} / {{ end }}, {{ for item in xs }} ... {{ end }} (with {{ loop.index }} etc.), {{ include "..." }} partials, {{# comments #}}, {{ raw }} ... {{ endraw }} verbatim blocks, and {{- -}} whitespace trim markers. See the Prompt templating reference for the full grammar and filter list. When called from an imported module, resolves relative to that module’s directory, not the entry pipeline. Without bindings, just reads the file
render_prompt(path, bindings?)path: string, bindings: dictstringPrompt-oriented alias of render(...). Use this for .harn.prompt / .prompt assets when you want the asset to be surfaced explicitly in bundle manifests and preflight output

Environment and system

FunctionParametersReturnsDescription
env(name)name: stringstring or nilRead environment variable
env_or(name, default)name: string, default: anystring or defaultRead environment variable, or return default when unset. One-line replacement for the common let v = env(K); if v { v } else { default } pattern
timestamp()nonefloatUnix timestamp in seconds with sub-second precision
elapsed()noneintMilliseconds since VM startup
exec(cmd, args...)cmd: string, args: stringsdictExecute external command. Returns {stdout, stderr, status, success}
exec_at(dir, cmd, args...)dir: string, cmd: string, args: stringsdictExecute external command inside a specific directory
shell(cmd)cmd: stringdictExecute command via shell. Returns {stdout, stderr, status, success}
shell_at(dir, cmd)dir: string, cmd: stringdictExecute shell command inside a specific directory
exit(code)code: int (default 0)neverTerminate the process
username()nonestringCurrent OS username
hostname()nonestringMachine hostname
platform()nonestringOS name: "darwin", "linux", or "windows"
arch()nonestringCPU architecture (e.g., "aarch64", "x86_64")
uuid()nonestringGenerate a random v4 UUID
home_dir()nonestringUser’s home directory path
pid()noneintCurrent process ID
cwd()nonestringCurrent working directory
execution_root()nonestringDirectory used for source-relative execution helpers such as exec_at(...) / shell_at(...)
asset_root()nonestringDirectory used for source-relative asset helpers such as render(...) / render_prompt(...)
source_dir()nonestringDirectory of the currently-executing .harn file (falls back to cwd)
project_root()nonestring or nilNearest ancestor directory containing harn.toml
runtime_paths()nonedictResolved runtime path model: {execution_root, asset_root, state_root, run_root, worktree_root}
date_iso()nonestringCurrent UTC time in ISO 8601 format (e.g., "2026-03-29T14:30:00.123Z")

Regular expressions

FunctionParametersReturnsDescription
regex_match(pattern, text)pattern: string, text: stringlist or nilFind all non-overlapping matches. Returns nil if no matches
regex_replace(pattern, replacement, text)pattern: string, replacement: string, text: stringstringReplace all matches. Throws on invalid regex
regex_captures(pattern, text)pattern: string, text: stringlistFind all matches with capture group details

regex_captures

Returns a list of dicts, one per match. Each dict contains:

  • match – the full matched string
  • groups – a list of positional capture group values (from (...))
  • Named capture groups (from (?P<name>...)) appear as additional keys
let results = regex_captures("(\\w+)@(\\w+)", "alice@example bob@test")
// [
//   {match: "alice@example", groups: ["alice", "example"]},
//   {match: "bob@test", groups: ["bob", "test"]}
// ]

Named capture groups are added as top-level keys on each result dict:

let named = regex_captures("(?P<user>\\w+):(?P<role>\\w+)", "alice:admin")
// [{match: "alice:admin", groups: ["alice", "admin"], user: "alice", role: "admin"}]

Returns an empty list if there are no matches. Throws on invalid regex.

Encoding

FunctionParametersReturnsDescription
base64_encode(string)string: stringstringBase64 encode a string (standard alphabet with padding)
base64_decode(string)string: stringstringBase64 decode a string. Throws on invalid input
url_encode(string)string: stringstringURL percent-encode a string. Unreserved characters (alphanumeric, -, _, ., ~) pass through unchanged
url_decode(string)string: stringstringDecode a URL-encoded string. Decodes %XX sequences and + as space

Example:

let encoded = base64_encode("Hello, World!")
println(encoded)                  // SGVsbG8sIFdvcmxkIQ==
println(base64_decode(encoded))   // Hello, World!
println(url_encode("hello world"))         // hello%20world
println(url_decode("hello%20world"))       // hello world
println(url_encode("a=1&b=2"))             // a%3D1%26b%3D2
println(url_decode("hello+world"))         // hello world

Hashing

FunctionParametersReturnsDescription
sha256(string)string: stringstringSHA-256 hash, returned as a lowercase hex-encoded string
md5(string)string: stringstringMD5 hash, returned as a lowercase hex-encoded string

Example:

println(sha256("hello"))  // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
println(md5("hello"))     // 5d41402abc4b2a76b9719d911017c592

Date/Time

FunctionParametersReturnsDescription
date_now()nonedictCurrent UTC datetime as dict with year, month, day, hour, minute, second, weekday, and timestamp fields
date_parse(str)str: stringfloatParse a datetime string (e.g., "2024-01-15 10:30:00") into a Unix timestamp. Extracts numeric components from the string. Throws if fewer than 3 parts (year, month, day). Validates month (1-12), day (1-31), hour (0-23), minute (0-59), second (0-59)
date_format(dt, format?)dt: float, int, or dict; format: string (default "%Y-%m-%d %H:%M:%S")stringFormat a timestamp or date dict as a string. Supports %Y, %m, %d, %H, %M, %S placeholders. Throws for negative timestamps

Testing

FunctionParametersReturnsDescription
assert(condition, msg?)condition: any, msg: string (optional)nilAssert value is truthy. Throws with message on failure
assert_eq(a, b, msg?)a: any, b: any, msg: string (optional)nilAssert two values are equal. Throws with message on failure
assert_ne(a, b, msg?)a: any, b: any, msg: string (optional)nilAssert two values are not equal. Throws with message on failure

HTTP

FunctionParametersReturnsDescription
http_get(url, options?)url: string, options: dictdictGET request
http_post(url, body, options?)url: string, body: string, options: dictdictPOST request
http_put(url, body, options?)url: string, body: string, options: dictdictPUT request
http_patch(url, body, options?)url: string, body: string, options: dictdictPATCH request
http_delete(url, options?)url: string, options: dictdictDELETE request
http_request(method, url, options?)method: string, url: string, options: dictdictGeneric HTTP request

All HTTP functions return {status: int, headers: dict, body: string, ok: bool}. Options: timeout (ms), retries, backoff (ms), headers (dict), auth (string or {bearer: "token"} or {basic: {user, password}}), follow_redirects (bool), max_redirects (int), body (string). Throws on network errors.

Mock HTTP

For testing pipelines that make HTTP calls without hitting real servers.

FunctionParametersReturnsDescription
http_mock(method, url_pattern, response)method: string, url_pattern: string, response: dictnilRegister a mock. Use * in url_pattern for glob matching (supports multiple * wildcards, e.g., https://api.example.com/*/items/*)
http_mock_clear()nonenilClear all mocks and recorded calls
http_mock_calls()nonelistReturn list of {method, url, body} for all intercepted calls
http_mock("GET", "https://api.example.com/users", {
  status: 200,
  body: "{\"users\": [\"alice\"]}",
  headers: {}
})
let resp = http_get("https://api.example.com/users")
assert_eq(resp.status, 200)

Interactive input

FunctionParametersReturnsDescription
prompt_user(msg)msg: string (optional)stringDisplay message, read line from stdin

Host interop

FunctionParametersReturnsDescription
host_call(name, args)name: string, args: anyanyCall a host capability operation using capability.operation naming
host_capabilities()dictTyped host capability manifest
host_has(capability, op?)capability: string, op: stringboolCheck whether a typed host capability/operation exists
host_tool_list()listList host-exposed bridge tools as {name, description, schema, deprecated}
host_tool_call(name, args)name: string, args: anyanyInvoke a bridge-exposed host tool by name using the existing builtin_call path
host_mock(capability, op, response_or_config, params?)capability: string, op: string, response_or_config: any or dict, params: dictnilRegister a runtime mock for a typed host operation
host_mock_clear()nilClear registered typed host mocks and recorded mock invocations
host_mock_calls()listReturn recorded typed host mock invocations

host_capabilities() returns the capability manifest surfaced by the active host bridge. The local runtime exposes generic process, template, and interaction capabilities. Product hosts can add capabilities such as workspace, project, runtime, editor, git, or diagnostics.

Prefer host_call("capability.operation", args) in shared wrappers and host-owned .harn modules so capability names stay consistent across the runtime, host manifest, and preflight validation.

host_tool_list() is the discovery surface for host-native tools such as Read, Edit, Bash, or IDE actions exposed by the active bridge host. Without a bridge it returns []. host_tool_call(name, args) uses that same bridge host’s existing dynamic builtin dispatch path, so scripts can discover a tool at runtime and then call it by name without hard-coding it into the initial prompt. Import std/host when you want small helpers such as host_tool_lookup(name) or host_tool_available(name).

host_mock(...) is intended for tests and local conformance runs. The third argument may be either a direct result value or a config dict containing result, params, and/or error. Mock matching is last-write-wins and only requires the declared params subset to match the actual host call call. Matched calls are recorded in host_mock_calls() as {capability, operation, params} dictionaries.

For higher-level test helpers, import std/testing:

import {
  assert_host_called,
  clear_host_mocks,
  mock_host_error,
  mock_host_result,
} from "std/testing"

clear_host_mocks()
mock_host_result("project", "metadata_get", "hello", {dir: ".", namespace: "facts"})
assert_eq(host_call("project.metadata_get", {dir: ".", namespace: "facts"}), "hello")
assert_host_called("project", "metadata_get", {dir: ".", namespace: "facts"}, nil)

mock_host_error("project", "scan", "scan failed", nil)
let result = try { host_call("project.scan", {}) }
assert(is_err(result))

Async and timing

FunctionParametersReturnsDescription
sleep(duration)duration: int (ms) or duration literalnilPause execution

Concurrency primitives

Channels

FunctionParametersReturnsDescription
channel(name?)name: string (default "default")dictCreate a channel with name, type, and messages fields
send(ch, value)ch: dict, value: anynilSend a value to a channel
receive(ch)ch: dictanyReceive a value from a channel (blocks until data available)
close_channel(ch)ch: channelnilClose a channel, preventing further sends
try_receive(ch)ch: channelany or nilNon-blocking receive. Returns nil if no data available
select(ch1, ch2, ...)channels: channeldict or nilWait for data on any channel. Returns {index, value, channel} for the first ready channel, or nil if all closed

Atomics

FunctionParametersReturnsDescription
atomic(initial?)initial: any (default 0)dictCreate an atomic value
atomic_get(a)a: dictanyRead the current value
atomic_set(a, value)a: dict, value: anyintSet value, returns previous value
atomic_add(a, delta)a: dict, delta: intintAdd delta, returns previous value
atomic_cas(a, expected, new)a: dict, expected: int, new: intboolCompare-and-swap. Returns true if the swap succeeded

Persistent store

FunctionParametersReturnsDescription
store_get(key)key: stringanyRetrieve value from store, nil if missing
store_set(key, value)key: string, value: anynilStore value, auto-saves to .harn/store.json
store_delete(key)key: stringnilRemove key from store
store_list()nonelistList all keys (sorted)
store_save()nonenilExplicitly flush store to disk
store_clear()nonenilRemove all keys from store

The store is backed by .harn/store.json relative to the script’s directory. The file is created lazily on first store_set. In bridge mode, the host can override these builtins.

LLM

See LLM calls and agent loops for full documentation.

FunctionParametersReturnsDescription
llm_call(prompt, system?, options?)prompt: string, system: string, options: dictdictSingle LLM request. Returns {text, model, input_tokens, output_tokens}. Throws on transport / rate-limit / schema-validation failures
llm_call_safe(prompt, system?, options?)prompt: string, system: string, options: dictdictNon-throwing envelope around llm_call. Returns {ok: bool, response: dict or nil, error: {category, message} or nil}. error.category is one of ErrorCategory’s canonical strings ("rate_limit", "timeout", "overloaded", "server_error", "transient_network", "schema_validation", "auth", "not_found", "circuit_open", "tool_error", "tool_rejected", "cancelled", "generic")
with_rate_limit(provider, fn, options?)provider: string, fn: closure, options: dictwhatever fn returnsAcquire a permit from the provider’s sliding-window rate limiter, invoke fn, and retry with exponential backoff on retryable errors (rate_limit, overloaded, transient_network, timeout). Options: max_retries (default 5), backoff_ms (default 1000, capped at 30s after doubling)
llm_completion(prefix, suffix?, system?, options?)prefix: string, suffix: string, system: string, options: dictdictText completion / fill-in-the-middle request. Returns {text, model, input_tokens, output_tokens}
agent_loop(prompt, system?, options?)prompt: string, system: string, options: dictdictMulti-turn agent loop with ##DONE## sentinel, daemon/idling support, and optional per-turn context filtering. Returns {status, text, iterations, duration_ms, tools_used}
daemon_spawn(config)config: dictdictStart a daemon-mode agent and return a daemon handle with persisted state + queue metadata
daemon_trigger(handle, event)handle: dict or string, event: anydictEnqueue a durable FIFO trigger event for a running daemon; throws VmError::DaemonQueueFull on overflow
daemon_snapshot(handle)handle: dict or stringdictReturn the latest daemon snapshot plus live queue state (pending_events, inflight_event, counts, capacity)
daemon_stop(handle)handle: dict or stringdictStop a daemon and preserve queued trigger state for resume
daemon_resume(path)path: stringdictResume a daemon from its persisted state directory
trigger_list()listReturn the live trigger registry snapshot as list<TriggerBinding>
trigger_register(config)config: dictdictDynamically register a trigger and return its TriggerHandle
trigger_fire(handle, event)handle: dict or string, event: dictdictFire a synthetic event into a trigger and return a DispatchHandle; execution routes through the trigger dispatcher
trigger_replay(event_id)event_id: stringdictFetch a historical event from triggers.events, re-dispatch it through the trigger dispatcher, and thread replay_of_event_id through the returned DispatchHandle
trigger_inspect_dlq()listReturn the current DLQ snapshot as list<DlqEntry> with retry history
trigger_test_harness(fixture)fixture: string or {fixture: string}dictRun a named trigger-system harness fixture and return a structured report. Intended for Rust/unit/conformance coverage of cron, webhook, retry, DLQ, dedupe, rate-limit, cost-guard, recovery, and dead-man-switch scenarios
llm_info()dictCurrent LLM config: {provider, model, api_key_set}
llm_usage()dictCumulative usage: {input_tokens, output_tokens, total_duration_ms, call_count, total_calls}
llm_resolve_model(alias)alias: stringdictResolve model alias to {id, provider} via providers.toml
llm_pick_model(target, options?)target: string, options: dictdictResolve a model alias or tier to {id, provider, tier}
llm_infer_provider(model_id)model_id: stringstringInfer provider from model ID (e.g. "claude-*""anthropic")
llm_model_tier(model_id)model_id: stringstringGet capability tier: "small", "mid", or "frontier"
llm_healthcheck(provider?)provider: stringdictValidate API key. Returns {valid, message, metadata}
llm_rate_limit(provider, options?)provider: string, options: dictint/nil/boolSet ({rpm: N}), query, or clear ({rpm: 0}) per-provider rate limit
llm_providers()listList all configured provider names
llm_config(provider?)provider: stringdictGet provider config (base_url, auth_style, etc.)
llm_cost(model, input_tokens, output_tokens)model: string, input_tokens: int, output_tokens: intfloatEstimate USD cost from embedded pricing table
llm_session_cost()dictSession totals: {total_cost, input_tokens, output_tokens, call_count}
llm_budget(max_cost)max_cost: floatnilSet session budget in USD. LLM calls throw if exceeded
llm_budget_remaining()float or nilRemaining budget (nil if no budget set)
llm_mock(response)response: dictnilQueue a mock LLM response. Dict supports text, tool_calls, match (glob), consume_match (consume a matched pattern instead of reusing it), input_tokens, output_tokens, thinking, stop_reason, model, error: {category, message} (short-circuits the call and surfaces as VmError::CategorizedError — useful for testing llm_call_safe envelopes and with_rate_limit retry loops)
llm_mock_calls()listReturn list of {messages, system, tools} for all calls made to the mock provider
llm_mock_clear()nilClear all queued mock responses and recorded calls

FIFO mocks (no match field) are consumed in order. Pattern-matched mocks (with match) are checked in declaration order against the request transcript text using glob patterns. They persist by default; add consume_match: true to advance through matching fixtures step by step. When no mocks match, the default deterministic mock behavior is used.

See Trigger stdlib for the typed std/triggers aliases, DLQ entry shapes, and the current shallow-path replay / manual-fire caveats.

// Queue specific responses for the mock provider
llm_mock({text: "The answer is 42."})
llm_mock({
  text: "Let me check that.",
  tool_calls: [{name: "read_file", arguments: {path: "main.rs"}}],
})
let r = llm_call("question", nil, {provider: "mock"})
assert_eq(r.text, "The answer is 42.")

// Pattern-matched mocks (reusable, not consumed)
llm_mock({text: "Hello!", match: "*greeting*"})
llm_mock({text: "step 1", match: "*planner*", consume_match: true})
llm_mock({text: "step 2", match: "*planner*", consume_match: true})

// Error injection for testing resilient code paths. The mock
// surfaces as a real `VmError::CategorizedError`, so `error_category`,
// `try { ... } catch`, `llm_call_safe`, and `with_rate_limit` all see
// it the same way they would a live provider failure.
llm_mock({error: {category: "rate_limit", message: "429 Too Many Requests"}})

// Inspect what was sent
let calls = llm_mock_calls()
llm_mock_clear()

Transcript helpers

FunctionParametersReturnsDescription
transcript(metadata?)metadata: dictdictCreate a new transcript
transcript_from_messages(messages_or_transcript)list or dictdictNormalize a message list into a transcript
transcript_messages(transcript)transcript: dictlistGet transcript messages
transcript_summary(transcript)transcript: dictstring or nilGet transcript summary
transcript_id(transcript)transcript: dictstringGet transcript id
transcript_export(transcript)transcript: dictstringExport transcript as JSON
transcript_import(json_text)json_text: stringdictImport transcript JSON
transcript_fork(transcript, options?)transcript: dict, options: dictdictFork transcript, optionally dropping messages or summary
transcript_summarize(transcript, options?)transcript: dict, options: dictdictSummarize and compact a transcript via llm_call
transcript_compact(transcript, options?)transcript: dict, options: dictdictCompact a transcript with the runtime compaction engine, preserving durable artifacts and compaction events
transcript_auto_compact(messages, options?)messages: list, options: dictlistApply the agent-loop compaction pipeline to a message list using llm, truncate, or custom strategy

Provider configuration

LLM provider endpoints, model aliases, inference rules, and default parameters are configured via a TOML file. The VM searches for config in this order:

  1. Built-in defaults (Anthropic, OpenAI, OpenRouter, HuggingFace, Ollama, Local)
  2. HARN_PROVIDERS_CONFIG if set, otherwise ~/.config/harn/providers.toml
  3. Installed package [llm] tables in .harn/packages/*/harn.toml
  4. The nearest project harn.toml [llm] table

The [llm] section uses the same schema as providers.toml, so project and package manifests can ship provider adapters declaratively:

[llm.providers.anthropic]
base_url = "https://api.anthropic.com/v1"
auth_style = "header"
auth_header = "x-api-key"
auth_env = "ANTHROPIC_API_KEY"
chat_endpoint = "/messages"

[llm.providers.local]
base_url = "http://localhost:8000"
base_url_env = "LOCAL_LLM_BASE_URL"
auth_style = "none"
chat_endpoint = "/v1/chat/completions"
completion_endpoint = "/v1/completions"

[llm.aliases]
sonnet = { id = "claude-sonnet-4-20250514", provider = "anthropic" }

[[llm.inference_rules]]
pattern = "claude-*"
provider = "anthropic"

[[llm.tier_rules]]
pattern = "claude-*"
tier = "frontier"

[llm.model_defaults."qwen/*"]
temperature = 0.3

Timers

FunctionParametersReturnsDescription
timer_start(name?)name: stringdictStart a named timer
timer_end(timer)timer: dictintStop timer, prints elapsed, returns milliseconds
elapsed()intMilliseconds since process start

Circuit breakers

Protect against cascading failures by tracking error counts and opening a circuit when a threshold is reached.

FunctionParametersReturnsDescription
circuit_breaker(name, threshold?, reset_ms?)name: string, threshold: int (default 5), reset_ms: int (default 30000)stringCreate a named circuit breaker. Returns the name
circuit_check(name)name: stringstringCheck state: "closed", "open", or "half_open" (after reset period)
circuit_record_failure(name)name: stringboolRecord a failure. Returns true if the circuit just opened
circuit_record_success(name)name: stringnilRecord a success, resetting failure count and closing the circuit
circuit_reset(name)name: stringnilManually reset the circuit to closed

Example:

circuit_breaker("api", 3, 10000)

for i in 0 to 5 exclusive {
  if circuit_check("api") == "open" {
    println("circuit open, skipping call")
  } else {
    try {
      let resp = http_get("https://api.example.com/data")
      circuit_record_success("api")
    } catch e {
      circuit_record_failure("api")
    }
  }
}

Tracing

Distributed tracing primitives for instrumenting pipeline execution.

FunctionParametersReturnsDescription
trace_start(name)name: stringdictStart a trace span. Returns a span dict with trace_id, span_id, name, start_ms
trace_end(span)span: dictnilEnd a span and emit a structured log line with duration
trace_id()nonestring or nilCurrent trace ID from the span stack, or nil if no active span
enable_tracing(enabled?)enabled: bool (default true)nilEnable or disable pipeline-level tracing
trace_spans()nonelistPeek at recorded trace spans
trace_summary()nonestringFormatted summary of trace spans

Example:

let span = trace_start("fetch_data")
// ... do work ...
trace_end(span)

println(trace_summary())

Agent trace events

Fine-grained agent loop trace events for observability and debugging. Events are collected during agent_loop execution and can be inspected after the loop completes.

FunctionParametersReturnsDescription
agent_trace()nonelistPeek at collected agent trace events. Each event is a dict with a type field (llm_call, tool_execution, tool_rejected, loop_intervention, context_compaction, phase_change, loop_complete) and type-specific fields
agent_trace_summary()nonedictRolled-up summary of agent trace events with aggregated token counts, durations, tool usage, and iteration counts

Example:

let result = agent_loop("summarize this file", tools: [read_file])
let summary = agent_trace_summary()
println("LLM calls: " + str(summary.llm_calls))
println("Tools used: " + str(summary.tools_used))

Error classification

Structured error throwing and classification for retry logic and error handling.

FunctionParametersReturnsDescription
throw_error(message, category?)message: string, category: stringneverThrow a categorized error. The error is a dict with message and category fields
error_category(err)err: anystringExtract category from a caught error. Returns "timeout", "auth", "rate_limit", "tool_error", "cancelled", "not_found", "circuit_open", or "generic"
is_timeout(err)err: anyboolCheck if error is a timeout
is_rate_limited(err)err: anyboolCheck if error is a rate limit

Example:

try {
  throw_error("request timed out", "timeout")
} catch e {
  if is_timeout(e) {
    println("will retry after backoff")
  }
  println(error_category(e))  // "timeout"
}

Tool registry (low-level)

Low-level tool management functions for building and inspecting tool registries programmatically. For MCP serving, see the tool_define / mcp_tools API above.

FunctionParametersReturnsDescription
tool_remove(registry, name)registry, name: stringdictRemove a tool by name
tool_list(registry)registry: dictlistList tools as [{name, description, parameters}]
tool_find(registry, name)registry, name: stringdict or nilFind a tool entry by name
tool_select(registry, names)registry: dict, names: listdictReturn a registry containing only the named tools
tool_count(registry)registry: dictintNumber of tools in the registry
tool_describe(registry)registry: dictstringHuman-readable summary of all tools
tool_schema(registry, components?)registry, components: dictdictGenerate JSON Schema for all tools
tool_prompt(registry)registry: dictstringGenerate an LLM system prompt describing available tools
tool_parse_call(text)text: stringlistParse <tool_call>...</tool_call> XML from LLM output
tool_format_result(name, result)name, result: stringstringFormat a <tool_result> XML envelope

Structured logging

FunctionParametersReturnsDescription
log_json(key, value)key: string, value: anynilEmit a JSON log line with timestamp

Metadata

Project metadata store backed by host-managed sharded JSON files. Supports hierarchical namespace resolution (child directories inherit from parents). The default filesystem backend persists namespace shards under .harn/metadata/<namespace>/entries.json and still reads the legacy monolithic root.json shard.

FunctionParametersReturnsDescription
metadata_get(dir, namespace?)dir: string, namespace: stringdict | nilRead metadata with inheritance
metadata_resolve(dir, namespace?)dir: string, namespace: stringdict | nilRead resolved metadata while preserving namespaces
metadata_entries(namespace?)namespace: stringlistList stored directories with local and resolved metadata
metadata_set(dir, namespace, data)dir: string, namespace: string, data: dictnilWrite metadata for directory/namespace
metadata_save()nilFlush metadata to disk
metadata_stale(project)project: stringdictCheck staleness: {any_stale, tier1, tier2}
metadata_status(namespace?)namespace: stringdictSummarize directory counts, namespaces, missing hashes, and stale state
metadata_refresh_hashes()nilRecompute content hashes
compute_content_hash(dir)dir: stringstringHash of directory contents
invalidate_facts(dir)dir: stringnilMark cached facts as stale
scan_directory(path?, pattern_or_options?, options?)path: string, pattern: string or options: dictlistEnumerate files and directories with optional pattern, max_depth, include_hidden, include_dirs, include_files

MCP (Model Context Protocol)

Connect to external tool servers using the Model Context Protocol. Harn supports stdio transport (spawns a child process) and HTTP transport for remote MCP servers.

FunctionParametersReturnsDescription
mcp_connect(command, args?)command: string, args: listmcp_clientSpawn an MCP server and perform the initialize handshake
mcp_list_tools(client)client: mcp_clientlistList available tools from the server
mcp_call(client, name, arguments?)client: mcp_client, name: string, arguments: dictstring or listCall a tool and return the result
mcp_list_resources(client)client: mcp_clientlistList available resources from the server
mcp_list_resource_templates(client)client: mcp_clientlistList resource templates (URI templates) from the server
mcp_read_resource(client, uri)client: mcp_client, uri: stringstring or listRead a resource by URI
mcp_list_prompts(client)client: mcp_clientlistList available prompts from the server
mcp_get_prompt(client, name, arguments?)client: mcp_client, name: string, arguments: dictdictGet a prompt with optional arguments
mcp_server_info(client)client: mcp_clientdictGet connection info (name, connected)
mcp_disconnect(client)client: mcp_clientnilKill the server process and release resources

Example:

let client = mcp_connect("npx", ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])
let tools = mcp_list_tools(client)
println(tools)

let result = mcp_call(client, "read_file", {"path": "/tmp/hello.txt"})
println(result)

mcp_disconnect(client)

Notes:

  • mcp_call returns a string when the tool produces a single text block, a list of content dicts for multi-block results, or nil when empty.
  • If the tool reports isError: true, mcp_call throws the error text.
  • mcp_connect throws if the command cannot be spawned or the initialize handshake fails.

Auto-connecting MCP servers via harn.toml

Instead of calling mcp_connect manually, you can declare MCP servers in harn.toml. They will be connected automatically before the pipeline executes and made available through the global mcp dict.

Add a [[mcp]] entry for each server:

[[mcp]]
name = "filesystem"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]

[[mcp]]
name = "github"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]

Each entry requires:

FieldTypeDescription
namestringIdentifier used to access the client (e.g., mcp.filesystem)
commandstringExecutable to spawn for stdio transports
argslist of stringsCommand-line arguments for stdio transports (default: empty)
transportstringstdio (default) or http
urlstringRemote MCP server URL for HTTP transports
auth_tokenstringOptional explicit bearer token for HTTP transports
client_idstringOptional pre-registered OAuth client ID for HTTP transports
client_secretstringOptional pre-registered OAuth client secret
scopesstringOptional OAuth scope string for login/consent
protocol_versionstringOptional MCP protocol version override

The connected clients are available as properties on the mcp global dict:

pipeline default() {
  let tools = mcp_list_tools(mcp.filesystem)
  println(tools)

  let result = mcp_call(mcp.github, "list_issues", {repo: "harn"})
  println(result)
}

If a server fails to connect, a warning is printed to stderr and that server is omitted from the mcp dict. Other servers still connect normally. The mcp global is only defined when at least one server connects successfully.

For HTTP MCP servers, use the CLI to establish OAuth once and let Harn reuse the stored token automatically:

harn mcp redirect-uri
harn mcp login notion

MCP server mode

Harn pipelines can expose tools, resources, resource templates, and prompts as an MCP server using harn mcp-serve. The CLI serves them over stdio using the MCP protocol, making them callable by Claude Desktop, Cursor, or any MCP client.

Declarative syntax (preferred):

tool greet(name: string) -> string {
  description "Greet someone by name"
  "Hello, " + name + "!"
}

The tool keyword declares a tool with typed parameters, an optional description, and a body. Parameter types map to JSON Schema (string -> "string", int -> "integer", float -> "number", bool -> "boolean"). Parameters with default values are emitted as optional schema fields (required: false) and carry their default value into the generated tool registry entry. Each tool declaration produces its own tool registry dict.

Programmatic API:

FunctionParametersReturnsDescription
tool_registry()dictCreate an empty tool registry
tool_define(registry, name, desc, config)registry, name, desc: string, config: dictdictAdd a tool (config: {parameters, handler, returns?, annotations?, ...})
mcp_tools(registry)registry: dictnilRegister tools for MCP serving
mcp_resource(config)config: dictnilRegister a static resource ({uri, name, text, description?, mime_type?})
mcp_resource_template(config)config: dictnilRegister a resource template ({uri_template, name, handler, description?, mime_type?})
mcp_prompt(config)config: dictnilRegister a prompt ({name, handler, description?, arguments?})

Tool annotations (MCP spec annotations field) can be passed in the tool_define config to describe tool behavior:

tools = tool_define(tools, "search", "Search files", {
  parameters: { query: {type: "string"} },
  returns: {type: "string"},
  handler: { args -> "results for ${args.query}" },
  annotations: {
    title: "File Search",
    readOnlyHint: true,
    destructiveHint: false
  }
})

Unknown tool_define config keys are preserved on the tool entry. Workflow graphs use this to carry runtime policy metadata directly on a tool registry, for example:

tools = tool_define(tools, "read", "Read files", {
  parameters: { path: {type: "string"} },
  returns: {type: "string"},
  handler: nil,
  policy: {
    capabilities: {workspace: ["read_text"]},
    side_effect_level: "read_only",
    path_params: ["path"],
    mutation_classification: "read_only"
  }
})

When a workflow node uses that registry, Harn intersects the declared tool policy with the graph, node, and host ceilings during validation and at execution time.

Declarative tool approval

agent_loop, workflow_execute, and workflow stage nodes accept an approval_policy option that declaratively gates tool calls:

agent_loop("task", "system", {
  approval_policy: {
    auto_approve: ["read*", "list_*"],
    auto_deny: ["shell*"],
    require_approval: ["edit_*", "write_*"],
    write_path_allowlist: ["/workspace/**"]
  }
})

Evaluation order: auto_denywrite_path_allowlistauto_approverequire_approval. Tools that match no pattern default to AutoApproved. require_approval calls the host via the canonical ACP session/request_permission request and fails closed if the host does not implement it. Policies compose across nested scopes with most-restrictive intersection: auto-deny and require-approval take the union, while auto_approve and write_path_allowlist take the intersection.

Example (agent.harn):

pipeline main(task) {
  var tools = tool_registry()
  tools = tool_define(tools, "greet", "Greet someone", {
    parameters: { name: {type: "string"} },
    returns: {type: "string"},
    handler: { args -> "Hello, ${args.name}!" }
  })
  mcp_tools(tools)

  mcp_resource({
    uri: "docs://readme",
    name: "README",
    text: "# My Agent\nA demo MCP server."
  })

  mcp_resource_template({
    uri_template: "config://{key}",
    name: "Config Values",
    handler: { args -> "value for ${args.key}" }
  })

  mcp_prompt({
    name: "review",
    description: "Code review prompt",
    arguments: [{ name: "code", required: true }],
    handler: { args -> "Please review:\n${args.code}" }
  })
}

Run as an MCP server:

harn mcp-serve agent.harn

Configure in Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "my-agent": {
      "command": "harn",
      "args": ["mcp-serve", "agent.harn"]
    }
  }
}

Notes:

  • mcp_tools(registry) (or the alias mcp_serve) must be called to register tools.
  • Resources, resource templates, and prompts are registered individually.
  • All print/println output goes to stderr (stdout is the MCP transport).
  • The server supports the 2025-11-25 MCP protocol version over stdio.
  • Tool handlers receive arguments as a dict and should return a string result.
  • Prompt handlers receive arguments as a dict and return a string (single user message) or a list of {role, content} dicts.
  • Resource template handlers receive URI template variables as a dict and return the resource text.

Workflow and orchestration builtins

These builtins expose Harn’s typed orchestration runtime.

Workflow graph and planning

FunctionParametersReturnsDescription
workflow_graph(config)config: dictworkflow graphNormalize a workflow definition into the typed workflow IR
workflow_validate(graph, ceiling?)graph: workflow, ceiling: dict (optional)dictValidate graph structure and capability ceilings
workflow_inspect(graph, ceiling?)graph: workflow, ceiling: dict (optional)dictReturn graph plus validation summary
workflow_clone(graph)graph: workflowworkflow graphClone a workflow and append an audit entry
workflow_insert_node(graph, node, edge?)graph, node, edgeworkflow graphInsert a node and optional edge
workflow_replace_node(graph, node_id, node)graph, node_id, nodeworkflow graphReplace a node definition
workflow_rewire(graph, from, to, branch?)graph, from, to, branchworkflow graphRewire an edge
workflow_set_model_policy(graph, node_id, policy)graph, node_id, policyworkflow graphSet per-node model policy
workflow_set_context_policy(graph, node_id, policy)graph, node_id, policyworkflow graphSet per-node context policy
workflow_set_auto_compact(graph, node_id, policy)graph, node_id, policyworkflow graphSet per-node auto-compaction policy
workflow_set_output_visibility(graph, node_id, visibility)graph, node_id, visibilityworkflow graphSet per-node output-visibility filter ("public"/"public_only"/nil)
workflow_policy_report(graph, ceiling?)graph, ceiling: dict (optional)dictInspect workflow/node policies against an explicit or builtin ceiling
workflow_diff(left, right)left, rightdictCompare two workflow graphs
workflow_commit(graph, reason?)graph, reasonworkflow graphValidate and append a commit audit entry

Workflow execution and run records

FunctionParametersReturnsDescription
workflow_execute(task, graph, artifacts?, options?)task, graph, artifacts, optionsdictExecute a workflow and persist a run record
run_record(payload)payload: dictrun recordNormalize a run record
run_record_save(run, path?)run, pathdictPersist a run record
run_record_load(path)path: stringrun recordLoad a run record from disk
load_run_tree(path)path: stringdictLoad a persisted run with delegated child-run lineage
run_record_fixture(run)runreplay fixtureDerive a replay/eval fixture from a saved run
run_record_eval(run, fixture?)run, fixturedictEvaluate a run against an embedded or explicit fixture
run_record_eval_suite(cases)cases: listdictEvaluate a list of {run, fixture?, path?} cases as a regression suite
run_record_diff(left, right)left, rightdictCompare two run records and summarize stage/status deltas
eval_suite_manifest(payload)payload: dictdictNormalize a grouped eval suite manifest
eval_suite_run(manifest)manifest: dictdictEvaluate a manifest of saved runs, fixtures, and optional baselines
eval_metric(name, value, metadata?)name: string, value: any, metadata: dictnilRecord a named metric into the eval metric store
eval_metrics()listReturn all recorded eval metrics as {name, value, metadata?} dicts

workflow_execute options currently include:

  • max_steps
  • persist_path
  • resume_path
  • resume_run
  • replay_path
  • replay_run
  • replay_mode ("deterministic" currently replays saved stage fixtures)
  • parent_run_id
  • root_run_id
  • execution ({cwd?, env?, worktree?} for isolated delegated execution)
  • audit (seed mutation-session metadata for trust/audit grouping)
  • mutation_scope
  • approval_policy (declarative tool approval policy; see below)

verify nodes may also define execution checks inside node.verify, including:

  • command to execute via the host shell in the current execution context
  • assert_text to require visible output to contain a substring
  • expect_status to require a specific exit status

Tool lifecycle hooks

FunctionParametersReturnsDescription
register_tool_hook(config)config: dictnilRegister a pre/post hook for tool calls matching pattern (glob). deny string blocks matching tools; max_output int truncates results
clear_tool_hooks()nonenilRemove all registered tool hooks

Context and compaction utilities

FunctionParametersReturnsDescription
estimate_tokens(messages)messages: listintEstimate token count for a message list (chars / 4 heuristic)
microcompact(text, max_chars?)text, max_chars (default 20000)stringSnip oversized text, keeping head and tail with a marker
select_artifacts_adaptive(artifacts, policy)artifacts: list, policy: dictlistDeduplicate, microcompact oversized artifacts, then select with token budget
transcript_auto_compact(messages, options?)messages: list, options: dictlistRun the same transcript auto-compaction pipeline used by agent_loop

Delegated workers

FunctionParametersReturnsDescription
spawn_agent(config)config: dictdictStart a worker from a workflow graph or delegated stage config
sub_agent_run(task, options?)task: string, options: dictdictRun an isolated child agent loop and return a clean envelope {summary, artifacts, evidence_added, tokens_used, budget_exceeded, ...} without leaking the child transcript into the parent
send_input(handle, task)handle, taskdictRe-run a completed worker with a new task, carrying forward worker state where applicable
resume_agent(id_or_snapshot_path)id or pathdictRestore a persisted worker snapshot into the current runtime
wait_agent(handle_or_list)handle or listdict or listWait for one worker or a list of workers to finish
close_agent(handle)handledictCancel a worker and mark it terminal
list_agents()nonelistList worker summaries tracked by the current runtime

spawn_agent(...) accepts either:

  • {task, graph, artifacts?, options?, name?, wait?} for typed workflow runs
  • {task, node, artifacts?, transcript?, name?, wait?} for delegated stage runs
  • Either shape may also include policy: <capability_policy> to narrow the worker’s inherited execution ceiling.
  • Either shape may also include tools: ["name", ...] as shorthand for a worker policy that only allows those tool names.
  • Either shape may also include execution: {cwd?, env?, worktree?} where worktree accepts {repo, path?, branch?, base_ref?, cleanup?}.
  • Either shape may also include audit: {session_id?, parent_session_id?, mutation_scope?, approval_policy?}

Worker configs may also include carry to control continuation behavior:

  • carry: {artifacts: "inherit" | "none" | <context_policy>}
  • carry: {resume_workflow?: bool, persist_state?: bool}

To give a spawned worker prior conversation context, open a session before spawning and set model_policy.session_id on the worker’s node. Use agent_session_fork(parent) if the worker should start from a branch of an existing conversation; agent_session_reset(id) before the call if you want a fresh run with the same id.

Workers return handle dicts with an id, lifecycle timestamps, status, mode, result/error fields, transcript presence, produced artifact count, snapshot/child-run paths, immutable original request metadata, normalized provenance, and audit mutation-session metadata when available. The request object preserves canonical research_questions, action_items, workflow_stages, and verification_steps arrays when the caller supplied them. When a worker-scoped policy denies a tool call, the agent receives a structured tool result payload: {error: "permission_denied", tool: "...", reason: "..."}.

sub_agent_run(task, options?) is the lighter-weight context-firewall primitive. It starts a child session, runs a full agent_loop, and returns only a single typed envelope to the parent:

  • summary, artifacts, evidence_added, tokens_used, budget_exceeded, session_id, and optional data
  • ok: false plus error: {category, message, tool?} when the child fails or hits a capability denial
  • background: true returns a normal worker handle whose mode is sub_agent

Options mirror agent_loop where relevant (provider, model, tools, tool_format, max_iterations, token_budget, policy, approval_policy, session_id, system) and also accept:

  • allowed_tools: ["name", ...] to narrow the child tool registry and capability ceiling
  • response_format: "json" to parse structured child JSON into data from the final successful transcript when possible
  • returns: {schema: ...} to validate that structured child JSON against a schema

Artifacts and context

FunctionParametersReturnsDescription
artifact(payload)payload: dictartifactNormalize a typed artifact/resource
artifact_derive(parent, kind, extra?)parent, kind, extraartifactDerive a new artifact from a prior one
artifact_select(artifacts, policy?)artifacts, policylistSelect artifacts under context policy and budget
artifact_context(artifacts, policy?)artifacts, policystringRender selected artifacts into context
artifact_workspace_file(path, content, extra?)path, content, extraartifactBuild a normalized workspace-file artifact with path provenance
artifact_workspace_snapshot(paths, summary?, extra?)paths, summary, extraartifactBuild a workspace snapshot artifact for host/editor context
artifact_editor_selection(path, text, extra?)path, text, extraartifactBuild an editor-selection artifact from host UI state
artifact_verification_result(title, text, extra?)title, text, extraartifactBuild a verification-result artifact
artifact_test_result(title, text, extra?)title, text, extraartifactBuild a test-result artifact
artifact_command_result(command, output, extra?)command, output, extraartifactBuild a command-result artifact with structured output
artifact_diff(path, before, after, extra?)path, before, after, extraartifactBuild a unified diff artifact from before/after text
artifact_git_diff(diff_text, extra?)diff_text, extraartifactBuild a git-diff artifact from host/tool output
artifact_diff_review(target, summary?, extra?)target, summary, extraartifactBuild a diff-review artifact linked to a diff/patch target
artifact_review_decision(target, decision, extra?)target, decision, extraartifactBuild an accept/reject review-decision artifact linked by lineage
artifact_patch_proposal(target, patch, extra?)target, patch, extraartifactBuild a proposed patch artifact linked to an existing target
artifact_verification_bundle(title, checks, extra?)title, checks, extraartifactBundle structured verification checks into one review artifact
artifact_apply_intent(target, intent, extra?)target, intent, extraartifactRecord an apply or merge intent linked to a reviewed artifact

Core artifact kinds commonly used by the runtime include resource, workspace_file, workspace_snapshot, editor_selection, summary, transcript_summary, diff, git_diff, patch, patch_set, patch_proposal, diff_review, review_decision, verification_bundle, apply_intent, test_result, verification_result, command_result, and plan.

Sessions

Sessions are the first-class resource for agent-loop conversations. They own a transcript history, closure subscribers, and a lifecycle. See the Sessions chapter for the full model.

FunctionParametersReturnsDescription
agent_session_open(id?)id: string or nilstringIdempotent open; nil mints a UUIDv7
agent_session_exists(id)idboolSafe on unknown ids
agent_session_length(id)idintMessage count; errors on unknown id
agent_session_snapshot(id)iddict or nilRead-only deep copy of the transcript
agent_session_reset(id)idnilWipes history; preserves id and subscribers
agent_session_fork(src, dst?)src, dststringCopies transcript; subscribers are not copied
agent_session_trim(id, keep_last)id, keep_last: intintRetain last keep_last messages; returns kept count
agent_session_compact(id, opts)id, opts: dictintRuns the LLM/truncate/observation-mask compactor
agent_session_inject(id, message)id, message: dictnilAppends {role, content, …}; missing role errors
agent_session_close(id)idnilEvicts immediately regardless of LRU cap

Pair with agent_loop(..., {session_id: id, ...}): prior messages load as prefix and the final transcript is persisted back on exit.

Transcript lifecycle

Lower-level transcript primitives. Most callers should prefer sessions; these remain useful for building synthetic transcripts, replay fixtures, and offline analysis.

FunctionParametersReturnsDescription
transcript(metadata?)metadata: anytranscriptCreate an empty transcript
transcript_messages(transcript)transcriptlistReturn transcript messages
transcript_assets(transcript)transcriptlistReturn transcript asset descriptors
transcript_add_asset(transcript, asset)transcript, assettranscriptRegister a durable asset reference on a transcript
transcript_events(transcript)transcriptlistReturn canonical transcript events
transcript_events_by_kind(transcript, kind)transcript, kindlistFilter transcript events by their kind field
transcript_stats(transcript)transcriptdictCount messages, tool calls, and visible events on a transcript
transcript_summary(transcript)transcriptstring or nilReturn transcript summary
transcript_fork(transcript, options?)transcript, optionstranscriptFork transcript state
transcript_reset(options?)optionstranscriptStart a fresh active transcript with optional metadata
transcript_archive(transcript)transcripttranscriptMark transcript archived and append an internal lifecycle event
transcript_abandon(transcript)transcripttranscriptMark transcript abandoned and append an internal lifecycle event
transcript_resume(transcript)transcripttranscriptMark transcript active again and append an internal lifecycle event
transcript_compact(transcript, options?)transcript, optionstranscriptCompact a transcript with the runtime compaction engine
transcript_summarize(transcript, options?)transcript, optionstranscriptCompact via LLM-generated summary
transcript_auto_compact(messages, options?)messages, optionslistApply the agent-loop compaction pipeline to a message list
transcript_render_visible(transcript)transcriptstringRender only public/human-visible messages
transcript_render_full(transcript)transcriptstringRender the full execution history

Transcript messages may now carry structured block content instead of plain text. Use add_user(...), add_assistant(...), or add_message(...) with a list of blocks such as {type: "text", text: "..."}, {type: "image", asset_id: "..."}, {type: "file", asset_id: "..."}, and {type: "tool_call", ...}, with per-block visibility: "public" | "internal" | "private". Durable media belongs in transcript.assets, while message/event blocks should reference those assets by id or path.