Connect an event source and activate bindings
This recipe wires up event-driven ingestion on a running instance: point a
source mechanism (e.g. fhir-poll) at a real endpoint, then decide which of
the bindings contributed by your plugins are allowed to fire. As an operator
you own the connection — the sources: and eventBindings: blocks of the
instance manifest (libs/shared/src/plugin/instance.ts). The query each
binding runs and the map it uses to project a raw record into a domain
event are authored by the plugin, not configured here — see
Contribute an event source and binding
and The open event model
for that side.
Before you start
The plugin contributing the source mechanism and the bindings you want must already be installed — see Declare and install plugins. Sources and bindings are plugin
contributes.eventSources/contributes.eventBindingsentries (libs/shared/src/plugin/manifest.ts); the instance can only activate what a loaded plugin contributes, never invent one.You need the exact
nameof each binding and thesourcemechanism it targets. List what's loaded:curl -s http://localhost:4000/api/plugins | jq '.[].contributes.eventBindings'or read the plugin's
plugin.yamldirectly. For example,external-plugins/fhir/plugin.yamlcontributes one mechanism,fhir-poll(external-plugins/fhir/sources/poll.ts), and several bindings on top of it —fhir-creatinine,fhir-medication-prescribed,fhir-patient-admitted,fhir-patient-discharged,fhir-allergy-recorded,fhir-communication-received,fhir-condition-recorded, plus oneresolve-style binding,fhir-validation-decided, that resumes suspended runs instead of starting new ones.
1. Configure the source connection
Add an entry under sources: in the instance manifest, keyed by the
mechanism's name (not a binding's name). The value is an opaque config
object passed straight through to the mechanism's factory as
SourceFactoryOpts.config (libs/shared/src/plugin/source.ts) — its shape is
whatever that specific mechanism reads, not something the core validates.
For the bundled fhir-poll mechanism, the recognized keys are baseUrl,
intervalSeconds (poll period, default 15) and count (page size, default
50):
sources:
fhir-poll: { baseUrl: http://localhost:8090/fhir, intervalSeconds: 15 }
A source is only instantiated if at least one of its bindings ends up active in step 2 — configuring a connection for a mechanism with no active binding is a harmless no-op, not an error.
2. Choose which bindings are active
By default, every binding whose source was configured in step 1 is active —
you get all of them. To activate only a subset, add an eventBindings:
allowlist naming the bindings you want, by name:
eventBindings: [fhir-creatinine, fhir-medication-prescribed]
Omit the key entirely to keep everything available on the configured sources
active — this is the shipped default in instance.yaml and
instance.example.yaml, which both leave eventBindings commented out. The
rule is applied verbatim in startEventSources
(apps/api/src/server/api.ts): a binding is active only if its source has a
sources: entry and (no allowlist is set, or the binding's name is in
it).
3. Restart the API
The instance manifest is read once at boot
(apps/api/src/instance.ts, loadInstance — the INSTANCE_CONFIG env var if
set, else instance.yaml at the repo root), and startEventSources() runs
once, right after the HTTP listener comes up
(apps/api/src/server/api.ts). Changes to sources: or eventBindings: are
not hot-reloaded — restart the API process to pick them up.
Verify
- Watch the boot logs for the
sourcescomponent: a successful start logssource démarréewith the source name and the active feed names; a source name with no matching mechanism logsmécanisme de source introuvable; a factory that throws onstart()logsdémarrage échoué(all three fromstartEventSourcesinapps/api/src/server/api.ts). - Confirm real traffic turns into activity: watch
GET /api/runsfor new runs after data lands upstream, or check theclinical.events.generatedcounter (attributessource,feed,event,outcome) foroutcome: "accepted"— see Enable observability. - For a
resolvebinding — it resumes a suspended validation instead of starting a run, see Add human validation — confirm the targeted run leaves its pending state once the upstream decision lands.
Troubleshooting
- No
sourceslogs at all:startEventSourcesreturns early when zero bindings are active. Confirmsources:is keyed on the mechanism'sname(e.g.fhir-poll), not on a binding's name. mécanisme de source introuvable: thesourcea binding points to isn't contributed by any loaded plugin. Confirm the plugin is installed and double-check the exact source name viaGET /api/plugins.- A binding you expected is silently inactive: it's excluded by your
eventBindings:allowlist, or itssourcehas no matching key undersources:. Both are matched by exact string equality — a typo doesn't error, it just leaves the binding off. démarrage échouéforfhir-poll: the factory throws synchronously ifbaseUrlis missing from bothconfig.baseUrland theFHIR_BASE_URLenvironment variable (external-plugins/fhir/adapters/client.ts,resolveBaseUrl) — addbaseUrlunder thefhir-pollentry.- Source is polling but no runs appear: the binding's
mapmay be failing (projection (map) échouée, counted as a rejected generation) or the projected eventtypehas no workflow trigger claiming it (ingestEventrejects with "Aucun déclencheur ne revendique l'événement…"). Both are plugin/workflow authoring concerns, not instance configuration — see Contribute an event source and binding and Workflow DSL.
Related
- Declare and install plugins — get the plugin that contributes the source and bindings onto the instance first.
- Configure ports and adapters — the sibling delegation mechanism, in the same manifest, for request/response ports rather than inbound events.
- Connect a FHIR warehouse — configuring the
fhirport adapters that typically run alongsidefhir-poll. - Enable observability — metrics and logs for ingestion.
- Instance manifest — full schema for
sourcesandeventBindings. - Contribute an event source and binding and The open event model — how bindings, queries and maps are authored on the plugin side.