Upgrade the platform
Meridian has two independently versioned things to upgrade: the platform
(the four core libraries baked into the meridian-api and
meridian-config-plane images) and plugins (published separately and
resolved by the running host at boot). This recipe covers both, and — this is
the part that bites if done out of order — the sequencing between them.
Before starting, make sure you already have a registry set up (Set up a plugin registry) and know how your instance declares its plugins (Declare and install plugins).
Before you start
Images never bundle business plugins.
Dockerfileinstalls only the workspace and the core plugins (external-plugins/, copied into the image); every plugin underplugins:in the instance manifest is resolved against the registry (or apathoverride) at boot, inensureInstancePlugins(apps/api/src/registry.ts). A plugin that is listed but cannot be resolved — no registry configured, version not found, download failed — fails the boot loudly, the same as an invalid manifest. Practically: publish a plugin version before you deploy or restart anything that will require it. Doing it the other way round turns a routine restart into an outage.Version resolution is real semver, via the npm
semverpackage, and it treats the two kinds of plugins differently. Keep them apart when you plan a rollout:- A registry plugin (
plugins:with a version range) is resolved at boot (resolveRequiredPlugins,libs/plugin-cli/src/registry/resolve.ts): the host picks the highest combination of published versions of the instance ranges whosesdk:range admits its plugin contract and whosedependsOnranges agree with the plugins it loads and with each other. A version that does not fit is set aside for the next one, with the warning « plugin requis : versions plus hautes écartées ». When no combination fits, or the range has no published version, it is a hard failure: the boot aborts. - A plugin found on
PLUGINS_PATHor through apath:entry is not resolved, even whenplugins:lists it: the local copy loads, and a version outside the instance range only logs a warning. The host checks itssdk:range against its plugin contract and itsdependsOnranges against the versions of its dependencies when it loads it (apps/api/src/plugins.ts). A mismatch is a soft failure: the plugin is skipped with the log linecontrat plugin incompatible — plugin ignoréor « dépendance signalée », and the rest of the host still boots.
A plugin that uses declared events under a range below
^0.1.1is a third case: it loads, with the warning « plage sdk insuffisante ».- A registry plugin (
A run's workflow spec is journaled when the run starts, and so is the result of each step it has executed. Workflow YAML changes never affect a run already in flight. Plugin code can, depending on the journal format the run started with. When a run resumes after a restart, the nodes it has not executed yet run the new code. A run of format 2 also reads from its journal its trigger's outputs and the texts and outputs of the human tasks it has reached: a new plugin version that changes the declared event, the trigger's outputs or a task's title applies to new runs only. A run of format 1 computes them again with the loaded plugins: when the new version gives the run's event other outputs (a field added to or removed from the declared event, an output or its
fromchanged, a codedfromEventchanged) or a task another title, the replay of a run past that point, such as a suspended or waiting run, no longer matches the journal, and Restate retries the run until the previous version is loaded again. See Why durable execution and Recover runs blocked by a missing plugin.
1. Publish new plugin versions first
Do this regardless of whether the platform itself is also changing — it's the step that must land before anything else touches the affected environment.
From the plugin's own repo, build the deliverable
dist/(esbuild bundles every module the manifest references, inlining its runtime dependencies; onlynode:*and@meridian/*stay external):meridian-plugin build .Publish it to the target registry (local directory or
gs://bucket):meridian-plugin publish . --registry gs://meridian-plugins # or: export MERIDIAN_REGISTRY=gs://meridian-plugins && meridian-plugin publish .A published version is immutable —
publishPluginrefuses to overwrite an existing@publisher/name/<version>(libs/plugin-cli/src/registry/registry.ts). If you need to ship a fix, bump the version inplugin.yamland publish again; there is no in-place replace.Confirm it landed before moving on:
meridian-plugin versions @posos/demo --registry gs://meridian-plugins
See Build and bundle and Publish to the registry for the authoring side of this in full, and Bundling and distribution for why the artifact is self-contained.
2. Point instances at the new plugin version
This is safe to do any time after step 1 — the registry now has what any instance will ask for.
- If the instance manifest's range already covers the new version (e.g.
"@posos/demo": "^0.1.0"and you published0.1.2), no manifest change is needed: the next endpoint/proxy restart resolves the new highest match automatically, once itsdependsOnaccepts the plugins the instance loads (see Business plugins that require a newer core plugin). - If you need the new version specifically, tighten or bump the range in the
manifest and restart endpoint + proxy so
ensureInstancePluginsre-resolves it. See Declare and install plugins and the instance manifest reference for theplugins:block. - A stale local cache never blocks an upgrade to a newer version — only the
requested version is looked up, downloaded once into
MERIDIAN_PLUGIN_CACHE, and reused afterwards.
3. Upgrade the platform libraries (lockstep)
Do this when @meridian/shared, engine-core, llm-client, or plugin-cli
change — they version together as a fixed group
(.changeset/config.json, fixed), so plugin authors only ever have to check
one number.
Any PR touching one of the four libraries includes a changeset:
pnpm changesetOn merge to
main, the release workflow (changesets/action,.github/workflows/release.yml) opens or updates a "Version PR". Merging that PR runs:pnpm run version-packages # changeset version && lockfile refreshwhich bumps all four
package.jsonversions identically. Note thatPLUGIN_CONTRACT_VERSION(libs/shared/src/version.ts) — the constant the host compares every plugin'ssdk:range against — is decoupled from the package version and changes with the plugin contract: its patch for a new manifest key an older host would strip (0.1.1, declared events;0.1.2,resolve.pendingon an event binding;0.1.3, the default filter of a declared event;0.1.4, the description of a protocol's reception;0.1.5, conditional port rules;0.1.6, connections), its minor on a real break of the plugin-facing surface. A plugin that declaressdk: "^0.1.6"is refused by a host older than that contract, so deploy the host before such a plugin (@posos/fhir0.1.7 and@posos/posos0.6.4 declare^0.1.6,@posos/hl70.2.1 and@posos/pn130.1.1^0.1.4,@posos/common0.4.1^0.1.3). Publishing these packages to an external npm registry is deliberately disabled for now (release.yml): they're consumed in-workspace, so "upgrading the platform" means building new images from the mergedmain, not installing a new SDK release.Build the images from that commit.
endpointandproxyare the samemeridian-apiimage, started with different commands (pnpm --filter @meridian/api run restatevsrun server— seeDockerfile);uiis the separatemeridian-config-planeimage (Dockerfile.ui). For a compose deployment:pnpm run build:plugins # external plugins mounted via PLUGINS_PATH, not baked in docker compose --profile stack up -d --build
For the full changeset mechanics (drafting a changeset, reviewing the Version PR), see Release and version.
4. Roll out in order
Once the images (if any) are built and the plugin versions you need are published, restart in this order — it's the order that keeps in-flight runs safe and keeps every component talking to a component it can still reach:
- Endpoint — restart it first. It re-registers with Restate on boot; any
run already in progress resumes where the journal left it, which is the
durability guarantee Restate gives you (no drain step needed), provided the
new release replays the run's journal format: the same
commands, and state writes of the same shapes. A release changes neither
within a format; a new format is started only once
RUN_JOURNAL_FORMATis set to it. See Why durable execution. - Proxy — restart next.
- UI — restart last; it is stateless and only serves the console.
- Restate itself upgrades on its own schedule and procedure, independent
of the four services above — keep the
restate-datavolume across the upgrade so the journal survives.
Test the exact platform + plugin version combination in pre-production first; the same advice applies to plugin swaps and to a platform bump alike.
Verify Restate service discovery
The endpoint registers through RESTATE_ENDPOINT_URL, often a Kubernetes Service.
During a rolling update that address can still select an older pod before the new
one becomes Ready. A successful HTTP response from the admin API is only accepted
when it lists every service bound by the new endpoint. An incomplete response logs
enregistrement incomplet with the missing names and retries every three seconds
within the registration budget. endpoint Restate démarré logs the expected names;
déploiement enregistré logs the discovered names.
If authoring returns 404 service 'meridianWorkflowAssist' not found, compare
GET /services on Restate's admin API with that list. Both
meridianWorkflowAssist and meridianWorkflowTests must be registered. Once the
new endpoint is Ready, registering its existing address again through
POST /deployments with { "uri": "<RESTATE_ENDPOINT_URL>", "force": true }
refreshes discovery. Retry the generation after registration succeeds.
Workflows that use a new core node
A platform version can add a core node type. An endpoint older than that version does not know the type, and the proxy validates with its own catalogue, so it does not report the mismatch. The core node types added so far:
| Node type | On an endpoint that does not know it |
|---|---|
flow.merge (merge branches) |
When a wired input of an unknown node is absent, the endpoint skips the node (node-skipped-unknown-type) instead of failing the run. A merge of exclusive branches always has an absent input, so an older endpoint skips it together with everything downstream, without an error. |
trigger.event-received (« Événement reçu », the trigger configured by a declared event) |
A new run fails at its trigger (Trigger invalide : trigger.event-received). A run in flight throws the same way at its next replay, before its first journaled entry: Restate reports a journal mismatch and retries the invocation until the endpoint knows the trigger again, or until the invocation is killed. The run keeps its last state meanwhile; the endpoint logs rejeu interrompu : … once at warn and does not count it as failed (Run outcome). |
- Publish no workflow that contains one of these node types until every endpoint replica runs an image that knows it.
- Before rolling the endpoint back to an image older than such a node type:
- Unpublish the workflows that contain it (Unpublish in the workflow's
version panel, or
POST /api/workflows/<slug>/unpublish). This stops new runs only. - Wait until no run of these workflows is
running,suspendedorwaitingin the Executions view, or cancel those runs in Restate while the endpoint still runs the current image. An image that does not know the node cannot replay the journal of a run that already executed it: the run stops on a Restate journal mismatch, or, forflow.merge, fails withDeploymentErrorwhen every wired input of the merge is present. A run that has not reached a merge yet would skip it and everything downstream. - Roll the endpoint back.
- Unpublish the workflows that contain it (Unpublish in the workflow's
version panel, or
A run blocked on a journal mismatch after a rollback or a plugin removal is detected, restored or ended as described in Recover runs blocked by a missing plugin.
Journal formats
The journal format of a run fixes the commands its replay must reproduce (the durable
steps, their names and order) and the values it writes: the shapes of the set state
values of the snapshot, the trace and the observations. Restate compares both at each
replay. The proxy starts each run in the format that RUN_JOURNAL_FORMAT chooses (1 or
2, 1 by default; see
Environment variables), logs it at boot
(format de journal des runs démarrés : <n>), passes it in the invocation argument
(journalFormat) and records it in the run index (journal_format, shown by the run
detail and the runs list). The endpoint runs and replays each run in that format,
whatever its own release. The formats so far:
| Format | Runs of this format | A replay reads from the journal |
|---|---|---|
| 1 | Started with RUN_JOURNAL_FORMAT=1 (the default), or by a proxy older than format 2 (no journalFormat in the invocation argument, journal_format NULL). |
The spec, the event and the result of each step. The trigger's outputs and the title, payload, deadline and outputs of each human task are computed again by the loaded plugins, and no step closes a validation request. |
| 2 | Started with RUN_JOURNAL_FORMAT=2. |
Also the trigger's outputs (step trigger), the request of each human task (step <node>/request) and its outputs (step <node>/outputs). Each validation request is published and closed in a step (publish-validation-request, close-validation-request), whatever the record ports bound. |
A run of format 2 still takes from the loaded plugins, and can still diverge on: a node
type's ports, configuration and output types, the removal of a node type, and the checks
of the outputs of the other nodes, which run again at replay against the loaded ports. A
task pending when a new plugin version loads gets that version's onResolve or
onTimeout.
- Rollout. Deploy the release on every endpoint replica, then set
RUN_JOURNAL_FORMAT=2on the proxy. Theapiimage carries both processes and a deployment usually bumps them together, so the variable, not the image order, keeps runs of format 2 away from an endpoint that does not know it. An endpoint older than format 2 ignoresjournalFormat: it runs a run of format 2 with the commands of format 1, and once the endpoint is upgraded that run no longer matches its journal and stays blocked. An endpoint that knows format 2 but not a later format fails a new run of that format at once: « Format de journal 3 inconnu de ce moteur (formats connus : 1, 2). Déployer l'endpoint dans une version qui le connaît. » - Rollback. An endpoint that does not know a format cannot replay its runs: they stop
on a journal mismatch (
rejeu interrompu : …, see Run outcome) until an endpoint that knows it runs again. To roll the endpoint back below format 2:Set
RUN_JOURNAL_FORMAT=1on the proxy and restart it: new runs start in format 1.Wait until no run of format 2 is in flight. On the run index (
RUN_DB_URL), this query returns no row:SELECT id, workflow_file, created_at FROM runs WHERE final_status IS NULL AND journal_format = 2;A run that has ended is frozen by the next settlement pass (see Run index).
Roll the endpoint back.
- Runs in flight at the upgrade keep their format. A run of format 1 stays exposed to a plugin version that changes its event or the title of a task it waits on, and a console decision does not close its validation request in the record (see Recover runs blocked by a missing plugin).
Business plugins that require a newer core plugin
The core plugins (@posos/common, @posos/clinical, @posos/fhir…) come from the
image; the business plugins come from the registry, published when their change merges.
A business plugin version whose dependsOn requires a core plugin version that the
deployed image does not carry, or whose sdk range requires a plugin contract the image
does not have, is not selected at boot: the highest version of the instance range that
the image accepts is, and the boot logs « plugin requis : versions plus hautes écartées »
with the version set aside and its reason. The required plugins are resolved together,
with backtracking: when the newest version of a business plugin requires a version of
another required plugin that its instance range excludes, or that the image does not
accept, the previous version of the first one is selected, with a version of the other
that fits. Once every
endpoint and proxy replica runs the image that carries the required version, the next
restart selects the new version. A rollback of the image works the same way: the
business plugin versions that the previous image does not satisfy are set aside at the
next boot.
Every process resolves on its own at boot, with the same registry and the same image: endpoint and proxy select the same versions. During a rollout, a replica of the old image and a replica of the new one can select different versions until the rollout ends.
When no combination of versions of the instance ranges fits, the boot fails. 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; a reason that cites another required plugin marks it « (requis) », a version the resolver examined rather than a loaded plugin. Publish a compatible version, deploy the image first, or change an instance range.
A business plugin listed in plugins: that is also found on PLUGINS_PATH is not
resolved on the registry: the copy of the image loads, and the boot warns when its
version is outside the instance range.
Plugin dependency versions
The host checks each dependsOn range of a plugin against the version of the dependency
found (« dépendance signalée », kind dependency), whether that copy loads or is refused
itself. A plugin whose range does not accept that version, whose range is invalid, or that
cites itself, is not loaded; a plugin whose dependency is not found, or was refused with a
version in the range, loads without the contributions that cite it.
The repository's plugins declare ^<major>.<minor>.0 of the series they are tested with
and load with no warning; a plugin that declares * is not constrained, as before.
For plugin authors, meridian-plugin validate and build fail on the same problems,
and on a dependency they cannot find (neither in node_modules nor in a sibling
directory). For a dependency above the declared range, they print:
- @acme/vitals › @posos/clinical (dependency) : « @posos/clinical » en 0.3.0, hors de la plage « ^0.2.0 », plugin non chargé. Charger une version de « @posos/clinical » dans « ^0.2.0 », ou déclarer "^0.3.0" si le plugin fonctionne avec 0.3.0.
meridian-plugin deps lists each dependency with its range, the version found and its
state. publish refuses an invalid range, and a dependency published on the target
registry whose versions are all outside the range:
- @acme/vitals › @posos/demo (dependency) : toutes les versions de « @posos/demo » publiées sur gs://meridian-plugins sont hors de la plage « ^0.3.0 ». Publier une version de « @posos/demo » dans « ^0.3.0 » d'abord ou corriger la plage.
Review the plugin against the new version of the dependency before moving its range to
the new series; a new range is a new plugin.yaml, so it needs a new plugin version (see
Release and version).
Platform metrics under meridian.*
The release that names the platform's metrics under meridian. exports no clinical.*
metric: clinical.events.ingested becomes meridian.events.ingested, and so on for the
fourteen instruments, with the same attributes, units and kinds
(Observability signals). The OTel log
scope of the OTLP export becomes meridian instead of clinical. Change the dashboards,
alerts and queries that read the old names in the same deployment as the proxy and the
endpoint: there is no period during which both names are exported.
Plugin manifests under contract 0.1.3
A host on contract 0.1.3 refuses two manifest forms that older hosts loaded, and
reports each at load:
- a trigger without
pinnedRoutingand withoutevent(aneventTypealone): the trigger is dropped (« déclencheur signalé »). Declare the event incontributes.eventsand reference it withevent; defaultSelectoron a node: the node loads, the key is ignored (« déclencheur signalé » on a trigger, « nœud signalé » on another node). On a trigger withevent, move the filter to that event (events[].defaultSelector) and declaresdk: "^0.1.3"; elsewhere, remove it.
A declared event whose type is the address of a pinned trigger is refused as well
(« événement signalé » for the plugin that declares it, « déclencheur signalé » for the
plugin that pins the address): rename the event or change the address. A declared event
with a field named done is refused too.
The host also drops, and reports, two forms that older hosts loaded:
- an agent without
model(or with an emptyprovider, no input or no output): the agent is dropped alone, the rest of the plugin loads (« nœud signalé »,agent « X » : modèle absent, agent ignoré. Renseigner « model ».). Fill in the model; - a declared event field or a node port on a dimension from outside the plugin's
dependsOnclosure: the event or the node is dropped (« événement signalé » or « nœud signalé »,dimension « mass » inconnue dans la fermeture dependsOn. …). The core declares no dimension: the ten common physical dimensions (mass,volume,time, the concentrations…) come from@posos/common0.4.1, so a plugin that types a port on one of them declares@posos/commonindependsOn.
meridian-plugin validate and build report these problems as the host does on the
same manifests (of the host's own node checks, only the dimensions of the ports are run
by the CLI; an unknown type on a port is not), so fix a plugin before publishing it for
such a host; build reports an address conflict to the plugin it builds when the other
plugin is in its dependsOn closure.
Unknown manifest keys
The manifest schema drops a key it does not know. The host reports each such key at load,
named by its path (« clé ignorée », kind key), and loads the plugin without
it, as earlier hosts did silently: a plugin already published loads the same. The plugin
contract does not change (0.1.3).
For plugin authors, meridian-plugin validate and build fail on an unknown key, and
publish refuses a dist/plugin.yaml that carries one, or that the schema of the CLI's
plugin contract rejects (a dist/ whose sdk range does not accept that contract is
published without these checks). A manifest that an earlier CLI built can carry such
keys: a misspelled key, the rest of a value cut by an unquoted comma in a { … }
mapping, or an agent's input shorthand next to its inputs list. Fix the manifest
(quote the value, correct or remove the key) before building a new version:
- @acme/vitals › contributes.nodes[0].outputs[1]["émis si l'événement le porte."] (key) : clé inconnue, ignorée : la valeur de « description » est coupée à une virgule. Mettre cette valeur entre guillemets.
Protocol reception under contract 0.1.4
The host is on contract 0.1.4: a plugin describes the reception of its protocol in the
manifest (media types and an example message of an HTTP connection, the default port and
the hint of each of its own transports), and the core names no protocol.
- Plugins.
@posos/hl70.2.1 and@posos/pn130.1.1 declaresdk: "^0.1.4". A host on contract0.1.3refuses them, pn13 with « contrat plugin incompatible — plugin ignoré », hl7 with « manifeste invalide » (its MLLP transport is written as an object, which the earlier schema does not read). They ship in the image with the host, so a normal rollout loads them together. Plugins in^0.1.xstay accepted. A plugin that listshttpordirectoryamong its own transports loads with « déclencheur signalé »: remove the name, the host offers both to every protocol. - Console and API.
GET /api/connections/protocolsserves each transport of a protocol as an object ({ name, defaultPort?, hint? }), with the hint in the language of?locale=. The console of this release reads both forms; an earlier console does not read objects, so deploy the console with or before the proxy. The routing audit and the hospital simulator read only the bindings of the route. The connection form proposes the transport's declared port (2575 for MLLP) and shows its hint. - Validation refusals. An HTTP refusal (
422) carrieserror« Validation refusée (profil « PN13-IS 4.0.34 »). », the profile named by its label; the inbox history shows « Validation refusée. » next to the report. A client that matches the text should read the status code andvalidation.status. - OpenAPI. The request body of an HTTP connection lists the media types and the
example of its protocol:
application/xmlandtext/xmlfor PN13 and CDA R2,application/hl7-v2andtext/plainwith an example message for HL7 v2.
Connections under contract 0.1.6
The host is on contract 0.1.6: a plugin contributes connection types and requires a
connection on an adapter, an event source or a node, and the host gives it an
authenticated handle on the connection the instance designates, or on the default
connection of the type. See Manage connections.
- Plugins.
@posos/fhir0.1.7 (connection typefhir-r4) and@posos/posos0.6.4 (connection typeposos-api) declaresdk: "^0.1.6". A host on contract0.1.5refuses them (« contrat plugin incompatible — plugin ignoré »), so upgrade the host first. Plugins in^0.1.xstay accepted. - Instance manifest. Move the FHIR URL (
ports.*.config.baseUrl; forsources.fhir-poll.baseUrl, see Event sources as inbound connection types) and the variablesFHIR_BASE_URL,POSOS_API_URL,POSOS_AUDIENCEandPOSOS_REGIONtoconnections:, and name the connection used by default for each type inconnectionDefaults:. The old forms are still read for one version, with a warning ([fhir] config.baseUrl est déprécié …,[posos] POSOS_API_URL, POSOS_AUDIENCE et POSOS_REGION sont dépréciés …). - Posos defaults. The Posos plugin no longer falls back to the preproduction URL and audience: the instance supplies them through a connection or the variables above.
- Credentials. Set
MERIDIAN_SECRET_KEYSon both the proxy and the endpoint when a connection created in the console uses a credential. See Manage credentials.
Validation channels under contract 0.1.8
The host is on contract 0.1.8: a human validation can be published to a messaging
channel and decided there, a validation can offer a choice between options, and incoming
decisions arrive as messages of the validation-decision format. See
Plugin manifest — Validation channels and incoming decisions.
- Plugins. A plugin whose inbound connection type declares
connectiondeclaressdk: "^0.1.8"; a host on contract0.1.7refuses it. Plugins in^0.1.xstay accepted. - Journal format. Channels are published and closed only for runs of journal format 2
(
RUN_JOURNAL_FORMAT=2on the proxy). A format 1 run publishes to the patient record only, as before. - Runs in flight. A run suspended before the upgrade replays its journal unchanged; its pending validation has no channel publication.
- API.
POST /api/runs/:id/resolveacceptschoicefor a choice validation; the run view'spendingcarriesmessageandoptions. - Bundled plugins.
@posos/common0.4.3 adds thehuman.choosenode and an optionalvalidationconnection onhuman.approveandhuman.choose;@posos/slack0.1.1 adds the Slack validation channel and its listening connection (see Decide validations in Slack).
Node connections under contract 0.1.7
The host is on contract 0.1.7: each instance of a node designates its connection
(nodes.<id>.connection), a node can require a connection by port (connection: { port }),
and a connection type can be reached over another protocol than HTTP (urlSchemes) and
give its credential to its plugin (exposeCredentials). See
Plugin manifest — Connection types.
- Plugins. A plugin that uses these keys declares
sdk: "^0.1.7"; a host on contract0.1.6refuses it, so upgrade the host first. Plugins in^0.1.xstay accepted. - Workflows. A workflow with
nodes.<id>.connectionneeds an endpoint on0.1.7: an older endpoint drops the key and uses the default connection. Roll out the endpoint before publishing such a workflow. - Connection messages. The issue on a
baseUrlof the wrong scheme readsbaseUrl : schéma https ou http attendu..
Notification channels
@posos/notify 0.2.0 makes the channel a connection designated by the notify.send node,
and the image ships two channel plugins, @posos/slack and @posos/mail. All three declare
sdk: "^0.1.7". See Send notifications.
- Node configuration.
recipients(destinations separated by commas) replaceschannel; achannelstill set is read as one recipient. The two demo drafts ofcontent/userecipients. - Webhook. Declare a connection of type
notification-webhookand bind it (ports.notification = { adapter: webhook, connection: <id> }) or designate it on the nodes.config.url,config.headers,config.timeoutMsandNOTIFY_WEBHOOK_URLare still read with a warning ([notify/webhook] config.url est déprécié …). The JSON body carriesrecipientsinstead ofchannel; with a connection,patientIdis sent only when the connection setspatientContext: true. - Deployment. A deployment that sets its own
PLUGINS_PATHadds/app/external-plugins/slackand/app/external-plugins/mailto load the new channels.
Decision models by connection
@posos/decision 0.2.0 and @posos/typesafe 0.2.0 declare sdk: "^0.1.7". A decision
model server is a connection of type typesafe-systemone; a decision.ask node
designates it, else it uses ports.decision. Without compatibility:
- Instance manifest.
ports.decision.config.providersanddefaultProviderare no longer read, norTYPESAFE_API_KEY,TYPESAFE_BASE_URLandTYPESAFE_DEFAULT_MODEL. Before the rollout, declare one connection per server (TypeSafe:baseUrl: https://api.typesafe.ai,auth: { scheme: bearer, token: env:TYPESAFE_API_KEY }; a former provider: itsbaseUrl,defaultModel,timeoutMs,headers, and its key as the connection's auth) and name the default inconnectionDefaults.typesafe-systemone. See Configure ports and adapters. - Workflows. The node's
providerfield is removed: a node that still sets it fails withChamp « provider » retiré : désigner la connexion du nœud.Designate the connection of the node instead (nodes.<id>.connection). No workflow ofcontent/set it.
Inbound reception renamed
The reception subsystem no longer carries the name of the external EAI system. At the
proxy's first boot the tables eai_connections, eai_inbox and eai_listener_status
are renamed inbound_connections, inbound_messages and inbound_listener_status
(data kept; the leader lock key is unchanged, so an older and a newer proxy still
exclude each other during a rolling deploy). Without compatibility:
- event ids derived from a received message start with
inbound:instead ofeai:; a fact received before the upgrade and again after it starts a new run; - the metrics
meridian.events.generatedandmeridian.events.contract_missingcarrysource: inbound; - the OpenAPI tag of HTTP connections is
Réception.
INBOUND_DIRECTORY_ROOT replaces EAI_DIRECTORY_ROOT, which is still read when the new
variable is unset.
The foreign key from inbound_messages to inbound_connections is dropped: an inbound
connection declared in the instance manifest has no row, and its messages reference it by
its identifier. Deleting a connection that has messages stays refused by the API.
EAI transports as inbound connection types
A transport is now an inbound connection type with declared parameters: http and
directory from the host, mllp from @posos/hl7 0.2.2 (sdk: "^0.1.6"). At the proxy's
first boot, the inbound_connections table moves the fields host, port, encoding and
directory of each connection into params (idempotent). The API keeps accepting those
fields at the top level of a POST or PATCH /api/connections body (the hospital
simulator still sends them), and the console builds the connection form from the
parameters the type declares. A plugin still declaring receive.transports keeps
working: the host turns each transport into an inbound type with the parameters host,
port and encoding.
Event sources as inbound connection types
An event source (fhir-poll of @posos/fhir, adsb-poll of @posos/aero-demo) is an
inbound connection type: a connection whose type is the source name polls with its
parameters and enables its bindings, and each record goes through the reception inbox
(inbound_messages) before its binding projects it. See
Connect an event source.
Instance manifest.
sourcesandeventBindingsare deprecated and still read: at boot, eachsources.<name>with at least one binding left byeventBindingsbecomes the read-only inbound connection<name>of type<name>, with the entry as its parameters and, as its bindings, theeventBindingslist restricted to the source (absent: every binding of the source). The proxy logsclés sources et eventBindings du manifeste d'instance dépréciées : déclarer une connexion du type de la source sous connections, avec ses bindings.Replace them with a connection per source, and put on each one the bindings of its own source:# Before sources: fhir-poll: { connection: entrepot, intervalSeconds: 15 } eventBindings: [fhir-creatinine, fhir-validation-decided]# After connections: fhir-poll: type: fhir-poll connection: entrepot intervalSeconds: 15 bindings: [fhir-creatinine, fhir-validation-decided]Keep the source name as the connection identifier. The event id of a record is
<connection id>:<feed>:<recordId>: the identifierfhir-pollkeeps the event idsfhir-poll:<feed>:<recordId>of the earlier releases, so the records re-read in the overlap window after the restart are duplicates. Under another identifier, those records produce new event ids and start new runs.Parameters are checked.
@posos/fhir0.1.8 and@posos/aero-demo0.2.4 declare the parameters of their source (params). A parameter the source does not declare, a value of another type (a numeric string such as"15"included) or out of bounds makes the connection unusable: the proxy logsconnexion entrante « <id> » du manifeste inutilisable : …and the source does not start.sources.<name>.baseUrlis no longer read: the connection gets the issuesources.<name>.baseUrl n'est plus lu : désigner une connexion par le paramètre connection.and does not start; declare afhir-r4connection and name it withconnection. Foradsb-poll, the issue ends withclé réservée aux connexions sortantes.: set the URL with theapiUrlparameter. The plugin contract stays0.1.6: an earlier host loads these plugin versions withoutlabelandparams.One replica polls. The sources start on the proxy replica that holds the reception lock (the Postgres advisory lock of the other inbound connections), not on every replica.
Records set aside. The table
source_deferred, the routeGET /api/sources/deferredand the outcomedeferredofmeridian.events.generatedare removed. At the proxy's first boot, the pending rows ofsource_deferredmove intoinbound_messages, under the connection named after the source, and the table is dropped; they are delivered only by a connection with that identifier. A record whose delivery fails is delivered again by the reception (delivery-unavailable, outcomeunavailable), and shows in the traffic history of its connection. Move the alerts onoutcome: deferredtooutcome: unavailable.Routing audit.
routing:audithas no--instanceoption: it reads the feeds of the pulled connections fromGET /api/connectionsandGET /api/connections/protocols. See Audit event routing.Health data. The polled records (FHIR resources) are kept in
inbound_messages, like HL7 v2 and PN13 messages, without purge. Include the table in the backup and retention policy of the application database (see Back up and restore).Rollback. An earlier release does not read a connection of a source type and polls only the sources under
sources: restore that form when rolling back.
Credential reference renamed credentialId
The field that names a credential in a workflow is credentialId, as on an exposed
agent (api.credentialId) and a connection: the configuration of a trigger.webhook
trigger, of a flow.wait node in webhook mode, and of an EAI connection.
- At the proxy's first boot, every version of every workflow in the definition
store (drafts, published and historical versions, so that a rollback stays
authenticated) has its
credentialkey renamedcredentialId, comments and layout kept, content hash recomputed; the definition generation is bumped when a row changes. The EAI connections table (inbound_connections.config) is migrated the same way. Both migrations are idempotent and run under a lock, so replicas booting together migrate once. - Specs that arrive later with
credential(an import, an older client) are renamed when they are saved. A workflow file kept in git withcredentialis renamed the next time it is seeded; rename it in the repository too. - Rollback of the platform. An earlier release reads
credentialonly: after a rollback, webhooks and webhook waits with authentication answer503until their workflows are saved again withcredential. Back up the definition store before the upgrade (see Back up and restore).
Run settlement
The release that settles runs in the proxy (see
Run index — Final status) adds, at the proxy's
first boot, the column restate_url to the run index and the partial index
runs_live_restate_url_created_at. It fills no existing row.
Rows of an earlier release have no
restate_url. At each settlement pass the proxy reads how their invocation ended: when its Restate knows the invocation (470,200, or a failure with a code), the row is stamped with the proxy'sRESTATE_URLand settled like the others. When Restate answers404, the row is left as it is and counted in the warning<n> run(s) d'un autre Restate ou sans Restate d'origine, non réglé(s), at each pass: such a row is never frozen automatically, since nothing says whether this Restate ran it. List these rows, check each run, and set the status of those you know have ended:SELECT id, workflow_file, created_at FROM runs WHERE final_status IS NULL AND restate_url IS NULL; UPDATE runs SET final_status = 'failed' WHERE id = '<runId>' AND final_status IS NULL;A proxy pointed at another Restate than the one of its rows (a wrong
RESTATE_URL, an index shared with another stack) freezes none of them: they are counted in the same warning.PostgreSQL: the
CREATE INDEXruns inside the init transaction, under the advisory lock, like the earlier indexes. On a largerunstable it holds writes onrunswhile it builds, and the other replicas wait for it at boot. To build it without holding writes, create the column and the index before the rollout; the boot then finds them:ALTER TABLE runs ADD COLUMN IF NOT EXISTS restate_url TEXT; CREATE INDEX CONCURRENTLY IF NOT EXISTS runs_live_restate_url_created_at ON runs (restate_url, created_at, id) WHERE final_status IS NULL;
Hermes connection for terminology
@posos/snomed 0.1.3 contributes the connection type hermes and its terminology
adapter requires a connection of that type. It declares sdk: "^0.1.6".
- Instance manifest. Move the Hermes URL (
terminology.snomed-ct.config.baseUrl, orHERMES_BASE_URL) to a connection of typehermesunderconnections:, and the language reference set (config.languageRefsetId, orHERMES_LANGUAGE_REFSET_ID) to its parameterlanguageRefsetId, written as a string. Name it inconnectionDefaults.hermes, or interminology.snomed-ct.connection. See Configure terminology. - Former configuration. The old forms are still read with a warning
(
[hermes] config.baseUrl est déprécié …,[hermes] HERMES_BASE_URL est déprécié …). - No implicit URL. Without a connection,
config.baseUrlorHERMES_BASE_URL, the adapter no longer falls back tohttp://localhost:8081: it logs[hermes] connexion indisponible (…)once and degrades as when Hermes is unreachable (codes accepted, subsumption refused, raw code as label).
5. Verify
GET /healthzandGET /readyzon the proxy —/readyzreturns 503 with a{ checks }detail if the Restate ingress or the run index isn't reachable yet after the restart.- Check the endpoint's plugin-load logs (
component: "plugins") for the plugin you just published. It should appear inplugins chargés, with nocontrat plugin incompatible — plugin ignoréline and none of the other load warnings (« dépendance signalée », « événement signalé », « exemple d'événement retiré », « déclencheur signalé », « nœud signalé », « binding hors contrat », « plage sdk insuffisante », « clé ignorée »), in particular no dropped agent (agent « X » : … agent ignoré) and no dimension outside thedependsOnclosure (dimension « X » inconnue dans la fermeture dependsOn), and nochargement échoué(a registry collision, such as a dimension already contributed by another plugin). The repository's plugins load without any of them. A registry plugin that resolved to an older version than the highest of its range is logged undercomponent: "registry"as « plugin requis : versions plus hautes écartées ». See Observability signals. - Check that the dashboards and alerts read
meridian.*metrics once the new proxy and endpoint export them. - Start one new event and confirm the run picks up the new plugin version/behavior, while runs already in flight before the restart keep behaving as journaled.
Releases that change event routing
Some releases change which workflows start on an event, the run ids, or the HTTP
contract between the console and the API. Their release notes give the release-specific
steps: a pending changeset under .changeset/ until the Version PR runs, then
the entry of that version in the library changelogs (libs/shared/CHANGELOG.md
and the other libraries of the fixed group). The API changelog
(apps/api/CHANGELOG.md, written by release-please from the commit titles)
does not carry them. These rules apply to all of them:
- Audit the routing of the deployed instance before the upgrade, and again before a rollback — see Audit event routing. Run the audit script of the release of the API it audits: it reads fields of the API of its own release and has no fallback for another one. For each event type that starts several workflows, accept it, set a filter or unpublish.
- No publication during the upgrade.
- Replace every proxy replica at once when the run ids change: two replicas of different versions ingesting the same event in the same instant would start it under two keys.
- Keep the stop shorter than
startOverlapSeconds(600 s by default) for eachfhir-pollconnection; beyond it, FHIR resources modified during the stop are not read. See Restarts and the overlap window. - Deploy the console with the API when the HTTP contract between them
changes: the
POST /api/eventsresponse, the node metadata served byGET /api/catalog(a trigger'sreceives, or a new trigger family such asconfigurable, served for « Événement reçu »), or thePOST /api/expressions/checkrequest (trigger: { type, config? }). A console released before « Événement reçu » cannot load a catalogue that serves it and shows the core nodes only; a later console leaves out only the node type whose trigger family it does not know. Upgrade themeridian-config-planeimage in the same rollout asmeridian-api, and downgrade both together. The release notes of the API and the console mark such a release as breaking.
Seeds changed by a release are not re-applied to a populated store. A seed file
whose slug the store does not know is seeded; when its event already starts a
published workflow, it is stored as a draft, with the log line
Graine « <slug> » semée en brouillon : …. Publish it from the console once
reviewed. See Manage workflow storage.
Roll back
- Plugin roll back is just re-pointing the manifest's range at the older
version (or leaving a
pathoverride in place) and restarting endpoint + proxy — published versions are immutable, so the old version is still sitting in the registry untouched. The runs that the newer version blocked go on once the version that wrote their journal is loaded again (see Recover runs blocked by a missing plugin). - Platform roll back is redeploying the previous image tag, after
unpublishing the workflows that use a core node the previous image does not
know and waiting for their runs in progress to end (see
Workflows that use a new core node).
The endpoint is not rolled back below a journal format while runs of that
format are in flight: set
RUN_JOURNAL_FORMAT=1, then roll the endpoint back once the runs of format 2 have ended (see Journal formats). If you also need to roll back the SDK version a plugin was checked against, the two kinds of plugins behave differently. A plugin found onPLUGINS_PATHor through apath:entry whosesdk:range no longer matches is dropped with a warning, not a hard boot failure, so a partial rollback degrades gracefully rather than blocking restart. A registry plugin is resolved again: the highest version the previous image accepts, by its plugin contract and its core plugin versions, is selected at the next boot, and the boot fails when no version of the range fits (see Business plugins that require a newer core plugin). An image older than the metrics rename exportsclinical.*again: switch the dashboards and alerts back with it. - A host older than plugin contract
0.1.1refuses every plugin that declaressdk: "^0.1.1". The plugins of the image roll back with it; a registry plugin on^0.1.1is skipped withcontrat plugin incompatible — plugin ignoré. Its nodes leave the catalogue: the workflows that use them fail validation, and events of its trigger types are refused. Pin such a plugin to a version on^0.1.0before rolling the host back.
Related
- Set up a plugin registry and Declare and install plugins — the registry and manifest sides of plugin resolution.
- Build and bundle and Publish to the registry — the plugin-author side of steps 1–2.
- Release and version — the changesets workflow behind step 3.
- Back up and restore — snapshot
restate-databefore a platform upgrade you're unsure about. - Audit event routing — which workflows start on each event type, before an upgrade or a rollback.
- Recover runs blocked by a missing plugin — runs in flight that Restate retries after a plugin or a core node type left the endpoint.
- Why durable execution — why restarting the endpoint never loses an in-flight run.
- Bundling and distribution and Autonomy and packaging — why plugin artifacts are self-contained and versioned independently of the platform.
Multiple-entry workflows
Deploy compatible Restate endpoints on every replica first, then the proxy and console. Multiple entry nodes are available by default, without an activation setting. The run store adds nullable trigger_id and spec_json columns automatically in SQLite and PostgreSQL; no backfill or single-entry YAML conversion is required. Prefer RUN_JOURNAL_FORMAT=2 for new runs: it freezes the entry projection and branch plan.
Before rolling back to a binary that predates multiple entries, drain those runs and unpublish or convert the multi-entry definitions to single-entry workflows. Keep a compatible console to inspect historical multi-entry runs.