Declare and install plugins
This recipe adds a plugin to a running Meridian instance: declare it in the
instance manifest and let boot resolve it, either from a registry (by version
range) or from a local build (path: override). It also covers PLUGINS_PATH,
the filesystem-only alternative that bypasses the manifest's plugins: list
entirely.
It assumes you already have an instance manifest (see First instance) and, if you're pulling from a registry, a reachable one (see Set up a plugin registry). For the full manifest schema, see Instance manifest.
Add a plugin from the registry
Make sure
registry:is set in the manifest, orMERIDIAN_REGISTRYis exported — the manifest key wins over the env var (apps/api/src/registry.ts,ensureInstancePlugins):apiVersion: meridian/instance-v1 registry: gs://meridian-plugins # or a local dir, or file:///srv/pluginsAdd an entry under
plugins:, keyed by the plugin's package name. The shorthand form is a bare semver range; the long form is an object with aversionkey (libs/shared/src/plugin/instance.ts,PluginRequirement):plugins: "@posos/demo": "^0.1.0" "@posos/geriatrie": { version: "0.1.0" }The ranges are resolved together with the npm
semverpackage against the versions published on the registry (resolveRequiredPlugins,libs/plugin-cli/src/registry/resolve.ts). The highest version of each range wins, unless itssdkrange does not admit the host's plugin contract, or itsdependsOnranges disagree with a version the instance loads: a local plugin (PLUGINS_PATHroots,path:entries, such as the core plugins of the image), including one the host refuses, whose version still counts, or the version chosen for another required plugin. The next version in the range is then examined, and the search goes back to change the version of another required plugin when that is what rules a version out. A plugin listed inplugins:that is also found locally is not resolved on the registry: the local copy loads, and a version outside the instance range only logs a warning. A business plugin published before the image that carries the core plugin version or the plugin contract it requires is therefore not selected: the previous version stays until that image is deployed, rollback included. Any rangesemveraccepts works (^,~,1.2.x,>=1.0.0 <2.0.0, an exact version,*). See Instance manifest for the full rule.Optionally confirm a matching version is actually published before you restart:
meridian-plugin versions "@posos/demo" --registry gs://meridian-pluginsRestart the process. Resolution happens once at boot, before plugins are loaded (
apps/api/src/server/api.tsandapps/api/src/restate/server.tsboth callloadPlugins(await ensureInstancePlugins(instance))):pnpm --filter @meridian/api run server # catalog/API process pnpm --filter @meridian/api run restate # workflow engine processEach process resolves
plugins:independently. A version missing from the local cache is downloaded from the registry intoMERIDIAN_PLUGIN_CACHE(default<repo>/.data/plugins, relative paths resolved from the repo root); a version already cached is reused without contacting the registry. The resulting folder becomes an extra discovery root passed toloadPlugins.
Pin an exact version instead of a range
Use an exact version string ("1.4.2") instead of a range when you need
reproducible boots regardless of what gets published later — an exact version
is a range with one match. There's no separate "pinning" syntax: it's the same
version field. When the pinned version's dependsOn rejects a version the
instance loads, there is no other candidate and the boot fails.
Override with a local path (skip the registry)
Use this for local development against an unpublished plugin, or a hotfix you don't want to push through the registry yet.
Build the plugin so it has a
dist/(meridian-plugin build, esbuild — see Build and bundle). The registry-based flow above downloads this same shape of folder;path:just points at one directly instead.Replace the version with a
path:for that plugin's entry:plugins: "@acme/vitals": { path: ../vitals/dist }A requirement declares
versionorpath, never both — the manifest schema rejects an entry that sets neither or sets both (libs/shared/src/plugin/instance.ts,PluginRequirement.refine).Relative paths are resolved from the repository root, not from the manifest file's own location — the same rule
INSTANCE_CONFIGitself uses (apps/api/src/registry.ts,fromRepoRoot).Restart the process. No registry is contacted for this entry at all — the path is used as-is, so nothing is downloaded and nothing is cached (
apps/api/src/registry.ts,ensureInstancePlugins: thereq.pathbranch returns before a registry store is even opened).
Load a plugin without declaring it (PLUGINS_PATH)
PLUGINS_PATH is independent of the manifest's plugins: list — it's a
discovery root the plugin host scans on every boot regardless of what the
instance declares (apps/api/src/plugins.ts, pluginRoots). Use it to mount
externally delivered plugins onto the host filesystem (on-prem, air-gapped,
container image layering) without touching the instance manifest at all. There
is no version resolution here: whatever is on disk at that path loads, as-is.
Point it at one or more compiled plugin directories — each one either a
dist/folder itself (containsplugin.yamldirectly) or a parent folder whose immediate subfolders each contain one. Separate multiple entries with a comma or the OS path-list delimiter (:on Linux/macOS,;on Windows):export PLUGINS_PATH=/opt/acme/plugins/vitals # or a parent of several plugin folders: export PLUGINS_PATH=/opt/acme/pluginsRestart the process:
PLUGINS_PATH=/opt/acme/plugins pnpm --filter @meridian/api run restatePLUGINS_PATHfolders are added alongside whatever the instance'splugins:resolved to; both kinds of roots are loaded the same way — there is no implicit in-repo root. See Plugin host loading for how discovery and load order work across these roots.
Verify
Watch the boot log: each resolved
plugins:entry logs its plugin name, resolved version, whether it was downloaded or served from cache (registry entries) or its local directory (path:entries), and a final line listing every plugin actually loaded (apps/api/src/registry.tsandapps/api/src/plugins.ts,loadPlugins). « plugin requis : versions plus hautes écartées » lists the higher versions of the range that were set aside, each with its reason; « (requis) » in a reason marks a version the resolver chose for another required plugin.A plugin that is listed in
plugins:but cannot be resolved (invalid range, no registry configured, range matches no published version, no combination of versions compatible with the host and the local plugins) fails the boot loudly. Treat that the same as an invalid manifest: it's a configuration error, not a runtime warning. The versions set aside are logged first, then the message names the plugin left without a version and what rules its versions out, three versions at most, for example[registry] @posos/demo@« ^0.2.0 » : aucune version publiée compatible avec l'hôte et les plugins locaux (0.2.3 : « @posos/clinical » 0.2.4 hors de « ^0.3.0 »). Publier une version compatible ou modifier la plage de l'instance.Publish a compatible version, deploy the image that carries the required version, or change the instance range. Apath:that doesn't exist does not fail the boot: the host only warns « racine de plugins introuvable — ignorée » and loads without it.Open Plugins in the console: each loaded plugin is listed with its version, the directory it was loaded from, its events, its contributions and its manifest — see Inspect loaded plugins.
Check the
pluginslog for load warnings. Each plugin is checked at load: a problem logs one of « contrat plugin incompatible — plugin ignoré », « dépendance signalée », « événement signalé », « exemple d'événement retiré », « déclencheur signalé », « nœud signalé », « binding hors contrat », « plage sdk insuffisante » or « module non chargé ». See Plugin load warnings.- « module non chargé » names a module of the plugin that does not import, most often
a file missing from the plugin's
dist/. The contribution that uses it is ignored and the rest of the plugin loads. Rebuild the plugin withmeridian-plugin build, or ask its author for a fixed version. - « dépendance signalée » names a
dependsOnentry of the plugin. When the version of the dependency found is outside the declared range, the range is invalid, or the plugin cites itself, the plugin is not loaded: load a version of the dependency in the range, or install a version of the plugin whose range accepts the one found. When the dependency is not loaded, or was itself refused with a version in the range, the plugin loads without the contributions that cite it: install the dependency with the plugin, or fix its load. - « plugin en double — copie ignorée » names a plugin found in two directories: only
the first root loads (
diris the copy ignored,keptthe copy loaded). Remove one of the copies. - In the
registrylog, « « @s/x » : copie locale 1.0.0 chargée, hors de la plage de l'instance « ^2.0.0 » » names a plugin listed inplugins:that is also found locally: the local copy loads instead of the registry one. Remove the local copy, or change the instance range. - A declared event and an
eventtrigger resolve their context kinds, types and events in the plugin'sdependsOnclosure. When a plugin listed independsOnis not loaded, the events andeventtriggers that cite it are dropped, and the Plugins page marks them ignored at load. Install the dependencies with the plugin. - A plugin that declares
sdk: "^0.1.1"(declared events) needs a host on plugin contract0.1.1or later; an older host skips it withcontrat plugin incompatible — plugin ignoré.
- « module non chargé » names a module of the plugin that does not import, most often
a file missing from the plugin's
Related
- Inspect loaded plugins — what each loaded plugin declares, in the console.
- Instance manifest — full
registry:/plugins:/ports:schema. - Environment variables —
MERIDIAN_REGISTRY,MERIDIAN_PLUGIN_CACHE,PLUGINS_PATH,INSTANCE_CONFIG. - Set up a plugin registry — stand up the registry this doc pulls from.
- Publish to the registry
and Build and bundle — the
author side: producing and publishing the
dist/these mechanisms consume. - Bundling and distribution — why published versions are immutable and what's inside a built plugin.
- Plugin host loading — how discovery roots, load order, and SDK compatibility checks work.
- First instance — creating the manifest this recipe edits.