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:
Where Modules Live
Custom modules live under $CYRNEL_DATA_DIR/modules/<id>/. Each directory
must contain a module.json:
Fields:
id: Unique identifier for the module.
version: Semver version of the module (required, e.g. "1.2.3").
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).
engines (optional): Object declaring the Cyrnel core version range this
module supports. When present, the cyrnel field is required: it must be
a semver range (e.g. ">=3.0.0") that the Cyrnel core version must satisfy,
or the module is rejected at registration time. Omitting engines entirely
is allowed and means “no core version constraint”.
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
-
Place the built directory at
$CYRNEL_DATA_DIR/modules/my-adapter/.
-
Ensure it contains
module.json and the file named in main.
-
Either restart the API or call:
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
You should see your module alongside the built-ins, with
isBuiltin: false.
To explicitly enable / disable:
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:
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:
- Build the module (
tsc, esbuild, whatever).
cp -r dist $CYRNEL_DATA_DIR/modules/<id>/.
POST /modules/reload.
- 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 August 1, 2026