# Scripting cheatsheet

> A compact reference for writing Harn scripts. For the one-page agent reference, see Harn quick reference .

Website: https://harnlang.com/scripting-cheatsheet.html

This page documents Harn, which is pre-1.0. Language, standard library, and CLI APIs may change. If the intended version is unclear, clarify before using this page.

---

A compact reference for writing Harn scripts. For the one-page agent
reference, see [Harn quick reference](docs/llm/harn-quickref.md).

## Strings

Use standard double-quoted strings with `\n` escapes for short
literals, and triple-quoted `"""..."""` for multiline prose like
system prompts:

```harn
const greeting = "Hello, ${name}!"
const prompt = """
You are a strict grader.
Emit exactly one verdict.
"""
```

Heredoc-style `<<TAG ... TAG` is **only** valid inside LLM tool-call
argument JSON — in source code, the parser points you at triple
quotes.

## Prompt templates

Use `harness.fs.render_prompt("file.prompt", bindings)` / `harness.fs.render_prompt(...)` for
source-relative prompt assets, and `harness.fs.render_template(template, bindings)`
when the template should live inline in the module:

```harn
const template = """
pub fn {{ name }}(
  {{ for p in params }}{{ p }}{{ if !loop.last }}, {{ end }}{{ end }},
) {
  return "{{ name }}"
}
"""

const src = harness.fs.render_template(template, {
  name: "hello",
  params: ["name", "title = nil"],
})
```

The template language is the same either way: `{{ if }}`, `{{ for }}`,
filters like `| upper` / `| default: ...`, `{{ include "..." }}` for
file-backed partials, comments, raw blocks, and whitespace trimming.
See `prompt-templating.md` for the full reference.

## Slicing

End-exclusive slicing works on strings and lists:

```harn
const head = content[0:400]
const tail = content[len(content) - 400:len(content)]
const sub = xs[1:4]
```

`substring(s, start, end)` exists too. Its second argument is an
exclusive **end** index — the same convention as `s[start:end]` slicing,
`.substring`, and `list.slice` — and `end` defaults to the string length.

Strings are UTF-8, so `s[i]`, `s[a:b]`, `s.count`, and `substring(...)`
are each O(n) in the string length — a per-character cursor loop over a
large source file is O(n²). To scan source text, materialize once with
`chars(s)` (ASCII characters are interned, so it does not allocate per
char) and index the resulting list, which is O(1):

```harn
const cs = chars(src)
let i = 0
while i < cs.count {
  if cs[i] == "{" { /* ... */ }
  i = i + 1
}
```

## Selecting record fields

Use `pick` to build a record from named fields without repeating each value:

```harn
fn main(harness: Harness) {
  const ctx = pick(harness, ["env", "fs"])
  const path = ctx.env.get_or("APP_CONFIG", "harn.toml")
  harness.stdio.println(ctx.fs.exists(path))
}
```

`pick` needs no import. It preserves field types and stored `nil` values.
Runtime key lists produce optional fields. See [Pick fields from a record](pick.md).

## `if` is an expression

`if` / `else` produces a value. Drop it straight into `let`, an
argument, or a `return`:

```harn
const body = if len(content) > 2400 {
  content[0:400] + "..." + content[len(content) - 400:len(content)]
} else {
  content
}
```

## Stream operators

`stream.*` accepts lists, ranges, channels, generators, and lazy
`iter(...)` values. Operators stay lazy until a sink such as
`stream.collect`, `stream.fold`, or `stream.first` pulls from them.

```harn
const first_three = stream.collect(
  stream.take(results_channel, 3), {max: 3},
)

const tool_events = stream.collect(
  stream.filter(agent_events, { ev -> ev?.topic == "tool_call" }),
  {max: 100}
)

const winner = stream.first(stream.race(primary_stream, fallback_stream))

const total = stream.fold(
  stream.merge(worker_a, worker_b, worker_c),
  0,
  { acc, item -> acc + item.cost }
)
```

Always pass a realistic `{max: N}` to `stream.collect` when the upstream
can be unbounded.

## LLM resilience patterns

`agent_loop` accepts an `llm_caller:` closure that owns each turn's
`harness.llm.call(...)`. Wrap it with middleware from `std/llm/handlers` to
compose retry / fallback / shadow / logging / budget behavior:

```harn,ignore
import {default_llm_caller} from "std/llm/caller"
import {with_retry, with_fallback, compose} from "std/llm/handlers"

const caller = compose([
  with_retry({max_attempts: 4, base_ms: 250, backoff: "exponential"}),
])(default_llm_caller())

const result = agent_loop(harness, task, system, {
  loop_until_done: true,
  llm_caller: caller,
})
```

Migrating from `llm_retries: K` (removed in 0.10): use
`with_retry(default_llm_caller(), {max_attempts: K + 1})`. The off-by-one is
deliberate — `llm_retries` historically counted retries after the first
attempt; `max_attempts` counts total attempts. See
[`stdlib/llm-handlers.md`](./stdlib/llm-handlers.md) for the full
catalog (handlers, ensemble, refine, budget, defaults, safe, prompts,
catalog).

## Module scope

Top-level `const` / `let` and `fn` declarations are visible inside
functions defined in the same file — no wrapping in a getter fn
needed:

```harn
const GRADER_SYSTEM = """
You are a strict grader...
"""

pub fn grade(path) {
  return harness.llm.call(harness.fs.read_text(path), GRADER_SYSTEM, {
    provider: "auto",
    model: "local-gemma4-e4b",
  })
}
```

(Module-level mutable `let` cross-function mutation is not fully
supported yet. If you need shared mutable state across functions, use
atomics: `harness.runtime.atomic(0)`, `harness.runtime.atomic_add(a, 1)`,
`harness.runtime.atomic_get(a)`.)

## Results and error handling

```harn
const r = try { harness.llm.call(prompt, nil, opts) }
// Optional chaining short-circuits on Result.Err.
const text = r?.text ?? "no response"
// Explicit error inspection.
if unwrap_err(r) != "" {
  harness.stdio.log("failed")
}

// `try/catch` also works as an expression — the whole form evaluates to
// the try body's tail value on success or the catch handler's tail value
// on a caught throw, so simple fallbacks don't need Result gymnastics.
const answer = try {
  harness.llm.call(prompt, nil, opts).text
} catch (e) { "fallback" }
```

## Concurrency

```harn,ignore
// Spawn a task, collect its result.
const h = spawn { long_work() }
const value = await(h)

// parallel each: concurrent map over a list.
const doubled = parallel each xs { x -> x * 2 }

// parallel settle: concurrent map that collects per-item Ok/Err.
const outcome = parallel settle paths { p -> grade(p) }
harness.stdio.log(outcome.succeeded)

// Cap in-flight workers so you don't overwhelm the backend.
const results = parallel settle paths with { max_concurrent: 4 } { p ->
  harness.llm.call(p, nil, opts)
}
```

`max_concurrent: 0` (or a missing `with` clause) means unlimited. See
`concurrency.md` for the RPM rate limiter, channels, `select`,
`deadline`, and `defer`.

## Stream generators

Use `gen fn` plus `emit` for lazy script-level streams:

```harn
gen fn numbers() -> Stream<int> {
  emit 1
  emit 2
}

for n in numbers() {
  harness.stdio.log(n)
}
```

`Stream<T>` is distinct from the older `Generator<T>` type. Existing
`yield` behavior is unchanged; use `emit` inside `gen fn`. Streams are
single-pass, support `.next()` returning `{value, done}`, and propagate
throws to the consumer when the next item is pulled.

## CLI: `argv`

```bash
harn run my_script.harn -- file1.md file2.md
```

Inside the script:

```harn
fn grade_file(path) {
  harness.stdio.log(path)
}

for path in argv {
  grade_file(path)
}
```

`argv` is always defined as `list<string>`; empty when no positional
args were given.

## Fixed-arity tuples

```harn
const row = tuple("retries", 3)          // tuple<string, int>
const key: string = row[0]               // exact positional type
const value: int = row[-1]               // negative indexes work
const typed: tuple<string, int> = ["timeout", 30]
```

Bracket literals remain lists unless a `tuple<...>` annotation or function
parameter supplies tuple context. Constant out-of-bounds indexes are
`HARN-TYP-027`; dynamic indexes return the union of all positions plus `nil`.

## Reuse narrowing checks

A `const` keeps the narrowing facts from its condition:

```harn
fn normalize(value: string | int) -> string {
  const kind = type_of(value)
  const text = kind == "string"
  if text { return value.upper() }
  return to_string(value)
}
```

Declare a predicate when several callers need the same check:

```harn
fn is_text(value: unknown) -> value is string {
  return type_of(value) == "string"
}
```

Use `implies value is T` if a false result can still be `T`. Invalid predicate
contracts report `HARN-TYP-029`.

## Regex

```harn
const matches  = regex_match("[0-9]+", "abc 42 def 7")
const swapped  = regex_replace("(\\w+)\\s(\\w+)", "$2 $1", "hello world")
const captures = regex_captures("(?P<day>[A-Z][a-z]+)", "Mon Tue")
```

`regex_replace` replaces every match and supports `$1`, `$2`, and
`${name}` backrefs from the `regex` crate.

## LLM calls

```harn
const r = harness.llm.call(prompt, system, {
  provider: "auto",        // infers from model prefix
  model: "local-gemma4-e4b",
output: {schema: schema, validation: "error", stream_abort: true},
schema_retries: 2,       // retry with corrective nudge on schema mismatch
})
// the public answer (preferred for "the answer")
harness.stdio.log(r.text)
harness.stdio.log(r.data.verdict)    // parsed structured output
```

Key options:

| Option | Default | Notes |
|---|---|---|
| `provider` | `"auto"` | `"auto"` infers from model prefix (`local:` / `/` / `claude-*` / `gpt-*` / `:`). |
| `output` | `"text"` | `"json"`, a schema, or `{schema, strict?, validation?, stream_abort?}`. |
| `schema_retries` | `1` | Re-prompt after an `output` schema mismatch. |
| `schema_retry_nudge` | auto | String (verbatim), `true` (auto), or `false` (bare retry). |
| `effort` | provider default | Provider-neutral reasoning intent such as `low`, `medium`, or `high`. |
| `timeout_ms` | provider default | Whole-call timeout in milliseconds. |

See `docs/src/llm-and-agents.md` for the overview, or
`docs/src/llm/agent_loop.md` for `agent_loop`, tool dispatch, and the full
option surface.

## Rate limiting

`max_concurrent` bounds simultaneous in-flight tasks on the caller
side. Providers can also be rate-limited at the throughput layer via
`rpm:` in `providers.toml` / `harn.toml` or
`HARN_RATE_LIMIT_<PROVIDER>=N` env vars. The two compose: use
`max_concurrent` to prevent bursts, and `rpm` to shape sustained
throughput.

## More

- LLM-friendly one-pager: `docs/llm/harn-quickref.md` (hosted at
  <https://harnlang.com/docs/llm/harn-quickref.html> and loaded
  automatically by the `harn-scripting` Claude skill when present).
- Full mdBook: `docs/src/` (`introduction.md`, `language-basics.md`,
  `concurrency.md`, `error-handling.md`, `llm-and-agents.md`).
- Language spec: `spec/HARN_SPEC.md`.
- Conformance examples: `conformance/tests/*.harn`.

---

## Read next

- [Cookbook](https://harnlang.com/cookbook.md)
- [Reuse narrowing checks](https://harnlang.com/narrowing-checks.md)
