4.2.7 Custom Provider Declaration

Custom providers are the contract language between rules. A producing rule returns a provider instance with semantic data, and a consuming rule reads that provider from its direct dependencies during analysis.1 4.2.5 DefaultInfo & Runfiles answers "what files and runfiles does this target expose by default?". A custom provider answers the domain-specific question your ruleset needs, such as "what headers should downstream rules compile with?" or "what archives should a linker see?"2

A custom provider is the rule-to-rule contract
One global symbol names and documents the fields. Provider instances live on analyzed targets, where consumers can read them.
Provider symbol
LibraryInfo is declared once
one symbol, three uses
Global declaration
LibraryInfo = provider(
fields = ["headers", "archives"],
)
Fields restrict names, not value types. Document the expected shape and meaning of every field.
construct LibraryInfo(...)
require/read providers = [LibraryInfo]
lib/defs.bzl producer
Return default
DefaultInfo(
files = depset([...]),
)
Return custom
LibraryInfo(
headers = headers,
archives = archives,
)
Dependency target
//lib:core exposes providers
default files plus custom data
DefaultInfo files
LibraryInfo custom
headers archives

The provider instance travels on the dependency target, not through output files.

app/defs.bzl consumer
Require deps
"deps": attr.label_list(
providers = [LibraryInfo],
)
Read during analysis
for dep in ctx.attr.deps:
info = dep[LibraryInfo]
info.headers
info.archives
Producer constructs a LibraryInfo instance. Consumer requires and reads that same provider contract.

Declare The Data Shape

Declare a provider once in a .bzl file and bind it to a global symbol. The result of provider() must be stored in a global value if rule or aspect implementations need to use it.3

LibraryInfo = provider(
    doc = "Compilation data needed by downstream library consumers.",
    fields = {
        "headers": "depset of header Files from this target and deps.",
        "archives": "depset of compiled archive Files for linking.",
    },
)

The provider symbol has two jobs. First, it is a constructor for provider instances:

LibraryInfo(
    headers = depset(ctx.files.hdrs),
    archives = depset([archive]),
)

Second, the same symbol is the key used to retrieve that provider from a dependency target: dep[LibraryInfo].4 That is why custom providers are normally exported from a public .bzl entry point when third-party rules are expected to interoperate with them.

doc describes the provider for documentation generators, and fields restricts the allowed field names. The fields value can be a list of names or a dictionary from field name to field documentation. The dictionary form is more useful for public rulesets because it documents each piece of the contract next to the declaration.5 Field declarations do not make fields mandatory by themselves. They define which names may appear. If you need invariants, use a constructor helper or init.

Return Provider Instances

A rule returns providers from its implementation function. In a library-like rule, the implementation usually returns DefaultInfo for the files Bazel should build by default and a custom provider for data that consumers need during their own analysis.6

def _library_impl(ctx):
    archive = ctx.actions.declare_file(ctx.label.name + ".a")
    # Stand-in for the compiler action that would produce the archive.
    ctx.actions.write(
        output = archive,
        content = "",
    )

    transitive_headers = [
        dep[LibraryInfo].headers
        for dep in ctx.attr.deps
    ]

    headers = depset(
        direct = ctx.files.hdrs,
        transitive = transitive_headers,
    )

    return [
        DefaultInfo(files = depset([archive])),
        LibraryInfo(
            headers = headers,
            archives = depset(
                direct = [archive],
                transitive = [
                    dep[LibraryInfo].archives
                    for dep in ctx.attr.deps
                ],
            ),
        ),
    ]

This is where the earlier depset rule becomes concrete: provider fields that describe transitive data should stay as depsets, not lists copied at every node.7 A GoLibraryInfo-style provider uses this shape for a custom Go library rule: each library publishes its own archive and a depset of dependency metadata, then a binary rule flattens that data only when generating the linker configuration.8 The collection mechanics are covered in 4.1.5 depset vs list. The provider declaration is the API boundary that makes those mechanics visible to other rules.

Keep provider fields small and semantic. A provider should not be a dumping ground for every intermediate value the implementation happened to compute. Publish the facts consumers need to analyze their own actions: files, import paths, compile contexts, tool options, or transitive metadata. Internal scratch values should stay local to the implementation function.

Require Providers On Dependencies

Consumers should declare what provider contract they require on dependency attributes. For attr.label and attr.label_list, the providers parameter tells Bazel that every target in the attribute must return the required provider.9

consumer_rule = rule(
    implementation = _consumer_impl,
    attrs = {
        "deps": attr.label_list(
            providers = [LibraryInfo],
            doc = "Libraries that provide compilation data.",
        ),
    },
)

This is an analysis-time API boundary, not a runtime check. Bazel verifies the declared provider requirement while it constructs the configured target graph, before _consumer_impl can read ctx.attr.deps or any consuming action can run. A provider constraint therefore keeps an incompatible target out of the consumer's implementation instead of turning a contract mismatch into a later indexing error.

For example, this BUILD call deliberately passes a file-only target where the consumer requires LibraryInfo:

Won't build
filegroup(
    name = "plain_files",
    srcs = ["note.txt"],
)

consumer_rule(
    name = "broken_consumer",
    deps = [":plain_files"],
)

Analysis stops at the deps attribute. The first diagnostic names the consuming target and attribute, then says that //pkg:plain_files is missing mandatory provider LibraryInfo. Start there: replace the dependency with a target that returns the same LibraryInfo symbol, or change the public contract if file-only inputs are actually valid. Do not add a defensive hasattr() inside _consumer_impl. That code is not the boundary Bazel is reporting, and diagnostic wording and source locations vary by Bazel version.9

Once the constraint has passed, label attributes are resolved to Target objects during analysis, so the implementation can read the provider directly:

def _consumer_impl(ctx):
    archives = depset(transitive = [
        dep[LibraryInfo].archives
        for dep in ctx.attr.deps
    ])

The providers parameter has list-of-lists semantics. providers = [A, B] means each dependency must return both A and B. providers = [[A], [B]] means each dependency may return either A or B.10 For new code, prefer one clear provider contract when you can. The either/or form is useful for compatibility windows, but it makes the consumer responsible for handling multiple shapes.

Use init For Invariants

The plain provider constructor checks field names, but it does not know your ruleset's invariants. If every provider instance must normalize data, reject an empty field, or route construction through a public factory, pass an init callback to provider().11

def _library_info_init(*, headers, archives):
    if not archives:
        fail("LibraryInfo requires at least one archive")
    return {
        "headers": headers,
        "archives": archives,
    }

LibraryInfo, _new_library_info = provider(
    doc = "Compilation data needed by downstream library consumers.",
    fields = {
        "headers": "depset of header Files from this target and deps.",
        "archives": "depset of compiled archive Files for linking.",
    },
    init = _library_info_init,
)

When init is present, calling LibraryInfo(...) forwards the arguments to the callback. The callback must return a dictionary whose keys are provider field names. Bazel then creates the provider instance from that dictionary.12 In this form, provider() returns two values: the public provider symbol and a raw constructor that bypasses init.13 The official docs recommend binding that raw constructor to an underscore-prefixed name so ordinary user code cannot load it directly.14

Use init for provider-wide invariants, not for target-specific validation that belongs in the rule implementation. If the rule needs to validate user attributes, fail near the start of _impl. If every instance of LibraryInfo must have normalized headers no matter which rule created it, put that invariant in init.

Keep The Boundary Modern

Old Starlark rules returned struct values and exposed provider-like data as fields on Target objects. Bazel still documents a migration path for legacy providers, but the legacy style is strongly discouraged and should not be used in new code.15 Modern declared providers avoid name clashes and hide data behind the provider symbol: code can read dep[LibraryInfo] only if it loaded the same provider symbol.16

That symbol-based access is the reason provider design becomes an interface design question. Once another rule depends on LibraryInfo.headers, that field name and meaning are part of the public API. Renaming it or changing its type is more like changing a function signature than refactoring a local variable. The next chapter, 4.2.8 Provider-as-Interface Pattern, focuses on that composability pattern. Here the practical rule is simpler: declare the provider deliberately, document its fields, require it on dependency attributes, and return only data you are willing to support.

The mini-ruleset's complete GlyphInfo declaration documents each field, while glyph_library constructs and returns the provider from direct and transitive data.

key takeaway

Custom providers are typed analysis-phase messages. Declare the provider symbol globally, return provider instances from producing rules, require the provider on dependency attributes, and read it with dep[ProviderName].

Use fields to document and restrict the data shape. Use init only when provider instances need shared construction rules that should hold no matter which rule creates them.

Check your understanding · 4 questions

1.What is the dual role of a custom provider symbol such as LibraryInfo?

Select one answer

2.Which statements describe good custom provider design?

Select all that apply

3.True or false: provider requirements and initialization.

Choose True or False for each sentence

providers = [A, B] means each dependency must provide both A and B.
providers = [[A], [B]] means each dependency may provide either A or B.
The fields argument makes every listed provider field mandatory.
With init, provider() returns the provider symbol and a raw constructor.

4.A consumer_rule has deps = attr.label_list(providers = [LibraryInfo]), but its BUILD call passes a filegroup that does not return LibraryInfo. What should you inspect first?

Select one answer

0 of 4 answered

Footnotes

  1. Rules — providers as pieces of information exposed by a rule to dependent rules.

  2. Rules — examples of rules returning both DefaultInfo and language-specific provider data such as CcInfo.

  3. .bzl filesprovider() defines a provider symbol that must be stored in a global value.

  4. Provider — provider values are both constructors and keys for target indexing.

  5. .bzl filesdoc, fields, and allowed field forms for provider().

  6. Rules — implementation functions return a list of provider objects including DefaultInfo and custom providers.

  7. Writing Bazel rules: library rule, depsets, providers — providers and depsets used together to pass dependency information between rules.

  8. Writing Bazel rules: library rule, depsets, providersGoLibraryInfo carries direct library metadata and a depset of dependency metadata.

  9. Rules — dependency attributes should specify which providers their dependencies must provide. 1 2

  10. attrproviders parameter list-of-lists semantics for label and label-list attributes.

  11. Rules — custom initialization of providers for preprocessing, validation, and cleaner construction APIs.

  12. Rulesinit callback invocation and dictionary return contract.

  13. .bzl filesprovider(init = ...) returns the provider symbol and a raw constructor.

  14. Rules — raw constructors are typically bound to underscore-prefixed private names.

  15. Rules — legacy struct providers are strongly discouraged and supported mainly for migration.

  16. Rules — modern providers avoid name clashes and support data hiding through symbol-based access.