Documentation / Conception / Référence / Event ingestion API

Event ingestion API

POST /api/events ingests one event and starts a run for each workflow that receives it. It is the endpoint the Emit an event composer of the console calls, and the one an external producer calls directly. Event source bindings and EAI message bindings go through the same ingestion path (ingest.ts) without the HTTP layer. For the model behind routing, see Events and triggers.

Request body

A JSON object.

Field Required Description
type yes The event type. A loaded trigger must claim it: every declared event is claimed by Événement reçu (trigger.event-received), an address by its trigger.
Context identifiers per contract The id field of each context kind of the event: patientId for patient, encounterId for encounter… Send those of the required kinds.
Contract fields per contract The fields of the declared event, in the JSON form of their type (table below). Send the required ones.
Other fields no Accepted and passed on as received.
eventId no Identifier of the event. Generated as evt-<uuid> when absent. Send your own when you may retry: a retry without one is a new event.
occurredAt no Time of the event (ISO 8601). The reception time when absent or blank. Checked as a DateTime by the contract check.
__workflowFile no Designation: the workflow file (<slug>.workflow.yaml) that receives the event, alone. Removed from the event.
__allowDraft no With __workflowFile, true runs the workflow's draft when the file has no active version. Removed from the event.

The contract of an event is shown by the trigger inspector (Declared event), the Plugins page (Declared events) and GET /api/catalog (eventTypes). JSON form of a declared field type:

Declared type JSON value
String, Code, Uri string
Decimal, Number number
Integer integer number
Boolean true or false
Date string YYYY-MM-DD
DateTime ISO 8601 string
coded type the code, as a string
quantity { "value": <number>, "unit": "<unit>" }
reference { "id": "<id>" }
list array of the item form
object type object with the type's fields

Example, for BioResultReceived (context patient and encounter, both required; fields analyte String and value Decimal required, unit String optional, and others):

{
  "type": "BioResultReceived",
  "patientId": "patient-42",
  "encounterId": "sejour-7",
  "analyte": "creatinine",
  "value": 160,
  "unit": "µmol/L"
}

What is checked

Check Refusal
The body is JSON 400 JSON invalide; 413 above 10 MiB
type is present 400 Événement sans type.
A loaded trigger (core or plugin) claims type: a declared event, claimed by Événement reçu whether or not a plugin contributes a trigger for it, or an address 400 Aucun déclencheur ne revendique l'événement « <type> ».
The event conforms to the contract of its declared type (EVENT_CONTRACTS=enforce) 400 Événement « <type> » non conforme à son contrat., with issues

An event of an address type (a type claimed only by triggers with their own address, which no plugin declares) is checked only for its type. Under EVENT_CONTRACTS=observe (the default) or off, an event without patientId starts runs with no patient context, and an event without a required field starts runs whose trigger has no value for it.

Contract check

An event whose type is a declared event is checked against its contract once, after the claim and the refusal of a type with its own address, before routing. The instance's EVENT_CONTRACTS variable sets the mode (Environment variables):

Mode Event that does not conform
off Not checked.
observe (default) Accepted and routed. Each of its entries in the traffic history carries its issues (GET /api/events/recent, contract), the check is counted by meridian.events.contract, and a warning logs the number of issues.
enforce Refused: 400 Événement « <type> » non conforme à son contrat., with issues. The event starts no run.

An issue is a required field or required context identifier that is absent, a declared field or context identifier whose value does not conform to its type, or an occurredAt that is not a valid DateTime. The absence and validity rules are listed under contributes.events. Fields the contract does not declare are not checked. Each message names the field and the expected type, never the value received:

{
  "error": "Événement « BioResultReceived » non conforme à son contrat.",
  "issues": ["analyte : requis.", "value : attendu un nombre."]
}

The check does not modify the event: filters and triggers read it as received. It applies to a designated event (__workflowFile) too. It runs before duplicate detection, so an event sent again is checked again.

Routing and its refusals

Without __workflowFile, every published workflow whose trigger receives the type starts, subject to its own filter (fan-out): the workflows on its dedicated triggers and those on Événement reçu configured on the type alike, each with the run id <eventId>:<slug>. Filters are evaluated on the body as received.

Situation Response
No published workflow receives the type 400 Aucun workflow pour l'événement <type>
The type is claimed only by triggers with their own address (webhook, schedule, sub-workflow call, EAI reception) 400 L'événement « <type> » s'adresse à un workflow précis. Préciser __workflowFile.
Published workflows receive the type and no filter keeps the event 200, runs: [] (filtered)

With __workflowFile, only that workflow receives the event and its filter is not evaluated.

Situation Response
The file has no active version (and no draft fallback) 400 Workflow introuvable : <file>
The workflow's trigger receives another type 400 Le workflow « <name> » attend un événement <expected>, pas <type>.
The workflow's trigger has no event chosen (Événement reçu without config.event) 400 Le workflow « <name> » n'a pas d'événement de déclenchement. Choisir l'événement de son déclencheur.
The workflow's trigger is missing from the catalogue (plugin not loaded, misspelled type) 400 Le déclencheur « <trigger type> » du workflow « <name> » est absent du catalogue. Charger le plugin qui le fournit ou vérifier le type du déclencheur.
__allowDraft: the draft cannot be read 400 Brouillon « <slug> » illisible., with issues
__allowDraft: the draft fails the publication checks 400 Brouillon « <slug> » invalide — corrigez-le avant de le lancer., with issues

A draft run is indexed with specVersion: null and the draft's specHash. It takes the run id of the published version (<eventId>:<slug>): designating a draft with the eventId of a real event takes the published version's place for that event.

Response

{
  "eventId": "evt-3f2b8c1e-5d4a-4b7e-9c61-2a8f0d7e4b19",
  "runs": [
    { "id": "evt-3f2b8c1e-5d4a-4b7e-9c61-2a8f0d7e4b19:bio-result", "created": true, "match": "unfiltered", "status": "suspended", "…": "…" }
  ]
}
Field Description
eventId The event's id, received or generated.
runs One entry per workflow that received the event, in file order. Each entry is the run's view, as GET /api/runs/<id> serves it, plus created and match. The response waits up to about 6 seconds (40 × 150 ms) for new runs to leave running.
failed Present only with 503: { workflowFile, error } for each run that could not be submitted to Restate.
Status Meaning
201 At least one run started, no failure.
200 No run started and no failure: the event was already received (entries with created: false), or no filter keeps it (runs: []).
400 Refused, body { error, issues? } (tables above).
413 Body over 10 MiB, body { error }.
503 At least one run could not be submitted to Restate: runs lists the runs started or already known, failed the failures.

created is true for a run this request started and false for a run the event had already started (duplicate). match gives the reason of the delivery; the run detail in the console shows the same labels.

match Console label Reason
selector trigger filter (« filtre du déclencheur ») The workflow's filter kept the event.
unfiltered no filter (« sans filtre ») The workflow has no filter.
targeted designated (« désigné ») __workflowFile named the workflow.
pinned own address (« adresse propre ») The workflow's trigger has its own address.
null none A run indexed without a reason (runs from before fan-out routing).

OpenAPI description

GET /api/openapi.json describes POST /api/events (operation ingest_event) when at least one type is accepted: a type claimed by a trigger without its own address, which includes every declared event. The request body is a oneOf of one schema per accepted type, discriminated by type, with one example per type (the trigger's example event, the same for every trigger of a declared event).

Schema of Content
A declared event The contract's fields and the id field of each context kind of the event. required holds type, the required fields and the id fields of the required kinds.
A type without a contract type only: the body is free.
Both The envelope (eventId, occurredAt, optional, null accepted), the ingestion commands __workflowFile and __allowDraft, and additionalProperties: true, since fields outside the contract are accepted.

The schemas follow the contract check and are never stricter:

  • a field has the JSON form of its type (table under Request body). A DateTime, occurredAt included, carries the pattern of the check's grammar, which accepts the reduced precisions YYYY, YYYY-MM and YYYY-MM-DD; a Date has format: date. Time, Uri and the other text types are plain strings. A code of an internal value set lists its codes (enum), a reference is { id };
  • an optional field may also be null or a blank string (absent). A required text field must contain a non-blank character, and a required list at least one item;
  • a context id is a string or a number; a required one must contain a non-blank character, an optional one may be null.

The responses are those of the statuses table above: { eventId, runs, failed? } for 201, 200 and 503, { error, issues? } for 400 and 413.

Run ids

A run's id is <eventId>:<slug>, the slug being the workflow file name without .workflow.yaml. A trigger with its own address keeps the eventId as run id. Each workflow receives a copy of the event whose eventId is its run id; the original id is the run's sourceEventId, shown as Source event: in the run detail.

GET /api/runs?event=<eventId> lists the runs started by one event, runs indexed before fan-out routing included (by their id); the run detail's Same event link opens that list. A run id contains :: encode it in a URL path (encodeURIComponent).

Duplicates and retries

  • Each workflow receives an event once. Posting the same eventId again starts no second run of a workflow that received it: the response lists the existing runs with created: false.
  • A workflow published after the event's first reception does not receive it on a resend. When that leaves nothing to start, the response is 200 with the runs the event started earlier.
  • After a 503, send the same body with the same eventId: only the missing runs start. The composer fills the eventId in for this (Payload completed with eventId {eventId}: sending again only starts the failed workflows.).
  • An event that was refused or filtered leaves no trace: posting it again is a first reception.

Other producers

Event source bindings and EAI message bindings do not wait for the runs. Their outcome is accepted, duplicate, filtered or rejected, counted by the metric meridian.events.generated. When a run could not be submitted, the other workflows are still tried, then the inbound reception delivers the source record or the message again (unavailable): only the missing runs start. Their events go through the contract check: under enforce, a non-conforming event is rejected, and a record without a field or identifier the contract requires becomes no event (the binding's effective require, see Contribute an event source and binding). See Connect an event source and Connect an EAI.

See also

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