4.10.1 Module Extension Fundamentals
Module extensions are the Starlark plugin layer between MODULE.bazel and external repositories. They let Bazel read structured tags from modules across the resolved dependency graph, run extension logic once for that extension, and create repositories by calling repository rules.1 If 3.1.1 Bzlmod (MODULE.bazel) is the module-level dependency model and 4.9.1 Repository Rule Fundamentals is the mechanism that materializes a repository directory, a module extension is the adapter that turns module-graph declarations into those repository-rule calls.
Tags are gathered after module resolution, not by each module running local setup.
One implementation runs once for the whole graph, then declares repos.
Generation creates repositories. use_repo() maps selected names into this module's apparent namespace.
Why Extensions Exist
MODULE.bazel is deliberately not another WORKSPACE file. Bazel reads module files to construct and resolve the module graph before it fetches most source archives, so MODULE.bazel cannot contain arbitrary load() statements the way WORKSPACE did.2 That is why extension use has a different shape: use_extension() creates a proxy for an extension exported from a .bzl file, calls on that proxy create tags, and use_repo() imports selected generated repositories into the current module's apparent repository namespace.3
bazel_dep(name = "gazelle", version = "0.38.0")
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
use_repo(
go_deps,
"com_google_cloud_go_storage",
"io_opentelemetry_go_otel_sdk",
)
The important part is the separation of intent from materialization. The tag says "this module contributes this piece of dependency information." The generated repository is not visible until the module imports it with use_repo(). Those repo names are therefore part of the extension's public API.4 This explicit import also supports lazy evaluation: Bazel normally does not evaluate an extension until a use_repo()-imported repository is actually needed by the build.5
The Authoring Shape
An extension definition has the same top-level discipline as other Starlark extension points: assign the result of module_extension() to a global symbol in a .bzl file so MODULE.bazel can name it with use_extension().6 Instead of build-rule attributes or repository-rule attributes, it exposes one or more tag_class() schemas. Each tag class defines the attributes users may set on one method of the extension proxy.7
_artifact = tag_class(
attrs = {
"group": attr.string(mandatory = True),
"artifact": attr.string(mandatory = True),
"version": attr.string(mandatory = True),
},
)
maven = module_extension(
implementation = _maven_impl,
tag_classes = {
"artifact": _artifact,
},
)
With that definition, a user can write maven.artifact(...) in MODULE.bazel. Bazel collects those tags from every selected module that uses the same extension identity, then passes them to the implementation through module_ctx.modules.8 The official API guarantees module_ctx.modules is ordered by breadth-first traversal from the root module, which matters when an extension deliberately gives the root module priority or wants deterministic conflict handling.9
The implementation function then resolves the collected data and usually calls repository rules. That is the key relationship: a repository rule is still the thing that writes a repository directory. The module extension decides which repositories should be created after seeing the module graph.10
def _maven_impl(ctx):
artifacts = []
for mod in ctx.modules:
for tag in mod.tags.artifact:
artifacts.append("%s:%s:%s" % (
tag.group,
tag.artifact,
tag.version,
))
# Resolve artifacts, then call repo rules that create the repos.
_generate_maven_repositories(artifacts)
That is why extensions are most useful when a dependency cannot be represented as one direct bazel_dep(): Maven coordinates, Go modules, Cargo crates, generated toolchain repositories, or any ecosystem where Bazel must aggregate declarations, run ecosystem-specific resolution, then expose the result as Bazel repositories.11
One Extension, Many Modules
The trap for new extension authors is thinking of an extension call as local to one module. It is not. Bazel gathers tags belonging to the same extension identity across the whole selected module graph and calls the implementation once for that identity.12 A Go toolchain extension, for example, uses this to select one Go toolchain version from all go.download(...) tags, then generates shared toolchain repositories rather than letting every module instantiate and register its own copies.13
This graph-wide behavior is the main difference from calling a repository rule directly with use_repo_rule(). A direct repo-rule proxy creates repositories for the current module's own scope. A module extension can see contributions from dependency modules and make a coordinated decision.3 Use the direct repo-rule form for simple, local repository declarations. Reach for an extension when the rule needs information from more than one module or when the repository set is part of a reusable ruleset API.
The Circularity Boundary
Module extensions can load() their implementation dependencies from repositories already visible to the module that hosts the extension, and they can create new repositories by calling repo rules. They cannot create a repository and then load() Starlark from that newly created repository in the same extension. This is a real failure mode: trying to define and use a generated repository within one extension produces a circular repository-definition problem.14
The usual design fix is to split the work. One extension creates the configuration or helper repository. A second extension, loaded from already-available Starlark, consumes that repository or calls the legacy setup macros that need it.14 That pattern is more than a workaround. It is a reminder that extension files are part of the module-resolution surface, while the repositories they generate become available only after the extension has run. More advanced split-extension and toolchainization patterns belong in 4.10.4 Extension Design Patterns and 4.11.2 Toolchainization.
Where This Stops
This article is the mental model, not the full implementation checklist. The next item, 4.10.2 Implementation Function, covers module_ctx mechanics, extension_metadata(), and bazel mod tidy integration. 4.10.5 Repo Name Handling handles apparent versus canonical names inside extension-generated repositories. 4.10.6 Hermeticity Considerations covers the same host-dependence risks repository rules have: downloads, execute(), environment reads, OS and architecture dependence, and reproducibility metadata.
For fundamentals, keep the boundary simple: MODULE.bazel declares module dependencies and extension tags. The extension aggregates those tags after module resolution. Repository rules materialize the generated repositories. use_repo() decides which generated repositories the current module may reference by apparent name.
Module extensions are not build rules and not just prettier repository macros. They are graph-aware adapters: users contribute typed tags from MODULE.bazel, the extension reads all matching tags across the selected module graph, then calls repository rules to create the repositories that Bazel can load.
Use them when dependency setup must coordinate information across modules or bridge a non-Bazel package ecosystem into Bzlmod. Keep simple one-off repositories as use_repo_rule() calls, and keep implementation details that need generated repositories out of the same extension to avoid circular definitions.
Check your understanding · 3 questions
1.Which statements describe the basic role of a module extension?
Select all that apply
2.When is use_repo_rule() usually enough instead of a module extension?
Select one answer
3.True or false: visibility and evaluation of generated repositories
Choose True or False for each sentence
use_repo() imports selected generated repository names into the current module's apparent repository namespace.load() Starlark from that same newly created repo inside the same extension.Footnotes
-
Module extensions — definition of module extensions as graph-wide tag readers that create repositories by calling repository rules. ↩
-
Frequently asked questions — why
MODULE.bazeldoes not supportload()and why oldWORKSPACEload-after-repo patterns moved to module extensions. ↩ -
MODULE.bazel files —
use_extension(),use_repo(), anduse_repo_rule()APIs and their visibility scopes. ↩1 ↩2 -
Module extensions — generated repo names are part of an extension's API and must be imported with
use_repo(). ↩ -
Module extensions — lazy module extension evaluation and
bazel mod depsfor forcing evaluation during testing. ↩ -
.bzl files —
module_extension()creates an exported extension symbol used byuse_extension(). ↩ -
.bzl files —
tag_class()creates an attribute schema for extension tags. ↩ -
Writing Bazel rules: module extensions — two-part extension model: tag classes plus implementation function. Bazel gathers tags from all using modules and calls the implementation once globally. ↩
-
module_ctx —
modulesexposes the modules that use the extension, ordered breadth-first from the root module. ↩ -
Module extensions — implementation functions receive
module_ctxand call repository rules to generate repositories. ↩ -
Writing Bazel rules: module extensions — examples of using module extensions for Go, Maven, Cargo, NPM, ecosystem-specific resolution, and repository generation. ↩
-
Bazel External Dependencies Overhaul — original design motivation for extending
MODULE.bazelwith Starlark logic for non-Bazel registries after module dependency resolution. ↩ -
Writing Bazel rules: module extensions — Go toolchain example selecting a version from tags across modules and generating shared toolchain repositories. ↩
-
Migrating to Bazel Modules (a.k.a. Bzlmod) - Module Extensions — WORKSPACE versus MODULE.bazel differences, circular repository-definition constraint, and split-extension workaround. ↩1 ↩2