Skip to content

Halo on Halo: describe a workflow, get a validated pipeline ​

examples/70-pipeline-author.halo policies |<> loops |* parallel |! people

An agent writes a .halo program from a plain-English request. The real validator checks it; if there are errors, |<> loop sends the agent the validator's messages to fix. You approve the result before it's saved. RUN (from the Halo repo, or anywhere halo is installed): halo run examples/70-pipeline-author.halo \ --arg request="Every morning, summarize the top 5 Hacker News stories" \ --arg save_as=pipelines/hn-digest.halo

Open in the playground →

halo
// ============================================================
// Halo on Halo: describe a workflow, get a validated pipeline
// ============================================================
// An agent writes a .halo program from a plain-English request.
// The real validator checks it; if there are errors, |<> loop sends
// the agent the validator's messages to fix. You approve the result
// before it's saved.
//
// RUN (from the Halo repo, or anywhere `halo` is installed):
//   halo run examples/70-pipeline-author.halo \
//     --arg request="Every morning, summarize the top 5 Hacker News stories" \
//     --arg save_as=pipelines/hn-digest.halo
// ============================================================

policy Budget : MaxSpend(0.30)

tool Shell : Bash()

agent Author [anthropic/claude-sonnet-4-5, temp: 0.2]
  system """
    You write programs in Halo, a language for AI workflows. Reply with
    only the program: no Markdown fences, no commentary.

    Declarations (top level, any order):
      policy Name : Rule(args) & Rule(args)
        Rules: Block(PromptInjection | Profanity | PII | Email | APIKey | DestructiveOps | Markdown),
        Redact(PII | Email | Phone | APIKey | SSN | CreditCard), MaxWords(n), MaxChars(n),
        MaxTokens(n), MaxSpend(usd), Require(JSON | XML | Citation), MatchRegex("re"),
        Validate("field", "<=", "context.data.x.limit"), LogViolation()
      tool Name : Provider(args)
        REST("host/base") GET with named args as query params; `path: "/x/{{id}}"`
        POST("host/url", apiKey: Env.KEY, sendInput: false) named args as JSON body
        Bash() run(command: "...") -> {stdout, stderr, exitCode, json}
        Browser() fetch(url: "...") -> {title, content}
        File(".halo/x.json", keep: 10) read / append(content: ...)
        Slack(Env.SLACK_BOT_TOKEN) post(channel: "#c", text: "...")
        Email(Env.RESEND_API_KEY, from: "a@b.c") send(to:, subject:, body:)
        GitHub(Env.GITHUB_TOKEN) create_issue(repo:, title:, body:), list_issues(repo:, state:, limit:)
      store Name : VectorStore(namespace: "x")   log(content:, tags: [..]), search(query:, tags:, exclude_tags:, order: "recent", limit:)
      schema Name { field: String, n: Number, items: Array<String>, ok: Boolean }
      agent Name [anthropic/claude-haiku-4-5, temp: 0.2]
        system "..."          (optional; use triple quotes for several lines)
        equips ToolA, StoreB  (tools it may use)
        requires PolicyA      (policies it must obey)
      pipeline Name(arg: String, n: Number, items: List) -> Result:
        arg
          |> Agent.task("instructions with {{arg}}", context.data.x, key: value) as result
          |> Tool.method(named: value) as other

    Operators (each on its own line, indented under the pipeline):
      |> Call(...) as name                 run a call; `as` binds context.data.name
      |? (cond)                            stop unless the condition holds (&&, ||, !, ==, !=, <, >=, null)
      |? { (cond) => Call(), else => Call() }   branch
      |* { A.x() as a, B.y() as b }        parallel calls
      |* each(item in list) => Call(item)  one call per item; each(item in list, limit: 5) takes the first 5
      |<> loop(Agent, "cond", max: 3)      go back to the agent's stage while the condition (a string) holds
      |! yield(msg: "Approve?", [context.data.x]) as answer   ask a human; check context.data.answer.approved
      System.Print(x), System.Value(x) (makes x the flowing value),
      System.Pick(x, fields: ["a", "b.c"]) (keeps fields of an object or of each object in a list)
    Paths: list.0 is the first item, list.length the count, list.title every item's title.
    To use only the first N items of a list, write |* each(x in list, limit: N) => Call(x).
    Never use System.Pick to take items: it only keeps fields of objects.
    Directives above an agent or pipeline: @retry(2, 5s), @timeout(5m), @correct(2).

    Rules: a pipeline starts with one of its arguments. Read results only
    after they are produced (bind them with `as`). An agent may only call a
    tool through `tool: Name` if it equips it. Use Env.NAME only inside tool
    declarations. Prefer free public APIs that need no key.
  """
  requires Budget

pipeline AuthorPipeline(request: String, save_as: String) -> Result:
  request
    // 1. Draft the program
    |> Author.write("Write a complete Halo program for this request: {{request}}") as draft

    // 2. Validate it with the real validator (fences stripped, just in case)
    |> Shell.run(command: """
         mkdir -p .halo/drafts
         printf '%s\n' {{draft}} | sed '/^```/d' > .halo/drafts/draft.halo
         if command -v halo >/dev/null 2>&1; then CLI="halo"; else CLI="bun run index.ts"; fi
         NO_COLOR=1 $CLI validate .halo/drafts/draft.halo
       """) as check

    // 3. Errors? The author gets the validator's messages and tries again
    |<> loop(Author, "context.data.check.exitCode != 0", max: 4) as fixes

    // 4. You approve before it's saved
    |! yield(msg: "Save this pipeline to {{save_as}}?", [context.data.draft, context.data.check.stdout]) as review
    |? (context.data.review.approved == true)
    |> Shell.run(command: "mkdir -p \"$(dirname {{save_as}})\" && cp .halo/drafts/draft.halo {{save_as}} && echo {{save_as}}") as saved
    |> System.Value(context.data.check.stdout)

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