Rethinking namespaces in BAML

Folders should not get to change meaning behind your back.

BAML's namespace design is not just syntax. It is a bet that a developer, or an agent, should know where a name came from without reading an import prelude first.

BEP-008 idea compiler fixtures HIR path parser
Config same namespace. no ceremony. no import line.
root.llm.Response another namespace in this project. boundary shown at the callsite.
root.Config inside ns_llm, this points back to root.
baml.http.fetch external package shape. the stdlib follows the same rule.
function GetResponse(cfg: Config) -> root.llm.Response {
  root.llm.Response { text: "hello", model: cfg.model }
}

The usual namespace story starts in the wrong place.

If you come from TypeScript, Python, Rust, Go, or Java, you probably expect the top of the file to explain scope. Imports, package declarations, aliases, barrels, path config. Fine. Familiar.

But BAML is trying to be readable by humans and agents. That changes the priority. A reader should not have to reconstruct scope before understanding the line in front of them.

common pattern look away first

Import-oriented reading

import { Response as LLMResponse } from "../llm";
import { Config } from "../config";

export function getResponse(cfg: Config): LLMResponse {
  return { text: "hello", model: cfg.model };
}

The callsite looks clean because the boundary moved somewhere else.

BAML boundary inline

Callsite-oriented reading

function GetResponse(cfg: Config) -> root.llm.Response {
  root.llm.Response { text: "hello", model: cfg.model }
}

The line tells you: Config is local, root.llm.Response crosses a namespace.

BAML makes semantic folders opt-in.

Plain folders are just organization. They do not quietly become modules. Only folders with ns_ become namespace boundaries.

This is where the design stops being a BEP bullet and starts feeling like a product choice. Most projects get folders before they get architecture. BAML protects that boring phase.

baml_src/ root
prompts/ organization
types/ organization
ns_llm/ namespace: llm
ns_llm/ns_openai/ namespace: llm.openai

What the compiler does

path ns_llm/models.baml
filter only valid path segments with ns_ count
namespace ["llm"]

This comes from file_package.rs, where namespace names are extracted from ns_* path components.

The repo fixtures show the real rule.

Here is the important part: the design is enforced in tests and compiler code. It is not just an essay about language philosophy.

Root can point into a namespace

function GetResponse(cfg: Config)
-> root.llm.Response

namespaces_basic/main.baml:8 uses root.llm.Response.

A namespace can point back to root

function MakeResponse(
cfg: root.Config
) -> Response

ns_llm/models.baml:8 makes root.Config explicit.

Bare cross-namespace names fail

function UseConfig(
cfg: Config
) -> Response

The diagnostic fixture says this bare Config should fail. Use root.Config.

That is the pitch to people coming from other languages: BAML does not ask you to memorize a bigger module system. It makes the boundary smaller, louder, and harder to accidentally create.

Try the three cases.

The whole system fits in your head if you separate folder organization from namespace boundaries.

Plain folder

A folder like prompts/ is only for humans. Files inside it still belong to the parent namespace.

no new scope

baml_src/
  main.baml
  prompts/
    summarize.baml   # still root namespace

What developers from other languages should notice.

BAML is not trying to win a syntax beauty contest. The point is operational: if LLM apps are going to be read, generated, inspected, and called across languages, the scope model should be visible in the source shape.

TypeScript Aliases and barrels can make names feel local when they are not.
Python Folders can become packages, and import style becomes part of readability.
Rust crate:: is the closest mental cousin to BAML's root..
BAML Plain folder stays plain. ns_ creates scope. root. shows the crossing.

The useful idea is the visual contract.

I would not copy ns_ into every language. That is not the point.

The point is better: if a boundary changes name resolution, make it visually loud. If a folder is just cleanup, let it stay harmless.

That is why BAML namespaces feel more interesting than they look. The syntax is plain. The reading model is the product.