Providers and registries
A provider is how one external system — a messaging service, a calendar, a notes store — becomes something the platform can use. Understanding providers means separating two things that are easy to conflate: the type that describes a provider, and the instance that exists at runtime.
Types are factories; instances are the runtime object
Section titled “Types are factories; instances are the runtime object”A provider type is a factory. It carries a stable id, a human name, a Zod schema for
its configuration, and a setup function:
import { defineProviderType, ProviderInstance, z } from '@grundlag/core';
const messagingProvider = defineProviderType({ id: 'messaging', name: 'Messaging', config: z.object({ apiKey: z.string() }), setup: async ({ services, config }) => { const instance = new ProviderInstance('messaging'); instance.actions.register(sendMessage); return instance; },});The type is static and describes nothing about a running connection. It says what config a
provider needs and how to bring one to life. A provider instance is the result of
running setup — a ProviderInstance that owns three registries making up its public
surface: actions, events, and entities. Whatever setup registers there is everything
the platform can see; connection handling, caching, and API calls stay private behind it.
A fourth registry, buckets, is the exception that proves the rule. Registering a bucket
declares it rather than publishes it — it makes the provider’s blob storage known before
first use, which a host needs in order to mount routes at boot, but the bucket stays private
until its definition says exposed. See
Store blobs in a bucket.
The distinction matters because one type can produce many instances. The same messaging type, given two different API keys, yields two independent instances with the same capabilities pointed at different accounts.
How ProviderRegistry brings a type to life
Section titled “How ProviderRegistry brings a type to life”ProviderRegistry is where types become instances. When you register(type, config) it:
- Parses
configagainst the type’s schema. Invalid config throws aProviderConfigValidationError— carrying theproviderIdand Zodissues— andsetupnever runs. - Calls
setup({ services, config })with the parsed config and the sharedServicescontainer, and holds onto the resulting instance.
Because validation happens before setup, a misconfigured provider fails loudly at
registration rather than halfway through its first call. The registry passes the same
Services container to every provider, which is how providers reach shared capabilities —
a database, the provider registry itself — without importing anything host-specific. See
Services and dependency injection.
Aggregation: toActions()
Section titled “Aggregation: toActions()”Consumers — the agent, the sandbox, the HTTP API — do not want to walk a list of providers
and drill into each one’s action registry. They want a flat map of callable actions.
toActions() produces exactly that: it lists every registered provider, flattens all their
actions into one Record<string, Action>, and namespaces each key by its provider so that
messaging + sendMessage becomes a single safe function name. Collisions are
deduplicated. The result is one map the consumer can turn into tool definitions, script
bindings, or HTTP routes without knowing which provider anything came from.
This is the payoff of the type/instance split: the registry is the seam between “a set of configured providers” and “a namespace of actions”, and every consumer meets the platform on the far side of that seam.
The role of ProviderTypeRegistry
Section titled “The role of ProviderTypeRegistry”ProviderTypeRegistry is the simpler cousin. It holds the types a host knows how to
create — register, list, get by id — without instantiating any of them. A host uses
it to advertise which providers are available to configure (say, in a UI that offers a
catalogue), then hands a chosen type and its config to a ProviderRegistry to actually set
up. Types are the menu; the provider registry is the kitchen.
Why registries are behaviour-oriented
Section titled “Why registries are behaviour-oriented”Every registry on the platform — actions, events, entities, providers, types — exposes the
same small vocabulary: register, list, get. They are deliberately behaviour-oriented
rather than plain maps. A caller states what it wants to do (register this, list those,
fetch by id) and the registry decides how, which lets it enforce invariants at the boundary:
ActionRegistry rejects a duplicate id with ActionAlreadyRegisteredError;
ProviderRegistry validates config before setup. A bare map could not do that, and
consumers depending on the map’s shape would couple to storage detail instead of behaviour.
The uniform surface also means learning one registry teaches you all of them.
Why providers depend only on core
Section titled “Why providers depend only on core”A provider imports from @grundlag/core and nothing else — never the server, never
an agent. Everything a provider needs is a core contract: the define* builders, the
registries it populates, the Services container it reaches capabilities through. This is
what lets the same provider run unchanged inside an HTTP host, an agent, and a sandbox. The
dependency arrow points one way, down to core, and the runtime that hosts the provider stays
on the far side of the contracts. See Architecture for how the
layers fit together and Primitives for what a provider exposes.