v

Halo a language for AI pipelines

Declare agents, tools, and the policies that bound them; chain them with pipeline operators. The runtime checks every tool call an agent makes against its policies before it runs. halo plan shows what a run could do before it starts; halo replay reruns exactly what it did.

Outputs are the real CLI, run on these files when this page was built. halo test mocks every model and tool, so it runs offline.

Install

$ curl -fsSL https://halolang.com/install.sh | sh

One standalone binary with Bun built in: nothing else to install. The script picks the build for your OS and CPU, checks it against the release's SHA256SUMS, and puts it in ~/.halo/bin. Read the script before piping it to a shell. Update with halo upgrade.

$ halo init my-flow && cd my-flow
$ halo test main.halo --arg topic=tides   # offline: every model and tool mocked
$ halo run main.halo --arg topic=tides    # with ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY set

Builds: macOS (Apple Silicon, Intel), Linux x64 and arm64 (glibc and musl), Windows x64. Editor support (VS Code with a language server, Zed, Sublime) is in editors/.

Syntax at a glance

Declarations

policy Safe : Block(PII) & MaxSpend(1.00)
tool   Web  : Browser(maxChars: 8000)
schema Out { title: String, score: Number }

@retry(2, 5s)
agent Writer [anthropic/claude-sonnet-4-5, temp: 0.3]
  equips Web
  requires Safe
  system "You write short, sourced summaries."

pipeline Brief(url: String) -> Out:
  url |> Writer.summarize(tool: Web)

Operators

|>call an agent, tool, or pipeline
|? (cond)gate: stop unless it holds
|? { (c) => A(), else => B() }branch
|* { A, B }run in parallel
|* each(x in list) => A()map over a list
|<> loop(Agent, "cond", max: 3)retry until it passes
|! yield(msg: "...")ask a person; the run pauses
~> listen(Event)wait for an outside event
as namebind a result: context.data.name

Values in a stage

"Hello {{name}}"             // template: params, bindings
context.data.review.approved  // anything earlier stages made
context.meta.cost             // spend so far (USD)
Env.GITHUB_TOKEN              // env, only in tool declarations
[A, B]  3  2s  true  null     // lists, numbers, durations, ...

Agents that choose tools

agent Fixer [anthropic/claude-sonnet-4-5, max_steps: 20]
  equips Code, Tests
  requires FixerTools

pipeline Fix(dir: String) -> Result:
  dir |> Fixer.fix("Make the tests pass.", tools: [Code, Tests])
  // or: [..., tools: auto] on the agent

Full grammar and every built-in: language reference →

Agents with tools

With tools: [...] the agent calls tools until it has an answer, using each provider's native tool calling (Anthropic, OpenAI-compatible, Gemini, Ollama). Each call is checked against the agent's policies before it runs; results pass through its Redact/Block rules before the model reads them; errors go back to the model instead of failing the run.

Below is a real run of examples/74-software-team.halo's fixer on a project with two bugs, as halo run recorded it.

Policy rules

Policies attach to agents with requires. The runtime enforces these deterministically; there's no model judging another model.

RuleChecksExample
Tool calls an agent chooses
AllowToolsOnly these methods are offered at allAllowTools("Code.*", "Tests.run")
DenyToolsRefuse these methodsDenyTools("Files.delete")
AllowEffectsOnly methods with these effectsAllowEffects(read, write)
DenyArgsRefuse calls whose argument matches a regexDenyArgs("Sh.run", "command", "^(?!bun test)")
ApproveTools · ApproveEffectsAsk a person first; the run pauses until answeredApproveEffects(external)
Content in and out
BlockStop a prompt, answer, or tool result that contains itBlock(PromptInjection, APIKey)
RedactRemove it before the model or the logs see itRedact(Email, SSN, CreditCard)
Enforce · RequireThe answer is JSON / XML / cites sourcesEnforce(JSON)
MatchRegex · ValidateThe answer matches, or a field comparesValidate("amount", "<=", "context.data.limit")
MaxWords · MaxChars · MaxTokensLength limitsMaxWords(120)
Spend
MaxSpendStop when the run has spent this much; each call's reply is capped to fitMaxSpend(3.00)

Add @correct(n) to send a rejected answer back to be fixed. All rules and categories →

Plan & replay

halo plan

Static: every model call with its worst-case cost, every tool it could call and its effect (read / write / external), what needs approval, and the spend cap. Nothing runs.


            

halo replay

Every run records what it got back: model answers, tool results, people's answers. Replay feeds them back with no calls. --live-models or --live-tools redo one side. Unrecorded tools are refused, not run.


            

CLI


        

Tool providers

Declare a tool with tool Name : Provider(args). Each method has an effect, which policies and halo plan use: read write external.

ProviderMethodsFrom

Provider reference →

Packages

Any API can ship as a Halo tool. A package is a halo-tool.json declaring providers of three kinds: http (declarative REST; no package code runs, requests stay under the declared URL), mcp (an MCP server), or module (TypeScript; needs --trust). Packages are checksummed, pinned in halo.lock, and verified on every load; trust is per machine, never read from the lock.

$ halo search payments
$ halo add stripe
$ halo add world-time --trust
$ halo tools
$ halo install --trust   # on a new machine

# publish your own
$ halo pack ./my-api
$ halo publish my-api-1.0.0.halotool --registry ./index.json

Writing a package →

halo serve

Runs the same files as a service: HTTP API, webhooks, cron schedules, watches, approval links and Slack buttons, MCP at /mcp, and a run viewer at /. It listens on localhost unless you set a token.

{
  "files": ["pipelines/"],
  "schedules": [{ "pipeline": "DailyDigest", "cron": "30 8 * * 1-5" }],
  "webhooks":  [{ "path": "github", "pipeline": "Triage", "secret": "Env.GH_HOOK_SECRET" }],
  "watches":   [{ "pipeline": "OnStatusChange", "url": "https://status.example.com/api.json", "every": "2m" }],
  "notify":    { "slack": "Env.SLACK_WEBHOOK_URL" },
  "slack":     { "signingSecret": "Env.SLACK_SIGNING_SECRET" }
}
$ halo serve --port 8080
$ curl -X POST localhost:8080/api/pipelines/Triage/runs -H 'content-type: application/json' -d '{"args":{"issue":42}}'
$ claude mcp add halo -- halo mcp ./pipelines   # pipelines as MCP tools

Playground

Halo's real parser, validator, and planner, compiled for the browser. Nothing leaves the page; no model is called.

Examples

↑↓ move · ↵ open · esc close