Writing a plugin
document:docs/pluginsTeach ac scan and ac drift to read events from a format of your own.
A plugin is an npm package that teaches autocatalog a new event format: a
company contract format, a registry export, a framework’s decorators. Install
it next to autocatalog, and ac scan and ac drift read your events the way
they read JSON Schema, into the same catalog, site and breaking-change gate.
EventCatalog has plugins too, and its generators write into the catalog. A plugin
here returns what it found and writes nothing, so ac drift can run it on two
commits and compare.
autocatalog-plugin-zod is one. Yours works the same way.
You may not need one
Section titled “You may not need one”If your team already keeps its events in a file of its own, describe that file
instead of writing code. Add sources to
contracts/autocatalog.conventions.json:
{ "$schema": "https://unpkg.com/autocatalog-docs-core/contracts/autocatalog.conventions.schema.json", "sources": [ { "kind": "event", "files": ["packages/sdk/event-schemas.json"], "match": ["v{version}.{id}", "v{version}/{id}"], "domain": "payments", "meta": { "owner": { "team": "payments" }, "producers": ["payments-api"] } } ]}fileslists JSON files in the repository. Each is one object with an event per key and that event’s JSON Schema as the value.matchsays how a key names an event, and the first pattern that fits wins.{id}is required. Without{version}, the event is0.1.0and marked for review. Everything else is plain text, not a regular expression.domainis the domain of every event in those files.metaholds what the files cannot say, for all their events. It takes the fields of anx-autocatalogblock.
ac scan reports a key that fits no pattern, and a listed file this repository
does not have, and carries on. When a file holds several versions of an event,
the newest is published and the rest are reported. The site reads earlier
versions from git, through today’s mapping, so a commit from before you added it
still counts. A schema a generator wrapped in a named
definition, as zod-to-json-schema does, is unwrapped.
The event is the whole message, envelope included: a consumer receives all of it, so a change to a shared field is a change to every event.
A mapping runs no code, so ac reads one without --allow-code-execution. If
those files are generated, have CI regenerate them and fail on a difference, or
the catalog reads a stale copy.
One file for every team
Section titled “One file for every team”Publish the conventions file once, in an npm package or a repository every team has, and name it:
ac scan --conventions node_modules/@acme/conventions/autocatalog.conventions.jsonac drift --base-ref origin/main --conventions node_modules/@acme/conventions/autocatalog.conventions.jsonIt replaces the repository’s own file, and ac drift --base-ref reads the same
one on both sides. A repository with none of the listed files finds no events.
Writing to the contract yourself
Section titled “Writing to the contract yourself”To skip mapping altogether, write what the catalog reads. These JSON Schemas are generated from the code that reads the files:
| Contract | Describes |
|---|---|
autocatalog-core/contracts/event-manifest.schema.json | An event’s autocatalog.json |
autocatalog-core/contracts/graph-v1.schema.json | The resource graph at /catalog.json |
autocatalog-agents-core/contracts/<kind>.schema.json | An agent, tool, mcp-server or other manifest |
autocatalog-docs-core/contracts/autocatalog.conventions.schema.json | The conventions file |
They cover a file’s shape. A few checks have no JSON Schema form, such as a
snapshot header holding a ${NAME} rather than a value, and ac still applies
those when it reads the file. For types, import from the same packages.
The package
Section titled “The package”ac finds plugins in the project’s node_modules by name:
autocatalog-plugin-<name>, or the same under your scope,
@acme/autocatalog-plugin-<name>. A company plugin can stay on a private
registry.
The autocatalog.plugin block in package.json says what it adds:
{ "name": "@acme/autocatalog-plugin-orders", "version": "1.0.0", "type": "module", "autocatalog": { "plugin": { "description": "Events from the order service's contract files", "discovery": { "entry": "./index.js", "export": "ordersPlugin" } } }}discoverynames the module and exportac scanandac driftcall.commandandentryadd a subcommand:"command": "from-orders"makesac from-orders, which runsentrywith the arguments it was given.
A package can declare both.
The scanner
Section titled “The scanner”The export is a DiscoveryAdapter from autocatalog-core: a name and a
discover function. discover gets the repository root and returns each
event it found with the file it came from.
import fs from 'node:fs/promises';import path from 'node:path';
/** Every `orders/*.order-event.json` file is one event. */export const ordersPlugin = { name: 'orders', async discover({ root }) { const dir = path.join(root, 'orders'); const files = (await fs.readdir(dir).catch(() => [])).filter((f) => f.endsWith('.order-event.json'), ); const candidates = []; const errors = [];
for (const file of files) { try { const contract = JSON.parse(await fs.readFile(path.join(dir, file), 'utf8'));
candidates.push({ event: { id: contract.id, version: contract.version, name: contract.name, summary: contract.summary, domain: 'orders', folderName: contract.name.replaceAll(' ', ''), schema: contract.schema, examples: [], }, finding: { file: path.join('orders', file), symbol: contract.id, kind: 'order-event', id: contract.id, needsReview: false, source: 'orders', }, }); } catch (error) { errors.push(`${file}: ${error.message}`); } }
return { candidates, errors }; },};event.schemais a JSON Schema object. Drift compares these between commits, so the same contract must give the same schema every run.finding.needsReview: truemarks an event the plugin guessed at. The catalog shows it as needing review.ac scanreports each oferrorsand fails. Return one per unreadable file, and keep going.
With TypeScript, type the export as DiscoveryAdapter from autocatalog-core
and the compiler checks every field.
Running it
Section titled “Running it”A plugin runs code, so ac asks first:
ac scan --allow-code-executionWithout the flag, ac scan stops and names the installed plugin. With it,
ac runs the plugin in its own Node process, with read access to the
repository and its dependencies, a memory cap, a 30 second timeout and a 16 MB
limit on what it returns. It needs Node 24 or later.
ac names the scanner after its package: @acme/autocatalog-plugin-orders
reports as @acme/orders, so each company’s plugins keep names of their own.
Renamed from adapters
Section titled “Renamed from adapters”Plugins used to be called adapters. A package named autocatalog-adapter-<name>,
or declaring autocatalog.adapter, keeps working. If both the old and new names
of a package are installed, ac uses the new one. autocatalog-adapter-zod is
now autocatalog-plugin-zod, with the same exports. The TypeScript type is still
DiscoveryAdapter.
Testing it
Section titled “Testing it”Call discover on a fixture folder and check the candidates. Then install the
package into a scratch project, add one contract file, and run
ac scan --allow-code-execution: the event appears under events/.