Attributes
Attributes are declarative metadata attached to a top-level declaration
with the @ prefix. They compile to side-effects (warnings, runtime
registrations) at the attached declaration, and stack so a single decl
can carry multiple. Arguments are restricted to compile-time values:
strings, numbers, booleans, nil, bare or dotted identifiers, lists,
dicts, and simple call-shaped sentinels such as schedule("...").
There is no runtime evaluation.
Syntax
attribute ::= '@' attr_name ['(' attr_arg (',' attr_arg)* [','] ')']
attr_arg ::= [attr_name ':'] attr_value
attr_name ::= IDENTIFIER | 'retry'
attr_value ::= literal | dotted_ident | attr_list | attr_dict | attr_call
dotted_ident ::= IDENTIFIER ('.' IDENTIFIER)*
attr_list ::= '[' [attr_value (',' attr_value)* [',']] ']'
attr_dict ::= '{' [attr_key ':' attr_value (',' attr_key ':' attr_value)* [',']] '}'
attr_key ::= IDENTIFIER | STRING
attr_call ::= dotted_ident '(' [attr_value (',' attr_value)* [',']] ')'
@deprecated(since: "0.8", use: "compute_v2")
@test
pub fn compute(x: int) -> int { return x + 1 }
Attributes attach to the immediately following declaration —
either pipeline, fn, tool, struct, enum, type, interface,
or impl. Attaching to anything else (a let, a statement) is a parse
error.
Standard attributes
@deprecated
@deprecated(since: "0.8", use: "new_fn")
pub fn old_fn() -> int { ... }
Emits a type-checker warning at every call site of the attributed function. Both arguments are optional; when present they are folded into the warning message.
| Argument | Type | Meaning |
|---|---|---|
since | string | Version that introduced the deprecation |
use | string | Replacement function name (rendered as a help line) |
@test
@test
pipeline test_smoke(harness: Harness, task) { ... }
Marks a pipeline as a test entry point. The conformance / harn test
runner discovers attributed pipelines in addition to the legacy
test_* naming convention. Both forms continue to work.
@job, @schedule, @queue, and @retry
import "std/triggers"
@job("scan")
@retry(max: 3, backoff: "exponential")
@schedule("0 * * * *", "UTC")
@queue("scan-jobs")
pub fn scan(harness: Harness, event: TriggerEvent) -> dict {
return {status: "ok", request: event.provider_payload.raw}
}
@job marks an exported function as a trigger-dispatched background
entrypoint. Its first two parameters are the root Harness and the normalized
TriggerEvent; ordinary helpers called from the job should accept narrower
capability handles. harn run --as-job file.harn --job scan --request req.json
runs that job once, delivering the request JSON as
event.provider_payload.raw and printing the returned value as JSON.
harn serve worker file.harn activates every @scheduled job in the
file and consumes any declared @queue worker queues until shutdown.
@retry(max: N, backoff: "...") applies the dispatcher's retry policy
to the job. max/max_attempts must be a non-negative integer;
backoff/policy accepts svix, linear, or exponential. A compact
@job("scan", retry: { max: 3, backoff: "linear" }) form is also
accepted for generated metadata that already mirrors trigger configs.
@schedule, @queue, and @retry are inert without @job and produce
a serve diagnostic.
Durable persona annotations
Durable persona metadata can be declared directly on a function, tool, or
pipeline with @persona, @step, @trigger, @handoff, and @budget:
@persona(
triggers: [github.pr_opened, schedule("*/30 * * * *")],
tools: [github, ci, linear],
autonomy: act_with_approval,
budget: {daily_usd: 20, frontier_escalations: 3},
handoffs: [review_captain, human_maintainer],
receipts: required,
)
@trigger(github.check_failed)
@handoff(target: review_captain, reason: "risky diff")
@budget(daily_usd: 20, max_tokens: 100000)
fn merge_captain(ctx: HandlerCtx) { ... }
@step(
name: "plan",
model: "gpt-5.4-mini",
approval: optional,
receipt: audit,
error_boundary: fail,
retry: {max_attempts: 2},
)
fn plan_step(ctx: HandlerCtx) { ... }
The annotations are first-class parser and type-checker metadata. The attached declaration remains an ordinary callable declaration at runtime unless a host or packaging layer consumes the metadata. Existing manifest and dict-style trigger/persona metadata remains valid Harn data and is not rewritten by this syntax.
| Attribute | Arguments |
|---|---|
@persona | Named metadata: triggers, schedules, tools, autonomy, budget, handoffs, context_packs, evals, receipts, model, owner, name, description |
@step | Named metadata: name, model, approval (required/optional), receipt (audit/none), error_boundary (fail/continue/escalate), retry ({max_attempts: N}) |
@trigger | Positional trigger specs (github.pr_opened, "github.pr_opened", schedule("cron")) or named id, provider, kind, event, when, schedule, budget |
@handoff | Named target/to, reason, schema, artifact |
@budget | Named numeric fields: daily_usd, hourly_usd, run_usd, max_tokens, frontier_escalations, max_autonomous_decisions_per_hour, max_autonomous_decisions_per_day; string/symbol exhaustion policy via on_exhausted or on_budget_exhausted |
For @persona functions, harn lint reports
persona-body-must-call-steps when the body directly calls a non-stdlib
function that is not declared with @step. Projects may allow specific
legacy helpers with [lint].persona_step_allowlist.
@command(name?, description?, hint?)
@command(name: "review", description: "Review the diff", hint: "(optional focus area)")
pipeline review_branch(harness: Harness, task) { ... }
Marks a top-level pipeline as an ACP slash-command. The Harn ACP adapter
(harn serve --pipeline ...) discovers @command-tagged pipelines from
the loaded source and advertises them to clients via the
available_commands_update session notification. When a client invokes
/<name> args, the named pipeline is compiled as the entry point and
the post-name text is passed to the pipeline as the prompt global.
Arguments are all optional and string/symbol-typed:
name— slash-command name advertised to the client. Defaults to the pipeline's declared name.description— human-readable summary shown next to the command.hint— placeholder text the client displays before the user types arguments. Maps to ACP'sUnstructuredCommandInput.hint.
The attribute is compile-time metadata only; outside of an ACP session the attached pipeline runs unchanged. Slash-command discovery refreshes on every prompt, so editor changes to the pipeline file propagate without restarting the agent.
@complexity(allow)
@complexity(allow)
pub fn classify(x: int) -> string {
if x == 1 { return "one" }
...
}
Suppresses the cyclomatic-complexity lint warning on the attached
function. The bare allow identifier is the only currently accepted
form. Use it for functions whose branching is intrinsic (parsers,
tier dispatchers, tree-sitter adapters) rather than accidental.
The rule fires when a function's cyclomatic score exceeds the default
threshold of 25. Projects can override the threshold in
harn.toml:
[lint]
complexity_threshold = 15 # stricter for this project
Cyclomatic complexity counts each branching construct (if/else,
guard, match arm, for, while, try/catch, ternary,
select case, retry) and each short-circuit boolean operator
(&&, ||). Nesting, guard-vs-if, and De Morgan rewrites are all
score-preserving — the only way to reduce the count is to
extract helpers or mark the function @complexity(allow).
@host_entry
@host_entry
pub fn dispatch_audit_exports(harness: Harness, args: dict) -> dict {
const db = connect(harness.postgres, args.database_url)
return post(harness.net, summarize(db))
}
Declares that an embedding host supplies this function's arguments — it is reached through a runtime call-into-script path from the program that embeds Harn, not from any call site in Harn source. The attribute takes no arguments and produces no runtime registration.
Its effect is on tooling. Without it the function body is the only
available evidence about the signature, so the
capability-attenuation lint reports the parameter as over-broad and
harn fix --capability-migrations-only rewrites it to the capabilities
the body happens to touch — here {net: HarnessNet, postgres: HarnessPostgres}, which the host has no way to construct. The failure
lands at dispatch rather than at harn check. With @host_entry the
signature is treated as a contract with a caller no Harn source can
see: the capability parameter is neither narrowed nor renamed, and the
capability-record carrier is never proposed for it at any capability
count.
Use it only when a host really is the caller. A function Harn's own
runtime enters is already recognized without any annotation — main, a
@job, a trigger handler, a handler: callback, and a connector
runtime export — and annotating one of those instead would record
something untrue.
@acp_tool
@acp_tool(name: "edit", kind: "edit", side_effect_level: "mutation")
pub fn apply_edit(path: string, content: string) -> EditResult { ... }
Compiles to the same runtime registration as an imperative
tool_define(tool_registry(), name, "", { handler, annotations })
call, with the function bound as the tool's handler and every named
attribute argument (other than name) lifted into the
annotations dict. name defaults to the function name when
omitted.
| Argument | Type | Meaning |
|---|---|---|
name | string | Tool name (defaults to fn name) |
kind | string | One of read, edit, delete, move, search, execute, think, fetch, other |
side_effect_level | string | none, read, mutation, destructive |
Other named arguments pass through to the annotations dict unchanged,
so additional ToolAnnotations fields can be added without a parser
change.
@invariant
@invariant("fs.writes", "src/**")
tool write_patch() {
harness.fs.write_text("src/out.txt", "ok")
}
Attaches one or more compile-time capability invariants to a fn,
tool, or pipeline. Invariants are only evaluated when the user opts
into them with harn check --invariants; plain harn check keeps the
baseline type-check + preflight behavior. Each attributed handler is
lowered into a small control-flow graph plus simple data-flow summaries,
then the selected invariant checks run against that IR.
| Invariant | Configuration | Meaning |
|---|---|---|
@invariant("fs.writes", "src/**") | One or more allowed globs, passed positionally or as path_glob: / glob: / allow: | Every reachable file-system write must target a literal path proven to stay within one of the declared globs. |
@invariant("budget.remaining", target: "remaining") | Optional target: variable name, default budget.remaining | Assignments to the tracked budget value may only preserve it, decrement it, or refresh it from llm_budget_remaining(). |
@invariant("approval.reachability") | No extra args | Every reachable side-effecting call must be gated by a prior harness.interaction.request_approval(...) or enclosed inside a harness.interaction.dual_control(...) approval scope. |
@invariant("capability.policy", allow: "fs.write,llm.model", ...) | allow: capability list; optional workspace: globs; optional require_approval:, require_budget:, require_autonomy:, require_execution_policy:, require_command_policy:, require_egress_policy:, and require_approval_policy: capability lists | Every reachable capability effect must be declared, and selected capabilities must be guarded by the requested policy/gate on every path. |
The capability-policy lattice recognizes these canonical capabilities:
fs.write, process.exec, network.access, mcp.connector,
llm.model, worker.dispatch, human.approval, and
autonomy.policy. Common aliases such as workspace.write, command,
connector, llm, and worker normalize to those names. The checker
classifies nominal capability methods such as harness.mcp.call(...),
harness.tools.invoke(...), and harness.process.exec(...). The privileged
host_call("capability.operation", ...) wire is additionally classified when
it appears in a trusted provenance-stamped host module; ordinary modules cannot
invoke or re-export that wire.
Calls like with_execution_policy(...), with_command_policy(...),
with_approval_policy(...), with_autonomy_policy(...),
with_dynamic_permissions(...), harness.net.egress_policy(...),
harness.interaction.request_approval(...), harness.interaction.dual_control(...), and budget-bearing
harness.llm.call(..., {budget: ...}) satisfy the corresponding gates.
Invariant violations surface through harn check --invariants,
harn explain --invariant <name> <handler> <file>, and the LSP. Each
diagnostic carries a concrete CFG path so editors and the CLI can show
how the violating call or assignment is reached.
Unknown attributes
Unknown attribute names produce a type-checker warning so that misspellings surface at check time. The attribute itself is otherwise ignored — code still compiles.