Skip to main content
Every rbs build file is written in the RBS language, a deterministic, Python-like configuration language. Files use the .rbs extension. This page is the language reference for anyone writing build files; see Custom Rules for the rule-authoring API and External Rule Packages for ext.rbs.

File types

The language in .rbs files

If you know Python, you already know most of the RBS language:
The dialect rbs evaluates is deliberately restricted so build files stay analyzable and terminate:
  • No while loops and no recursion. Iterate with for over finite collections.
  • if and for statements only inside functions. At the top level of a file you may assign names, define functions, call rules, and load(). (Conditional expressions like x = a if cond else b are fine anywhere.)
  • Top-level names are assigned once — no reassigning a global.
  • No implicit string concatenation. Adjacent string literals are a parse error; see Gotchas.
  • print() writes to the build output; fail() aborts the build.

Build file structure

A directory with a BUILD.rbs is a package. A build file loads the rules it needs, then declares targets by calling them:

load()

load() executes another .rbs file (once — modules are cached) and binds selected top-level names from it into the current file. If the path has no extension, rbs tries .rbs and then .rbi. Load cycles are detected and reported as errors.
load() bindings are file-local — they are not re-exported. A module that loads a symbol does not make it loadable from itself. An aggregator module must rebind explicitly:

Predeclared symbols

Every build file executes with these names already defined — no load() needed for any of them: In addition, every registered rule is predeclared by its bare name — built-in rules like genrule, task, and filegroup can be called without the native. prefix, and a rule defined with native.define_rule in a loaded module is callable wherever it is loaded.
env and infra are predeclared in every .rbs file. There is no @rbs//env/... or similar module to load — writing such a load() is an error.

platform

A struct describing the platform being built for:
Supported platform names: linux-amd64, linux-arm64, darwin-amd64, darwin-arm64, windows-amd64.

output_path

A struct of workspace output directories (all live under .rbs/):

glob()

Returns a sorted list of files matching the patterns, as paths relative to the current package directory. Pattern syntax: * (any characters except /), ? (one character), [...] (character class), ** (any files and directories, recursively).

fail()

Stops evaluation and fails the build with the message (exit code 1).

config_setting()

Stores a global configuration value under key. value may be any plain value — a bool, number, string, list, or dict.

register_toolchain()

Registers a toolchain by name with arbitrary attributes (kind or toolchain_type classifies it). Rules reference a registered toolchain via their toolchain = parameter, and rule implementations read its attributes through ctx.toolchain. In practice most workspaces use a language toolchain rule instead of calling this directly:

workspace()

WORKSPACE.rbs only. Declares the workspace’s identity. The name must be letters, digits, ., - or _, starting with a letter or digit, and may only be declared once.

Attributes every rule accepts

Beyond each rule’s own attributes, these work on every target:

Labels

Targets are addressed by label: @rbs//... and @<namespace>//... are load paths for rule modules, not target labels.

Environments: the env module

Environment handling is schema-driven. A .env.schema file (written in the same build language) declares environments and variables; plain dotenv files supply the values (.env base, .env.<name> per-environment overlay, .env.local personal, gitignored); targets bind an allow-list with env.vars(). The environment is selected with -e <name> / RBS_ENV, and an undeclared name is a hard error. Validation happens at analysis time — a missing required variable fails the build, not the process at runtime.

env.var()

Declares one variable. Returns a value usable in env.schema(vars = {...}).

env.schema()

Composes variables into a schema. Called from a .env.schema file it registers as that package’s schema; assigned to a name in a shared module it becomes a reusable standard. Calling env.schema() twice in one .env.schema is an error — compose with extends instead. Conflicting declarations of the same variable across extends are an error unless the extending schema restates the variable explicitly.

env.environment() and env.files()

The root .env.schema declares which environments exist — rbs hardcodes none. extends layers another environment’s overlay file underneath; infra links the environment to an infra.environment(). env.files() overrides the overlay filename pattern (default .env.{env}); the pattern must contain {env}, and one workspace has one pattern.

env.vars() and env.all()

Both return a plain dict[string, string] for a target’s env attribute. env.vars() binds exactly the named, schema-declared variables — only those reach the process and only those enter its cache key. A name not declared in any reachable schema is an analysis-time error listing the declared names. override layers literal values on top (they win over files). env.all() injects every declared variable, at the cost of a wider cache key.

env.name()

Returns the selected environment name (-e / RBS_ENV), so schemas can make declarations conditional.

Value sources

A variable’s value normally comes from the env files. source = points it elsewhere:
registers a custom resolver (Vault, cloud secret managers, …) at module top level.

Infrastructure: the infra module

infra is predeclared everywhere and is the surface for infrastructure-as-code definitions. Its members, grouped: See the infrastructure guides for end-to-end usage; this page only records that the module exists in every file with these members.

The native module

native exposes every built-in rule and SDK function. All of them are also predeclared by bare name, so genrule(...) and native.genrule(...) are the same call.

Built-in target rules

genrule

run_tool

Like genrule, but invokes a registered tool by name with an argument list instead of a shell string.

task

A runnable command. command is a list[string] argv. Run it with rbs run //pkg:name.

filegroup

Groups files under one label, typically fed by glob():

Rule and SDK definition functions

These register capabilities rather than declaring targets. Most run at module top level in rule packages; auto-registration at top level is the mechanism — the module executes, the definition registers. Companion list_* functions (list_skills, list_subagents, list_mcp_servers, …) enumerate what is registered, and get_toolchain_attribute(toolchain, attribute) / toolchain_path(...) read toolchain metadata.

Utility functions

The file, directory, JSON, archive, HTTP, and tool helpers (native.file_read, native.dir_create, native.json_parse, native.archive_extract, native.http_download, native.tool_run, …) are the same operations exposed on ctx inside rule implementations, where they are documented: see Custom Rules → the ctx API.

Gotchas

No implicit string concatenation. Unlike Python, adjacent string literals do not concatenate — it is a parse error. Use + or join:
load() does not re-export. See load() — aggregator modules must rebind (load("x.rbs", _y = "y") then y = _y). env and infra are predeclared. Never write a load() for them. env = ... works on every rule — even rules whose definition never declared an env attribute, including rules you define yourself. Control flow lives in functions. A top-level if or for statement is a parse error; wrap logic in a def and call it. struct() is only predeclared in loaded modules. Build a struct in a helper module and load it, rather than calling struct() directly in a BUILD.rbs.