The .aether file

The .aether format explained: one file is one flow, written as YAML with embedded JavaScript, with routing on each node instead of a separate edges list.

5 min read

One .aether file describes one flow. It’s plain YAML with embedded JavaScript expressions, readable in any editor and diffable in any pull request. You never have to write it by hand (the graph and inspector edit it for you), but knowing the shape pays off quickly.

The shape of a file

version: "1.0" # required, the format version
name: User CRUD Flow # required
description: Optional prose about what this flow covers.
variables: # optional flow-level defaults
  baseUrl: https://api.example.com
start: login # optional entry node id; defaults to the first node
nodes: # required, the steps
  - id: login
    type: request
    config:
      method: POST
      url: ${{ variables.baseUrl }}/auth/login
      capture:
        accessToken: response.body.token
    next: get_profile
  - id: get_profile
    type: request
    config:
      method: GET
      url: ${{ variables.baseUrl }}/me
      headers:
        Authorization: Bearer ${{ variables.accessToken }}
    next: null

The rules worth knowing

  • Every node has id, type, and config. The id must be unique in the file; it’s what routing points at. An optional label gives the node a friendlier display name on the graph.
  • Routing lives on the node. There is no edges list. A node names its successor in next (or then/else for conditions, body + next for loops, branches + next for parallel fan-outs). Omit it or set it to null to end that branch. The graph derives its edges from these fields, so the file can never disagree with the picture.
  • Two kinds of dynamic values, deliberately distinct:
    • ${{ ... }} does string interpolation inside YAML string values like url, headers, and body: url: ${{ variables.baseUrl }}/users.
    • With bare JavaScript, the entire value of check, expression, and over is a JS expression with response, variables, and env in scope. Don’t wrap these in ${{ }}.
  • Every JavaScript scope can print. log("…") is available in script code, hooks, check/expression/over, capture expressions, and ${{ }} interpolation. It formats its arguments like console.log and reaches the Console, bandura run, and the MCP server alike, even when the node it ran in failed. See the Node reference.
  • Layout is part of the file. Dragging a node on the graph writes ui.position into its YAML, so the arrangement survives commits and travels to teammates. A node without a position is laid out automatically.

Optional top-level blocks

Five optional keys sit alongside nodes at the top level:

  • data turns the flow into a data-driven run: it runs once per row, with that row in scope as data (use ${{ data.column }} in any URL, header, body, or assertion). Give a file or inline rows, exactly one of the two:

    data:
      file: users.csv # a .csv or .json file next to the flow
      # or, instead of a file:
      rows:
        - { email: [email protected], role: admin }
        - { email: [email protected], role: viewer }
      stopOnFailure: true # optional, stops at the first failing row (default: keep going)

    Each row is an independent test case (a capture in one row never leaks into the next), and the run reports a per-row passed/failed tally.

    In the app this block has a Dataset editor on the inspector’s flow panel, the one you get with no node selected. It switches between no dataset, a file and inline rows, and it counts what you have as you type: “3 rows of 4 columns”, naming them. Rows are written as a JSON array, and Convert pasted CSV turns a block of CSV or TSV pasted straight from a spreadsheet into that array. It also warns when a column is missing from a row, which otherwise surfaces much later as one puzzling failure, because a missing value interpolates as the empty string with no error of its own.

  • hooks wrap every request-like node (request / grpc / websocket) with before, after, and/or onError snippets, for cross-cutting work like signing, correlation ids, or audit logging. Each phase is a { script: "…" } block; hooks are per-flow and never merged across a subflow boundary. A phase can also read its body from a .js file beside the flow (file: in place of script:), which is how one signing hook is shared by several flows rather than pasted into each. See the Node reference for the full shape and for what a file-backed body can and cannot do.

  • auth declares how every request node authenticates, so you are not writing an Authorization header into each one. Basic, bearer, API key, OAuth2 (with a token that is fetched once and refreshed for you) and AWS SigV4:

    auth:
      type: bearer
      token: ${{ env.API_TOKEN }}

    A node can carry its own auth, which replaces the flow-level block rather than merging with it, so one request’s credential is always readable from one place. Cookies need no block at all: they are captured and replayed automatically. Both are covered in Authentication & cookies.

  • tls and proxy configure the connection rather than the credential: a client certificate for mutual TLS, a private CA to trust, and an HTTP or HTTPS proxy with its own login.

    tls:
      ca: ./certs/internal-ca.pem
    proxy:
      url: ${{ env.HTTP_PROXY }}

    Both follow the same replace-rather-than-merge rule auth does when a node sets its own. See Client certificates & proxies.

There is also a variables block for the flow’s own defaults. Note that its values are seeded literally: ${{ }} is resolved where a value is used, not where it is declared, so a dynamic value belongs in a script node rather than here.

Why it matters that this is just a file

Commit flows next to the code they test. Review a flow change as a normal diff; a changed URL or assertion reads exactly like a changed line of code. Hand the suite to CI with the CLI or to an AI agent with the MCP server. Both read the same files the editor does. If you’re weighing Bandura against Bruno, the other tool built on plain files in your repo, Bandura vs Bruno compares the two file formats and what each describes.

For every node type and its config fields, see the Node reference.

Last updated

Looking for something else? All 37 articles are on one page in the Help Center.