Skip to main content
This page covers the mechanics shared by every custom module, registration, and lifecycle. For type-specific skeletons and examples see: For the full interface reference, see AdapterModule and EnvironmentModule.

Installation

To use the cyrnel SDK in your TypeScript codebase, install the @cyrnel/sdk package:
npm install @cyrnel/sdk

Where Modules Live

Custom modules live under $CYRNEL_DATA_DIR/modules/<id>/. Each directory must contain a module.json:
{
  "$schema": "https://raw.githubusercontent.com/actelos/cyrnel/main/schema/module.schema.json",
  "id": "my-adapter",
  "name": "My Adapter",
  "type": "adapter",
  "description": "Talks to my service",
  "main": "./index.js"
}
Fields:
  • id: Unique identifier for the module.
  • name: Human readable name for the module.
  • type: Adapter or environment.
  • description: Surfaced in GET /modules.
  • main: Path to the built module file the host can import(). The file must default-export the module object (see below).
The full JSON Schema is available at schema/module.schema.json. You can reference it from your module.json via $schema for editor autocompletion and validation. The host expects compiled JavaScript here, not TypeScript. The expectation is that you ship modules built ahead of time.

Registering a Module

  1. Place the built directory at $CYRNEL_DATA_DIR/modules/my-adapter/.
  2. Ensure it contains module.json and the file named in main.
  3. Either restart the API or call:
    curl -X POST http://localhost:9371/modules/reload
    
reload re-scans $CYRNEL_DATA_DIR/modules/, registers any new directories, and reconciles the result against the modules table. A new module is inserted with enabled: true, missing: false by default.

Verifying

curl http://localhost:9371/modules
You should see your module alongside the built-ins, with isBuiltin: false. To explicitly enable / disable:
curl -X POST http://localhost:9371/modules/my-adapter/enabled \
  -H 'content-type: application/json' \
  -d '{"enabled":true}'
For adapters, enabling triggers setup({ config, secrets }) and hydration of any enabled services that target it. For environments, enabling makes it the active environment and drains the previous one.

Removing a Module

Stop the API, delete $CYRNEL_DATA_DIR/modules/<id>/, and restart. Or delete the directory and POST /modules/reload. The modules row is not deleted, it’s marked missing: true. The enabled flag is preserved so the module resumes at its previous state if it reappears. Trying to enable a missing module returns 409.

Module ID Collisions

If a custom module declares a name that already exists (built-in or otherwise registered), the host keeps the first registration and skips the duplicate. Built-ins are registered first.

Module Entry File

The file pointed to by main must default-export an object with the following shape:
import type { ModuleExport } from "@cyrnel/sdk";

// For adapter modules:
import type { AdapterModule } from "@cyrnel/sdk";

export default {
  configSchema: { /* JSON Schema for module-level config */ },
  secretsSchema: { /* JSON Schema for module-level secrets */ },
  instantiate(): AdapterModule {
    return new MyAdapter();
  },
} satisfies ModuleExport;
  • configSchema and secretsSchema must be plain JSON-only objects. No functions, class instances, Proxies, symbols, or circular references. The host validates this at registration time.
  • instantiate is a zero-argument factory that returns a fresh module instance. The host calls it each time the module is activated.
For the type-specific skeletons see:

Iteration Workflow

While developing:
  1. Build the module (tsc, esbuild, whatever).
  2. cp -r dist $CYRNEL_DATA_DIR/modules/<id>/.
  3. POST /modules/reload.
  4. Disable + re-enable the module to force teardown / setup.
The reload endpoint refreshes the registry but does not re-activate already-enabled modules. Toggle enabled to pick up code changes.

Packaging Notes

  • The host loads main with the native import(), it must be an ESM file the Node runtime can resolve directly. Bundle into a single file if your module has dependencies; the loader does not run npm install for you.
  • peerDependencies are not honoured. If your module needs @cyrnel/sdk types, ship them as build-time devDependencies and bundle anything you import.
  • Keep modules narrow. A single module that does many unrelated things is harder to review than several small ones.
Last modified on July 1, 2026