Documentation / Exploitation / Guides pratiques / Configure ports and adapters

Configure ports and adapters

This recipe binds the platform's hexagonal ports — patient, prescription, agent, notification, decision — to concrete adapters contributed by loaded plugins, in the instance manifest's ports: block. It assumes you already have an instance manifest and know which plugin you want to source each capability from. For the full field-by-field schema see instance manifest reference; for the concept behind ports/adapters see plugins concept and the glossary entry for Port (this is the hexagonal sense — not the node-level ports wired inside a workflow, see wire ports for those).

Prerequisites

  • An instance manifest file to edit (INSTANCE_CONFIG, defaulting to instance.yaml at the repo root — see environment variables). If you don't have one yet, start with your first instance.
  • The plugin that contributes the adapter you want to select is already loaded: via PLUGINS_PATH, or declared under the manifest's plugins: and resolved from a registry — see declare and install plugins and set up a plugin registry.
  • You know the adapter's name — the identifier that plugin registered for the port, not the plugin's package name. See bundled plugins for what ships with the platform, or the plugin's own manifest/docs otherwise.

Steps

1. Open the ports: block

ports is a free-form map, keyed by port name, of { adapter, config } (PortBinding, defined in libs/shared/src/plugin/instance.ts). The key is not restricted to any fixed list: every port is defined by the plugin that owns its contract — patient and prescription by @posos/clinical, notification by @posos/notify, decision by @posos/decision — while the SDK types only agent and terminology. A plugin can define and expose its own port under the same mechanism — it's wired the same way.

apiVersion: meridian/instance-v1
name: my-instance
ports:
  patient: { adapter: <name>, config: { ... } }
  prescription: { adapter: <name>, config: { ... } }
  agent: { adapter: <name> }
  notification: { adapter: <name>, config: { ... } }
  decision: { adapter: <name>, config: { ... } }

2. Bind patient and prescription (for clinical workflows)

No port is required at boot — an instance without a patient record starts fine. But any workflow touching the record (patient.*, order.* nodes, human validations published to the record) fails at execution with a configuration error naming the missing port, and GET /api/records/patient/:id has nothing to read (the patient adapters declare record: { contextKind: patient }, which is what wires that route and validation publication). There is no platform default: pick one adapter name per port and, if the adapter needs it, a config object.

Development, backed by the seeded in-memory store — the reference adapters of @posos/clinical (adapter name in-memory):

ports:
  patient:
    adapter: in-memory
    config:
      patients:
        - demographics: { patientId: patient-42, sex: male, ageYears: 72 }
  prescription:
    adapter: in-memory
    config:
      prescriptions:
        - id: presc-9001
          patientId: patient-42
          items: [{ coding: { code: metformine, display: "Metformine 1000 mg" } }]

Production, backed by a FHIR warehouse (@posos/fhir), reached through a connection of type fhir-r4 (see Manage connections):

connections:
  entrepot: { type: fhir-r4, baseUrl: http://fhir.internal:8080/fhir }
connectionDefaults:
  fhir-r4: entrepot
ports:
  patient: { adapter: fhir }                         # default connection of the type
  prescription: { adapter: fhir, connection: entrepot }

adapter is matched by the exact (port, name) pair against every loaded plugin's contributes.adapters entries (AdapterContribSchema in libs/shared/src/plugin/manifest.ts; matched by findAdapter() in apps/api/src/plugins.ts). If nothing matches, the host logs a warning and the port stays unbound; the first behavior that needs it fails at execution with:

Port « patient » non branché — sélectionnez un adapter dans le manifeste
d'instance (ex. ports.patient = { adapter: in-memory }).

(buildPorts() in apps/api/src/instance.ts binds; the guard lives in the consuming behaviors). For a full FHIR-specific walkthrough (auth, resource mapping) see connect a FHIR warehouse.

3. Bind agent (optional — needed for agent nodes)

Only required if the workflow(s) you run use agent nodes. The bundled runner is @posos/llm:

ports:
  agent: { adapter: llm, config: { maxIters: 8 } }   # maxIters optional, defaults to 8

The runner reads the LLM provider's API key from the environment (ANTHROPIC_API_KEY, OPENAI_API_KEY, …, per provider — see libs/llm-client/src), not from this config. Which provider/model an agent actually calls is decided per-agent, in the plugin that contributes the agent (see author an agent) — this binding only selects which runner implementation executes that call. There is no mock fallback: an agent node fails loudly at run time if its provider key is missing.

4. Bind notification (optional)

Selects the outbound diffusion channel of the notify.send nodes that designate no connection. A node that designates one (Slack, SMTP, webhook) uses it instead; see Send notifications. Two adapters of @posos/notify:

# Development: prints each notification to stdout, no config needed.
ports:
  notification: { adapter: console }
# POSTs each notification as JSON to the URL of a notification-webhook connection.
connections:
  relais-bips:
    type: notification-webhook
    baseUrl: https://bips.chu.local/notify
    auth: { scheme: bearer, token: env:BIPS_TOKEN }
ports:
  notification: { adapter: webhook, connection: relais-bips }

The webhook adapter retries a network error, a 429 or a 5xx up to three attempts, then fails the node; another error status fails it at once — delivery is part of the workflow's contract, not best-effort (external-plugins/notify/adapters/webhook.ts). Its former configuration (config.url, config.headers, config.timeoutMs, or NOTIFY_WEBHOOK_URL) is still read, with a warning. This port only governs outbound diffusion; writing a notification to the patient record itself goes through the patient port, independently of how (or whether) it's also pushed out.

5. Bind decision (optional — needed for Décision intelligente nodes)

Selects the decision model answering the Décision intelligente nodes (decision.ask) that designate no connection. @posos/decision owns the port and ships no adapter; the typesafe adapter of @posos/typesafe calls POST {baseUrl}/v1/systemone through a connection of type typesafe-systemone. Each server is a connection; a node designates one, else it uses the port bound here, on the connection it names or the default connection of the type:

connections:
  typesafe:
    type: typesafe-systemone
    baseUrl: https://api.typesafe.ai
    defaultModel: jev-latest                 # parameter of the type, default jev-latest
    auth: { scheme: bearer, token: env:TYPESAFE_API_KEY }
  jev-local:                                 # compatible server inside the network, no key
    type: typesafe-systemone
    baseUrl: http://localhost:8089
    defaultModel: jev
    timeoutMs: 30000
connectionDefaults:
  typesafe-systemone: typesafe
ports:
  decision: { adapter: typesafe }            # or { adapter: typesafe, connection: jev-local }

The connection carries the URL, the key (bearer, header, or none), the timeout of each attempt (timeoutMs, 30 s by default) and extra headers; the model is the node's, else the connection's defaultModel. Transient failures (network, timeout, 408, 429, 529, 5xx) are retried up to four attempts, following retry-after (external-plugins/typesafe/adapters/typesafe.ts). Another provider protocol takes another adapter of the same port, contributed by its own plugin with its own connection type.

6. Set adapter-specific config

config is an opaque object passed straight to the adapter's factory, (config, deps?) => PortImpl (AdapterContribSchema.module/export/config in libs/shared/src/plugin/manifest.ts) — the platform doesn't validate its shape; each adapter defines and documents its own fields. The bundled adapters follow one recurring pattern — a config field overrides an environment-variable fallback of the same purpose:

Adapter config field Env fallback
fhir (patient/prescription) none: connection of type fhir-r4 (baseUrl still read, deprecated) FHIR_BASE_URL (deprecated)
hermes (terminology) none: connection of type hermes (baseUrl, languageRefsetId still read, deprecated) HERMES_BASE_URL, HERMES_LANGUAGE_REFSET_ID (deprecated)
webhook (notification) none: connection of type notification-webhook (url still read, deprecated) NOTIFY_WEBHOOK_URL (deprecated)
typesafe (decision) none: connection of type typesafe-systemone none

An adapter that declares connection: { type } in its plugin manifest receives deps.connection: ports.<port>.connection names the connection, else the default connection of the type is used. Its URL and authentication live in the connection, not in config.

Because instance manifests are versioned in git, prefer the environment-variable route for anything secret (auth headers, API keys) rather than writing it into config — see security posture. terminology: bindings use the same PortBinding shape but route per code system rather than one adapter per port; see configure terminology for that recipe.

7. Apply the change

The manifest is parsed and its ports resolved once, at process start (loadInstance() in apps/api/src/instance.ts, called from the entry points apps/api/src/restate/server.ts and apps/api/src/server/api.ts) — there is no hot reload. Restart the endpoint and proxy processes to pick up the edit; see upgrade the platform for the restart order in a running deployment. An invalid manifest (bad YAML, wrong types) fails the same Zod parse just as loudly, before the process comes up.

Verify

  • On restart, the instance component logs one info line per port that resolved, with port and adapter fields, or a warning naming the adapter it couldn't find — check the endpoint/proxy logs (see enable observability for log shape and correlation).
  • If a declared port doesn't resolve, boot continues with the port unbound (a warning names the adapter); the first behavior that needs it fails with Port « … » non branché — fix the adapter name or load the missing plugin, then restart.
  • Exercise the binding end-to-end: send an event that reaches the port (e.g. run a workflow that reads the patient record, or trigger a notification) and confirm it lands where the adapter is supposed to send it — see monitor executions.
97 documents11 sectionssource : /docs · généré au build