Skip to main content

Consuming Core Services & the @core Alias

Your plugin frequently needs to touch core state — create a workspace, transition a status, ask which workflows are available. It does that through core services, and two rules make it work: use the right import alias, and know that a "core service" wears two different faces depending on whether you're calling it from the backend or the frontend.

The @core/* import alias (mandatory)

Plugin backend code must import core modules through the @core/* alias — never a relative path like ../../../api/src/.

import { query, getClient } from '@core/db/index.js';
import { logger } from '@core/services/logger.js';
import { coreWorkspaceService } from '@core/services/coreWorkspaceService.js';
import type { PluginManifest } from '@core/core/pluginTypes.js';

Why it's mandatory: relative paths break in Docker, because the compiled layout (/app/dist/) differs from the source layout (api/src/). The alias resolves to the source in development and to the compiled output in Docker — and because both resolve through the same compiled modules, your plugin shares singletons (the DB pool, the logger) with the core rather than spinning up duplicates. (The .js extension on imports is required by the project's module resolution; your local imports within the plugin also use .js.)

Backend imports vs. frontend HTTP

A core service like coreWorkspaceService.create() has two faces:

  • From plugin backend code, it's an importable function you call directly — for example inside a route handler or your result handler.
  • From plugin frontend code, the equivalent is a core API endpoint you call over HTTP — the plugin UI never imports backend services; it makes HTTP calls.

So "consume the core service" means import and call on the backend, and fetch the endpoint on the frontend. Don't try to reach backend code from the UI.

The services you'll use most

  • coreWorkspaceService.create({ workspaceStatusId }, client) — create a core workspace (e.g. when adding an entity, or in a result handler that branches). Pass the transaction client when inside one.
  • coreWorkspaceService.updateStatus(workspaceId, statusId) — perform a plugin-owned status transition (e.g. the default plugin's "submit input" → input_provided).
  • workflowService.getAvailableWorkflows(workspaceId) — the workflows eligible for a workspace, for your manage page's workflow picker.

The rule underneath

Never INSERT directly into circus.workspace or any other core table — go through these services. They're the sanctioned doorway between your plugin and core state, and using them is what lets the core keep its invariants (and its transaction) intact. Writing to core tables directly is the boundary violation the what-plugins-cannot-do rules forbid.