Documentation / Conception / Explications / Events and triggers

Events and triggers

A workflow starts on an event: a lab result received, a prescription recorded, a note added to the record. This page explains what an event carries, how a trigger receives it, and which workflows start when one arrives. The tasks (writing a filter, reading the routing table) are in Choose which events start a workflow, and the HTTP contract of POST /api/events is in Event ingestion API.

An event is a typed JSON object

An event is a JSON object whose type field names its event type (BioResultReceived, CommunicationReceived). The rest of the body carries the identifiers of the patient, the encounter or another context, and the facts of the event: the analyte, the value, the author of a note. The ingestion adds an eventId and an occurredAt when the producer does not send them.

Events are produced by event source bindings (a FHIR feed polled by the platform), by EAI message bindings (HL7 v2, PN13), by the Emit an event composer of the Executions page, and by any caller of POST /api/events. Webhooks, schedules and sub-workflow calls also produce events, addressed to one workflow (see Events addressed to one workflow).

Declared events and addresses

A plugin can declare an event type in its manifest (contributes.events). The declaration is the event's contract:

  • its context kinds, each required or optional; an event carries the identifier of a kind in that kind's id field (patientId for patient, encounterId for encounter);
  • its fields, each with a type (String, Decimal, DateTime, a coded type…) and a required flag;
  • an example;
  • optionally a default filter (see Meridian's own writes come back as events).

One plugin declares a given type. @posos/clinical declares the clinical events (BioResultReceived, MedicationPrescribed, ConditionRecorded…) and @posos/common declares CommunicationReceived.

Two kinds of triggers receive a declared event:

  • Événement reçu (trigger.event-received), a core trigger: the event is chosen in its configuration (trigger.config.event), among every declared event of the loaded plugins. Its outputs are the event's fields, in declaration order;
  • a dedicated trigger, which a plugin contributes for one of its events and which references it with event: in the manifest. Its outputs can be renamed or typed: a reference to the encounter, an Observation built from several fields.

Either way, the event type, the context kinds the trigger establishes, its example and its default filter are read from the declaration. When a plugin provides a dedicated trigger for an event, prefer it: its outputs are typed for the domain. A trigger with its own address (a webhook, a schedule, a sub-workflow call, an EAI reception) receives events of an address type instead, which no declaration describes: its payload is described by the trigger's outputs. A type is either a declared event or an address, never both. Declaring an event is a plugin author's task, described in Declare an event.

The contract can be read in three places:

  • in the editor, the trigger inspector shows the Declared event section (« Événement déclaré »): the context kinds with their identifier field, each marked required or optional, and the fields with their type, a required mark and their description. For Événement reçu, the section describes the event chosen in Event (« Événement »);
  • on the Plugins page, the Events tab of the declaring plugin lists its Declared events (« Événements déclarés »), and the Event catalogue lists every event type of the loaded plugins (see Inspect loaded plugins);
  • GET /api/catalog serves the contracts in eventTypes.

The contract is open

The inspector states it under the contract: Undeclared fields are accepted and passed on as received. A field the contract does not list is neither checked nor removed: it reaches the filters and the workflows as received.

The fields and identifiers the contract declares are checked at ingestion, according to the instance's EVENT_CONTRACTS mode (see Event ingestion API — Contract check):

  • observe, the default: an event without a required field or identifier, or with a value of another type, still starts its workflows, and the trigger has no usable value for that field. The traffic history marks the event non-conforming (see Monitor executions);
  • enforce: such an event is refused and starts no workflow;
  • off: nothing is checked.

An event of an address type is checked only for its type.

The declarations themselves are checked when the plugin loads: field types, context kinds, the example and the bindings that produce the event. An invalid declaration is ignored, a non-conforming example is removed, and a binding that does not cover the contract is reported in the API log and stays active.

Context kinds

A trigger on a declared event establishes every context kind of the event, required and optional, so a workflow on that trigger can list them all in its context (Workflow DSL — context). This holds for Événement reçu under the event of its configuration: its outputs are the event's fields, not the context identifiers, which a context.read node reads.

At run time a kind is present only when the event carries its identifier. encounter is optional on CommunicationReceived, MedicationPrescribed, MedicationDiscontinued, MedicationAdministered, ProcedureScheduled and ConditionRecorded: a prescription recorded outside any stay has no encounterId. The run of such an event has no encounter context. A context.read node on encounter then emits missing, a template that reads {context.encounter.id} skips its node, and a plugin node that needs the encounter fails. The same holds for a required kind whose identifier a producer left out: the run starts without that context.

What a filter reads

A trigger filter (trigger.selector) reads the event body as received, bound to event. It does not read the trigger's outputs, which the trigger computes after the filter has let the event through. The two often differ:

Event field (filter) Trigger output (graph)
event.value on BioResultReceived: the JSON number of the declared value: Decimal; event.analyte: the analyte key result: an Observation built from these fields; the analyte key is result.code.code
event.sender on CommunicationReceived: absent when the communication has no sender sender: always emitted, empty when the sender is unknown

A DateTime field is an ISO 8601 string and a coded field is a code string, so a filter compares them as text. A filter that reads a field the contract does not list passes the static check, which checks the syntax, the transforms and that paths start at event, not that the field is in the contract; on an event without that field, the path evaluates to undefined.

Filters are evaluated on the original payload, once per workflow, before each workflow receives its copy of the event. A guard reads the trigger's outputs inside a run that has already started: criteria.code.code == "creatinine" on result tests what event.analyte == "creatinine" tests in a filter. See Branch with guards and switch.

Which workflows start: fan-out

An event carries no workflow name. Among the published workflows whose trigger receives the event type — its dedicated triggers and Événement reçu configured on it alike (see Organisation metadata for the lifecycle):

  • each workflow without a filter starts on every event of the type;
  • each workflow with a filter starts on the events its filter keeps.

No workflow takes precedence over another: a filter restricts only its own workflow and takes no event away from the others. Two workflows with overlapping filters both start. Drafts never receive routed events.

Each workflow that starts gets its own run, with its own spec version, trace and status. The runs of one event execute in parallel, in no guaranteed order, and one failing does not affect the others. When no filter keeps an event, the event is filtered: no run starts. An event type that no published workflow receives is refused, and so is a type that no loaded trigger can receive: a type that is neither a declared event nor an address.

A filter that fails to evaluate (a transform applied to a value of the wrong type, such as |length on a number) excludes its workflow for that event and nothing else. A path that does not exist is not an error: it evaluates to undefined. The incident is logged and counted, and the traffic history shows Filter in error (« Filtre en erreur »).

Events addressed to one workflow

Some triggers have their own address: each of their events names one workflow, and fan-out never delivers it to the other workflows of the type.

  • Webhooks: the URL carries the workflow's own identifier.
  • Sub-workflow calls: the parent's workflow.execute node names its child.
  • Schedules: each tick is emitted for the file that owns the schedule.
  • EAI connections: each message received on a connection goes to the published workflows bound to it, subject to each one's filter (see Filter the messages of an EAI connection).

An event of one of these types sent to POST /api/events without a designation is refused.

Designation sends an event to one workflow: __workflowFile in the body names the workflow file, as the Emit an event composer does when a Target workflow is chosen. The designated workflow starts whatever its filter, and no other workflow receives the event. It must receive the event's type: a workflow on another type, or on Événement reçu with no event chosen, is refused.

Run identity and duplicates

A run's id is <eventId>:<slug>: the event id followed by the workflow file name without .workflow.yaml. Each workflow receives a copy of the event whose eventId is that run id; the original id is the run's source event (sourceEventId). A trigger with its own address keeps the eventId as run id.

Each workflow receives an event once. A second delivery of the same event to the same workflow is a duplicate: a retried POST, a FHIR feed re-reading its window or an EAI message delivered again starts only the workflows that have not received the event yet.

A workflow published after an event was first received does not receive that event when it is redelivered: a known event is routed only to workflows whose active version was published before its first reception. A designated delivery is not affected. An event that no workflow received (refused, filtered) leaves no trace, so its redelivery is handled as a first reception.

Meridian's own writes come back as events

A workflow that writes to the patient record can produce an event: a note written to the record (notify.send with toRecord) comes back as CommunicationReceived through the FHIR binding, with the sender « meridian », and a condition added to the record comes back as ConditionRecorded. With fan-out, such a write starts every workflow of that event type, including the one that wrote it.

A plugin can declare a default filter on an event (defaultSelector) for this case. CommunicationReceived has event.sender != "meridian": the console prefills it when a workflow is created on that event, with its dedicated trigger or with Événement reçu, and publication warns when a workflow's filter does not contain it. ConditionRecorded has no field that identifies the writer, and has no default filter. See Filter out Meridian's own writes.

See also

97 documents10 sectionssource : /docs · généré au build