Build an integration
An integration is an npm package. Noemata reads the package’s manifest to learn what the integration is, installs the frames and skills it ships, and, when the package has an entry module, calls that module to configure the collector and to edit the configuration of the software being observed. The integrations Noemata ships are built the same way: they come with the CLI in one package, @noemata/integrations, that provides several integrations under short ids such as claude_code.
This section covers what goes in the package. For how users install one, see Installing integrations from npm.
What you need
- Node.js 22 or later and a package manager. The package is plain ESM; TypeScript is optional.
@noemata/integration-sdk, as a peer dependency, if the package has an entry module. It contains the types and a few helpers and has no dependency on the rest of Noemata.- A Noemata project to test against.
noemata --versionprints the version yourcompatibilityrange must include.
Anatomy of a package
@acme/noemata-docker/├── package.json manifest: identity, compatibility, dependencies, asset paths├── settings.schema.json JSON Schema for the integration's settings (optional)├── src/index.js entry module: probe() and contribute() (optional)├── frames/ frame pack, installed to @frames/@acme/noemata-docker/ (optional)│ ├── README.md│ ├── views/│ └── ...└── skills/ skill pack, one directory per skill (optional) └── noemata-docker/SKILL.mdEvery part except package.json is optional. A package with only a frames/ directory is a complete integration, and so is one with only an entry module.
The manifest
Noemata reads the noemata field of package.json. It never executes package code to learn what the package is, so an incompatible or misconfigured package is rejected before it runs.
{ "name": "@acme/noemata-docker", "version": "1.2.0", "type": "module", "exports": "./src/index.js", "files": ["src", "frames", "skills", "settings.schema.json"], "peerDependencies": { "@noemata/integration-sdk": "^1" }, "noemata": { "name": "Docker", "description": "Per-container CPU, memory, network, and block-I/O metrics", "category": "containers", "signals": ["metrics"], "compatibility": ">=0.5.0", "requires": ["opentelemetry"], "frames": "./frames", "skills": "./skills", "settings": "./settings.schema.json" }}| Field | Required | What it holds |
|---|---|---|
name | yes | The display name shown in the wizard and in noemata integrations list. |
description | yes | One sentence on what the integration collects or installs. |
category | yes | One of coding, databases, host, containers, opentelemetry, noemata. The wizard groups integrations by it. |
signals | yes | Which of metrics, logs, traces the integration collects. An empty array is valid for a package that only installs content. |
compatibility | yes | A semver range of Noemata versions the package supports. See Compatibility. |
requires | no | Ids of integrations that must be installed alongside this one. See Dependencies. |
frames | no | Path, relative to the package root, of the frame pack directory. |
skills | no | Path, relative to the package root, of the skill pack directory. |
settings | no | Path, relative to the package root, of a JSON Schema for the integration’s settings. See Settings. |
required | no | true enables the integration in every project that loads the package, without an entry under installed. Default false. |
hidden | no | true keeps the integration out of the setup wizard; it is enabled by id or through another integration’s requires. Default false. |
The package’s name is the integration’s id: it is the key under integrations.installed in noemata.json, the value other packages put in requires, and the prefix frames use to reference the pack’s views.
The frame pack
The directory named by frames is installed into the project’s workspace as a managed pack. Its directory under @frames/ is the package name, prefixed with @ when the name has no scope: @acme/noemata-docker installs at @frames/@acme/noemata-docker/, and an unscoped noemata-docker at @frames/@noemata-docker/. A scoped name whose scope matches an integration Noemata ships nests inside that integration’s pack directory, for example @frames/@noemata/integration-docker/ inside @frames/@noemata/. Each pack is tracked by its own manifest, so nesting has no effect on how either pack is installed or removed.
Frames inside the pack reference each other, and frames from other packs, by the same path: a view at frames/views/containers.json is @acme/noemata-docker/views/containers to every other frame in the workspace. Reference only the packs you list under requires; the OpenTelemetry pack is the usual dependency, since it defines the services, hosts, and resources most dashboards build on.
The pack is validated when it installs, with the same checks noemata validate runs on hand-authored frames. Ship a README.md at the pack root describing the streams, scalars, entities, and measures the pack defines; that file is what agents read to learn the data. The authoring guides cover how to write the frames themselves.
The skill pack
Each direct child directory of the directory named by skills that contains a SKILL.md is one skill, and the directory name is the skill’s id. Noemata installs every skill into the skill hosts the project has enabled. A skill id must start with noemata-, which is how reconcile tells its installs apart from the user’s own skills in the same directory, and must be unique across the integrations installed in a project, so include your integration’s name, as in noemata-docker.
The entry module
The package’s main export (exports in package.json, as a string, a . entry, or a conditions object, nested or flat; main when there is no exports) is the entry module. It default-exports the result of defineIntegration() from @noemata/integration-sdk:
import { defineIntegration, which } from '@noemata/integration-sdk';
export default defineIntegration({ async probe(ctx) { const binary = await which('docker'); if (!binary) { return { available: false, detail: 'docker is not on PATH' }; } return { available: true, meta: { binary } }; },
contribute(probe, ctx) { return { collector: { receivers: { 'docker_stats/docker': { endpoint: 'unix:///var/run/docker.sock' }, }, pipelines: { metrics: { receivers: ['docker_stats/docker'] }, }, }, installSuggestions: probe.available ? [] : [ { subject: 'Docker', description: 'Docker is needed to collect container metrics.', methods: [{ id: 'brew', label: 'Homebrew', command: 'brew install --cask docker' }], }, ], }; },});probe() runs on every reconcile and reports whether the observed software is present on this host, plus any facts contribute() needs, such as a binary path or a detected version. contribute() then returns everything the integration wants applied: collector configuration, edits to files, install suggestions, and notes for the user. Contributions documents every field of both.
Both functions receive a context with the host platform, the user’s home directory, the workspace directory, the git root when there is one, a logger, and the project’s database connection. contribute() additionally receives the validated settings, the collector’s OTLP endpoints, a view of the other installed integrations, and a file store that reads the state files will have after earlier integrations’ edits are applied.
A failed probe does not disable the integration. Noemata still installs the frame pack and skills, still applies file edits, and still writes the collector component definitions; only the integration’s own collector pipelines stay inactive until a later reconcile finds the software. A probe that throws counts as a failed probe, with the error shown as its detail. Write contribute() so that it returns sensible defaults when probe.available is false.
A package with several integrations
A package that ships integrations which change together, such as a semantic layer and the dashboards built on it, lists them under integrations in the manifest, keyed by id. Each entry holds the fields of the table above, with every path still relative to the package root, plus entry: the subpath of exports that names the integration’s entry module. compatibility stays at the top level and applies to the whole package.
{ "name": "@acme/noemata-kubernetes", "version": "1.0.0", "type": "module", "exports": { "./cluster": "./src/cluster/index.js", "./workloads": "./src/workloads/index.js" }, "noemata": { "compatibility": ">=0.5.0", "integrations": { "kubernetes": { "name": "Kubernetes cluster", "description": "Cluster, node, and pod views", "category": "opentelemetry", "signals": ["metrics"], "required": true, "entry": "./cluster", "frames": "./frames/cluster" }, "kubernetes_workloads": { "name": "Kubernetes workloads", "description": "Deployment and HPA dashboards", "category": "opentelemetry", "signals": ["metrics"], "requires": ["kubernetes"], "entry": "./workloads", "frames": "./frames/workloads" } } }}The keys are the integrations’ ids: the keys under installed, the values in requires, and the pack directories under @frames/. An id is one path segment of lowercase letters, digits, _, and -. An entry without entry has no code and is always available. Users install the package once and enable the ids they want; an entry with required: true is enabled as soon as the package is installed. Two packages in one project cannot provide the same id: the first one to load keeps it and the other is reported.
Developing locally
Check the package without a project first:
noemata integrations check .This reads the manifest, checks the compatibility and SDK peer ranges against the running Noemata, imports each entry module, checks each skill pack and settings schema, and validates the frame packs against the running version beside the shipped packs they require. Every problem is printed and the exit code is non-zero when there is one, so the same command fits the package’s CI.
Then install the package into a test project from its checkout and reconcile:
noemata integrations add ../noemata-dockerA path argument installs the package as a link, so edits to the checkout are visible on the next noemata reconcile without reinstalling. noemata reconcile reruns probe() and contribute(), reinstalls the frame pack, and reports validation errors for the pack’s frames. noemata validate --online additionally renders every frame in the pack against the running stack.
Edits to files outside the repository are off for packages installed from npm until the user trusts the package. During development, set trusted on your package’s entry under integrations.packages in the test project’s noemata.json, or answer the wizard’s prompt when it lists the files your integration would write.
Publishing
Publish the package to npm like any other. Before each release, run noemata integrations check against the package, and:
- Set
compatibilityto the oldest Noemata version you have tested against, as a lower bound such as>=0.5.0. Noemata refuses to load a package whose range excludes the running version, so an upper bound blocks users on every new release; a break that would affect packages arrives as a new SDK major instead. See Compatibility. - Bump the package version. Users pin the version in their project’s
package.json, andnoemata integrations listshows which version each project has. - Keep
@noemata/integration-sdkas a peer dependency at the major version whose API you use. A new SDK major means theprobe()andcontribute()contract changed.