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,occurredAtincluded, carries thepatternof the check's grammar, which accepts the reduced precisionsYYYY,YYYY-MMandYYYY-MM-DD; aDatehasformat: date.Time,Uriand 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
nullor 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
eventIdagain starts no second run of a workflow that received it: the response lists the existing runs withcreated: 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
200with the runs the event started earlier. - After a
503, send the same body with the sameeventId: only the missing runs start. The composer fills theeventIdin 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
- Events and triggers: declared events, fan-out, duplicates
- Run and resolve: send an event and follow the run
- Choose which events start a workflow: trigger filters
- Monitor executions: the console view of a run