4.4.1 Rule Public API Design

A production rule is a public API in the same practical sense as a library function: downstream BUILD files call it, other rules may consume its providers, humans expect its default outputs to stay predictable, and tools may request its named output groups. The implementation can change, but the surface that users write and consumers read needs deliberate design.1

A rule API has several consumers — and each remembers names
Implementation details may change. Published call sites, providers, outputs, and docs need compatibility.
BUILD AUTHORS
Call the rule
attrs + defaults + labels Names such as srcs and deps become the user-facing schema.
my_library(
name = "core",
srcs = ["core.my"],
deps = [":runtime"],
)
CONSUMING RULES
Read semantic data
MyLibraryInfo fields Provider symbols and field meanings form the rule-to-rule interface.
info = dep[MyLibraryInfo]
headers = info.headers
archives = info.archives
BAZEL, CI, AND IDEs
Request artifacts
DefaultInfo + OutputGroupInfo Default outputs stay predictable. Named groups are available when a consumer requests them.
DefaultInfo(
files = depset([archive]),
)
OutputGroupInfo(
debug_files = depset([manifest]),
)
bazel build //app \
--output_groups=debug_files
RULESET USERS
Discover supported entry points
defs.bzl + docs + examples Public loads and documented behavior tell users what they may rely on.
load("//:defs.bzl", "my_library")
doc = "Compiles MyLang sources."
examples/basic/BUILD.bazel
Plan evolution at every named surface
saferAdd optional attrs with safe defaults. Add provider fields or output groups without removing old names. migrationRenaming or removing a used surface needs a transition plan.
Compatibility belongs to the consumers of the rule, not only to its implementation file.

The Surface Is Larger Than attrs

The first visible part of a rule API is the attribute set declared in rule(attrs = ...). Attribute schemas decide what users may write, which files or targets are accepted, which providers dependencies must expose, and which values are documented for Stardoc or similar tooling.2 That is the rule-function contract from 4.2.1 Rule Function, but the production question is stricter: "will this still be a good call site after hundreds of targets depend on it?"

Use ordinary names when the meaning is ordinary. The official style guide recommends srcs for source files, deps for compilation dependencies, data for runtime files, and runtime_deps for runtime-only dependencies.3 A custom name is useful only when the role is genuinely custom. The goal is not cleverness. It is a BUILD file another engineer and a tool can scan quickly.

Private attributes are part of the same design. If the rule always uses a compiler, formatter, or helper binary, hide that implementation dependency behind an underscore-prefixed attr such as _compiler instead of making every user wire it by hand.4 When that private attr is executable, build it in the execution configuration with cfg = "exec" so the tool runs where the action runs, not necessarily where the final artifact runs.5 If the tool varies by platform or user setup, the stable public surface may need to move from a private attr to a toolchain in 4.6.2 Defining, Registering & Accessing Toolchains.

my_library = rule(
    implementation = _my_library_impl,
    attrs = {
        "srcs": attr.label_list(
            allow_files = [".my"],
            doc = "Source files compiled by this library.",
        ),
        "deps": attr.label_list(
            providers = [MyLibraryInfo],
            doc = "Libraries that expose MyLibraryInfo.",
        ),
        "_compiler": attr.label(
            default = Label("//tools/my:compiler"),
            executable = True,
            cfg = "exec",
        ),
    },
    doc = "Compiles MyLang sources into a reusable library.",
)

This is not just input validation. The providers = [MyLibraryInfo] line tells users which interface a dependency must support before the implementation reads dep[MyLibraryInfo] during analysis.6

Providers Are the Consumer API

Custom providers are the rule-to-rule interface. A provider carries semantic information that another rule needs during analysis: headers, archives, import paths, descriptors, source maps, or tool-specific metadata.7 That is why 4.2.7 Custom Provider Declaration treats provider fields as contract fields, not temporary structs.

Design provider fields around capabilities, not around the current implementation. A field named archives says "consumers may link these files." A field named temp_outputs says "I leaked my implementation." A GoLibraryInfo-style provider exposes a direct library metadata struct plus a depset of transitive dependency metadata. The binary rule consumes that provider to generate linker input without depending on the producing rule's internal code layout.8

If other rulesets should implement compatible producers, export the provider symbol from the public .bzl entry point. Bazel provider access is symbol-based: the provider symbol is both the constructor and the key used as dep[ProviderName].9 Keeping a provider private is therefore an intentional choice: only code that can load that symbol can participate in the interface.

Default Outputs, Output Groups, and Internals

A rule often produces more files than a normal user wants from bazel build //pkg:target. DefaultInfo.files is the ordinary build result: the depset of files Bazel builds by default when the target is requested.10 For executable and test rules, DefaultInfo(executable = ...) also names the file Bazel launches, and DefaultInfo(runfiles = ...) carries runtime files.11

Output groups are a different public surface. They publish named depsets of files that users and tools can request selectively with --output_groups, or through filegroup(output_group = "...") in a BUILD file.12 Use them for categories like debug_files, lint_reports, source_maps, or ide_info when those files should be buildable on demand but not part of the target's normal output set.

return [
    DefaultInfo(files = depset([archive])),
    MyLibraryInfo(
        headers = headers,
        archives = depset([archive]),
    ),
    OutputGroupInfo(
        debug_files = depset([debug_manifest]),
    ),
]

Do not use output groups as a substitute for provider contracts. OutputGroupInfo should not convey file categories to consuming rule actions. If another rule needs semantic data, define a rule-specific provider instead.13 The practical split is:

SurfaceUse it for
DefaultInfo.filesFiles a normal build of the target should produce.
Custom providersSemantic data other rules need during analysis.
OutputGroupInfoNamed artifact sets humans, CI, IDEs, or aspects request selectively.
Internal File valuesIntermediates used only by this rule's actions.

Documentation Is Part of the API

Public Starlark APIs should be documented where the API is declared: put doc on rules, aspects, attributes, providers, and provider fields.14 Reusable rulesets should also expose a clear entry point such as defs.bzl, keep a top-level README that tells users what API to expect, and include examples that show supported usage.15

That layout matters because users copy what they can find. If //internal:rules.bzl is easier to load than //:defs.bzl, someone will depend on it. A stable public entry point lets you reorganize internals without forcing users to change load statements: load internal definitions into a public def.bzl and re-export the symbols users should load.16 The ruleset-level version of this concern continues in 4.11.1 Ruleset Layout & Public Entry Points.

Generated API docs are the mechanical backstop. Stardoc can extract documentation from Starlark declarations, and modern BCR documentation workflows can publish generated Starlark docs with the released module so users discover API docs next to the artifact they depend on.17 The detailed documentation item is 4.5.4 Stardoc — API Documentation, while example workspaces as executable documentation are covered in 4.5.3 Example Workspaces as Contract Tests.

Plan For Evolution Before Users Arrive

Once users depend on an attr, a provider field, an output name, or an output group string, changing it is a migration. Additive changes are easiest: add a new optional attr with a safe default, add a new provider field while keeping the old one, or add a new output group without removing the old group. Breaking changes need a compatibility plan, not just a release note.

Bazel's rule compatibility guidance frames the goal as a manageable migration: users should not be forced to upgrade a ruleset major version and a Bazel major version at the same time.18 For a production rule API, apply that same idea locally. If an old provider field must go away, keep both fields for a window, document the replacement, test both paths, and remove the old field in a release that is clearly allowed to break consumers. The broader ruleset policy belongs in 4.11.6 Rule Compatibility & Bazel Version Policy.

For a concrete facade, inspect the mini-ruleset's defs.bzl, which deliberately re-exports only its supported providers, rules, and macros.

For a production-shaped contrast, bazel_rules_hdl re-exports its public synthesis symbols from synthesis/defs.bzl, while place_and_route/build_defs.bzl consumes a typed synthesis provider and coordinates private implementation steps.19 Its public place_and_route target kind accepts SynthesisInfo and orchestrates the private OpenROAD stages without exposing each one as a target kind. Consumers still load synthesis, place-and-route, and Verilog rules from their respective public files. The stages and attributes remain specific to this ruleset.

key takeaway

A production rule API has four public faces: attrs users write, providers other rules consume, outputs users and tools request, and documentation/examples that teach the supported path.

Keep implementation tools private, provider fields semantic, default outputs small, output groups named and stable, and public .bzl entry points obvious. Most rule maintenance pain comes from treating one of those surfaces as internal after users have already built on it.

Check your understanding · 3 questions

1.Match each rule API surface to the role it should play.

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
DefaultInfo.files
Custom provider
OutputGroupInfo
Private attr such as _compiler

2.Which choices make a production rule API easier to evolve safely?

Select all that apply

3.True or false: designing attrs, providers, and outputs for reusable rules.

Choose True or False for each sentence

A dependency attr can require a provider so invalid dependencies fail before the implementation reads that provider.
Output groups are a good replacement for custom providers when another rule needs semantic dependency data.
An executable private tool attr should usually use cfg = "exec" so the tool is built for the execution platform.
Provider fields are safe to rename without migration because only the producing rule sees them.
0 of 3 answered

Footnotes

  1. Rules - rules define attributes, actions, outputs, providers, and implementation behavior.

  2. Rules - attribute schemas, common attributes, provider requirements, and documentation on rule attributes.

  3. .bzl style guide - standard rule names and attribute conventions for srcs, deps, data, and runtime_deps.

  4. Rules - private attributes and implicit dependencies for implementation tools.

  5. Rules - executable dependency attributes should choose cfg = "exec" for build-time tools.

  6. Rules - dependency attributes can require providers that dependencies must return.

  7. Rules - providers expose information to rules that depend on the target.

  8. Writing Bazel rules: library rule, depsets, providers - GoLibraryInfo provider and depset-based transitive metadata example.

  9. Writing Bazel rules: library rule, depsets, providers - public def.bzl exports go_library and GoLibraryInfo for compatible consumers.

  10. DefaultInfo - files field describes default outputs built from the command line.

  11. DefaultInfo - runfiles and executable constructor parameters. FilesToRunProvider via files_to_run.

  12. Rules - output groups can be requested with --output_groups and defined with OutputGroupInfo.

  13. Rules - output groups should not replace rule-specific providers for consumer actions.

  14. .bzl style guide - document rules, aspects, attributes, providers, and provider fields with doc.

  15. Deploying Rules - ruleset layout, README/API expectations, defs.bzl entry point, tests, examples, and documentation.

  16. Writing Bazel rules: simple binary rule - public def.bzl entry point re-exporting internal rule symbols.

  17. Bazel Starlark Docs on the Registry - starlark_doc_extract, bzl_library, docs_url, and BCR rendering for published Starlark API docs.

  18. Rule Compatibility - manageable migration avoids forcing users to upgrade a ruleset major version and Bazel major version simultaneously.

  19. bazel_rules_hdl — open HDL and silicon-design rulessynthesis/defs.bzl defines the public synthesis facade, while place_and_route/build_defs.bzl demonstrates a provider-linked rule coordinating private implementation steps.