Enrichment plugins
An enrichment plugin reads one source and returns artifacts, typed records that are attached to an atom. The built-in providers, such as OpenGraph, Wikipedia, and npm, use the interface described here.
Before you begin
- A clone of the Intuition Core repository (opens in a new tab),
with its dependencies installed (
bun install). Write the plugin as a workspace package inpackages/, with@0xintuition/atom-enrichmentandzodas dependencies. The enrichment packages are not published to npm. - An understanding of how classification feeds enrichment. See Classification and enrichment.
The npm provider, at packages/atom-enrichment/src/plugins/providers/npm, is
a compact, complete example that needs no API key.
Structure
This plugin enriches atoms whose URL points at a record on a fictional
service, example.org. It needs an API key, and declines when the key is
absent.
import {
type ClassificationDefinition,
defineEnrichmentPlugin,
type EnrichmentPlugin,
type EnrichmentRequest,
} from '@0xintuition/atom-enrichment';
import { z } from 'zod/v4';
const PLUGIN_ID = 'example-records';
const ARTIFACT_TYPE = 'example-record';
const recordSchema = z.object({
id: z.string(),
title: z.string(),
description: z.string().optional(),
});
// The artifact type, registered with the engine when you add the plugin.
export const exampleRecordArtifactType: ClassificationDefinition = {
slug: ARTIFACT_TYPE,
displayName: 'Example record',
category: 'knowledge',
dataSchema: recordSchema,
};
function parseRecordId(request: EnrichmentRequest): string | undefined {
const url = request.input.hints?.url;
const match = url?.match(/^https:\/\/example\.org\/records\/([a-z0-9-]+)$/);
return match?.[1];
}
export function createExampleRecordsPlugin(
options: { apiKey?: string } = {},
): EnrichmentPlugin {
return defineEnrichmentPlugin({
id: PLUGIN_ID,
version: '1.0.0',
runtime: 'universal',
artifactTypes: [ARTIFACT_TYPE],
priority: 40,
TTL: 3_600,
supports(request) {
return Boolean(options.apiKey) && parseRecordId(request) !== undefined;
},
async enrich(request, ctx) {
const id = parseRecordId(request);
if (!id || !options.apiKey) return [];
const response = await fetch(`https://api.example.org/records/${id}`, {
headers: { authorization: `Bearer ${options.apiKey}` },
signal: ctx.signal,
});
if (response.status === 404) return [];
if (!response.ok) {
throw new Error(`HTTP ${response.status} from api.example.org`);
}
const record = recordSchema.parse(await response.json());
return [
{
artifact_type: ARTIFACT_TYPE,
data: record,
meta: {
pluginId: PLUGIN_ID,
provider: PLUGIN_ID,
identityStrength: 'identifier',
fetchedAt: ctx.now(),
sourceUrl: `https://example.org/records/${id}`,
},
},
];
},
});
}Plugin fields
| Field | Meaning |
|---|---|
id | A unique identifier: 2 to 40 lowercase letters, digits, and hyphens, starting with a letter |
version | The plugin's semantic version |
runtime | server, client, or universal, which covers both |
artifactTypes | The artifact types the plugin can return. Each follows the same naming rule as id |
priority | Lower values run earlier. The default is 100 |
TTL | How long, in seconds, the engine caches the plugin's artifacts. Omit it to disable caching for the plugin |
supports | A fast check of whether the plugin applies to the request. It must not make network requests |
enrich | Fetches the data and returns artifacts. Return an empty array when there is nothing to return |
The request
supports and enrich receive the classified atom in request.input:
| Field | Contents |
|---|---|
atomType | The atom's category, such as person, song, or software |
jsonLd | The schema.org document that classification produced |
hints | Optional name, description, url, identifiers, and locale |
targets | Parsed targets for some platforms, such as a GitHub repository or an X profile |
The context
enrich also receives a context:
| Member | Purpose |
|---|---|
now() | The current time as an ISO 8601 string. Use it for meta.fetchedAt |
signal | An abort signal. Enrichment runs under a deadline, so pass it to every request |
logger | An optional logger with debug, warn, and error |
secrets | Optional secrets supplied by the host |
Artifacts
Each artifact has an artifact_type, its data, and a meta object.
meta.pluginId, meta.provider, and meta.fetchedAt are required.
meta.sourceUrl, meta.confidence, and meta.identityStrength are optional.
The engine validates data against the schema registered for the artifact
type. An artifact whose type is not registered, or whose data does not match,
is dropped and reported as a validation_error. Register your artifact type
when you add the plugin to your node.
Requirements
- Validate output with a schema. Parse the source's response before returning it. Unvalidated data from a third party should not enter the graph.
- Treat credentials as optional. Accept keys as options. If a key is
absent, return
falsefromsupports. Never throw for a missing credential. This is what allows a node to run without paid accounts. - Honor
ctx.signal. Pass the abort signal to every request. - Fail softly. Treat a 404 as an empty result. Throw for an unexpected
failure. The engine records the error for your plugin alone and still
stores the other plugins' artifacts. It classifies the error by its
message: timeouts, rate limits (
429), and upstream errors (a message that mentionsHTTP) are marked retriable. - Set an accurate
TTL. Use a short value for volatile data, such as prices, and a long one for stable data, such as publication metadata. - Read one source per plugin. Combine sources in a preset, not inside a plugin.
Identity strength
Set meta.identityStrength to describe how the plugin found the resource.
| Value | Use when the resource was found by |
|---|---|
identifier | An external identifier |
url | A direct URL to the resource |
title | A search by name |
A search by name is the weakest of the three, because a name can match the
wrong thing. An artifact found by name can contribute only descriptive
fields, such as names, descriptions, images, and dates. The full list is
exported as TITLE_STRENGTH_DESCRIPTIVE_FIELDS from
@0xintuition/atom-enrichment/extraction. Every other field, such as an ISBN
or a repository URL, is treated as identity-bearing, and a search by name cannot supply it. An
artifact without identityStrength is treated as identifier.
Add the plugin to your node
Atom services and the enrichment worker build their engine in the same file,
services/atom-services/src/service/runtime.ts. A plugin you add there
serves both POST /v1/enrich and every atom that passes through the node.
-
Add your package to the dependencies in
services/atom-services/package.jsonand runbun installfrom the repository root. -
In
runtime.ts, add the plugin to the default preset. FindcreatePresetFactoriesand extendallProviders:const allProviders = () => [ ...serverDefaultPreset(serverDefaultPresetOptions), createExampleRecordsPlugin({ apiKey: env.EXAMPLE_RECORDS_API_KEY }), ]; -
In the same file, register the artifact type. Create a registry that contains the built-in types and yours, and pass it to
createEnrichmentEngineincreateEnrichmentRuntime:import { createDefaultClassificationRegistry } from '@0xintuition/atom-enrichment'; const artifactTypes = createDefaultClassificationRegistry(); artifactTypes.register(exampleRecordArtifactType); // In createEnrichmentRuntime, inside createEngine: createEnrichmentEngine({ plugins: presetFactories[preset](), cache: cacheRuntime.enrichmentCache, classifications: artifactTypes, // The existing identity option stays as it is. }); -
Add
EXAMPLE_RECORDS_API_KEYto.env. Docker Compose passes only the variables listed indocker-compose.yml, so also add it to theenvironmentofatom-servicesand of the workers. -
Rebuild and restart the services:
docker compose up -d --build atom-services workers-enrichment -
Process an input your plugin supports:
curl -X POST http://localhost:4010/v1/process \ -H 'Content-Type: application/json' \ -d '{"rawInput":"https://example.org/records/123"}'The artifact appears in the response's
enrichment.artifacts. If it does not, look for your plugin inenrichment.skippedandenrichment.errors.
For atoms processed by the workers, artifacts are stored in kg.artifacts,
with the artifact type in artifact_kind.
Related
- Classification plugins, for sources that imply a type as well as metadata
- Enrichment providers
- Atom services