Skip to content

Building a Plugin ​

A plugin is any process that:

  1. Connects to RabbitMQ and binds a queue to the routing key pipeline.step.dispatched.<your-plugin-id> on the datacore.resource-lifecycle topic exchange.
  2. On each message, does its work, then calls back: POST {CORE_API_URL}/api/v1/internal/artifacts/{resource_id}.
  3. Nothing else. It never calls Core synchronously except that one callback, and it never talks to another plugin directly (Plugin Isolation).

The four shipped samples (plugins/markdown-summarizer, plugins/vector-embedder, plugins/qdrant-register, plugins/github-profile-scanner) are ~100-150 lines of TypeScript each and are the best reference — copy whichever is closest to what you're building.

The dispatch message you'll receive ​

json
{
  "event": "PIPELINE_STEP_DISPATCHED",
  "resource_id": "uuid",
  "occurred_at": "ISO-8601",
  "payload": {
    "pipeline_id": "uuid",
    "step_position": 0,
    "plugin_id": "your-plugin-id",
    "attempt_count": 0,
    "source_uri": "s3://bucket/key or https://...",
    "upstream_artifacts": [{ "type": "SUMMARY", "external_ref": "...", "producing_plugin_id": "..." }]
  }
}

upstream_artifacts gives you whatever earlier steps in the pipeline produced, in case your step needs them as input (e.g. Vector Embedder embeds the Markdown Summarizer's output rather than re-fetching the raw source, and Qdrant Register reads the vector Vector Embedder just computed).

The callback you must send ​

json
// success
{ "plugin_id": "your-plugin-id", "step_position": 0, "outcome": "SUCCESS", "artifact": { "type": "SUMMARY", "external_ref": "s3://bucket/key" } }

// failure (Core will retry per that step's configured policy, or mark the resource FAILED once exhausted)
{ "plugin_id": "your-plugin-id", "step_position": 0, "outcome": "FAILURE", "error": "a specific, human-readable reason" }

artifact.type can be one of the built-in types (VECTOR, GRAPH, SUMMARY) or a new type your plugin defines — adding a new artifact type is a one-line Prisma schema change plus a migration on the Core Warehouse side (see github-profile-scanner's REPO_ANALYSIS type for a worked example).

external_ref is a reference into wherever you stored the actual content — Core never asks you to send raw artifact bytes over the callback. Store your output in your own bucket/collection (or reuse Core's MinIO/Qdrant if you're deploying alongside it) and just report back a locator string.

Where your artifact's content gets viewed ​

If you want your artifact to be viewable in the Web UI (via the artifact chip → "view result" modal) and cleaned up on delete, Core needs to know how to fetch and delete its content given external_ref. Out of the box, that means either an s3:// prefix (fetched from/deleted in MinIO) or a qdrant:// prefix (fetched from/deleted in Qdrant) — see backend/src/routes/resources.ts. Report an external_ref in one of these two forms and viewing and cleanup just work, with zero extra code.

Storage as a plugin ​

A different storage backend (Pinecone, Weaviate, a different Qdrant/S3-compatible instance, anything) isn't a special mode bolted onto a processing plugin — it's just another plugin, chained as an ordinary pipeline step. qdrant-register is the worked example:

  1. vector-embedder computes an embedding and writes the raw vector to a temporary spot in the shared object store (s3://bucket/vectors-pending/{resource_id}.json), reporting that as an intermediate VECTOR artifact.
  2. qdrant-register (the next step) reads that upstream artifact via upstream_artifacts, pushes the vector into Qdrant, deletes the temporary object, and reports the final VECTOR artifact with external_ref: qdrant://collection/point-id — overwriting the intermediate one (Artifact is unique per (resource_id, type), so this is a normal upsert, not a special case).

Core needs no new code for this — the final artifact is a completely ordinary qdrant:// ref, viewed and cleaned up by the exact same built-in logic as any other. Writing your own storage plugin for a different backend means following the same two-step shape: a compute/produce step that hands off a temporary artifact, and a register step that owns actually writing to (and deleting from) your backend.

Making it configurable: a plugin's config ​

Hardcoding a connection URL means redeploying every time it changes. Instead, a plugin can read its own config — an arbitrary JSON object stored on its Plugin record, set via PUT /plugins/{id}/config or the Settings button on its card in the Web UI (see REST API reference):

ts
// qdrant-register resolves its own Qdrant URL like this:
const res = await fetch(`${CORE_API_URL}/api/v1/plugins/${PLUGIN_ID}`);
const plugin = await res.json();
const qdrantUrl = plugin.config?.qdrant_url || process.env.QDRANT_URL || 'http://localhost:6333';

config is entirely up to the plugin to define and interpret — Core just stores and returns whatever JSON object you PUT. A future "Pinecone Register" plugin might read config.api_key/config.index_name instead; Core doesn't need to know or care about the difference.

Packaging & deploying ​

Each plugin is its own Dockerfile + docker-compose.yml service, with its own environment variables (RABBITMQ_URL, CORE_API_URL, PLUGIN_ID, plus whatever storage credentials it needs). It does not need to live in this repository — a plugin can be built, hosted, and deployed entirely independently, as long as it can reach your RabbitMQ broker and your Core API.

Sharing it with others ​

Once your plugin works, you can list it on the Community Plugin Registry so other DataCore operators can discover it. This posts metadata and a link to your repo — not your code — so anyone interested still clones your repo, reviews it, and deploys it themselves.