.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:- No
whileloops and no recursion. Iterate withforover finite collections. ifandforstatements only inside functions. At the top level of a file you may assign names, define functions, call rules, andload(). (Conditional expressions likex = a if cond else bare 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 aBUILD.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.
Predeclared symbols
Every build file executes with these names already defined — noload()
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:linux-amd64, linux-arm64, darwin-amd64,
darwin-arm64, windows-amd64.
output_path
A struct of workspace output directories (all live under.rbs/):
glob()
Pattern syntax:
* (any characters except /), ? (one character),
[...] (character class), ** (any files and directories, recursively).
fail()
config_setting()
key. value may be any plain
value — a bool, number, string, list, or dict.
register_toolchain()
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()
env.schema(vars = {...}).
env.schema()
.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()
.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()
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()
-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:
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
genrule, but invokes a registered tool by name with an argument list
instead of a shell string.
task
command is a list[string] argv. Run it with
rbs run //pkg:name.
filegroup
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.