Your first workflow
In this lesson we build a small clinical workflow from scratch in the visual editor: a lab result arrives, and we compute the patient's estimated glomerular filtration rate (eGFR). We wire it up, test it on a forged event, save it, and run it for real — all without writing a line of code or a line of YAML by hand.
By the end you will have a saved, executable workflow and will have seen it compute a result twice: once in simulation, once in a real run.
Nothing to install: we use the Config Plane UI that ships with the platform. If you do not have an instance running yet, follow Bring up your first instance first, then come back here.
What we are building
Two things happen in our workflow:
- A trigger fires when a lab result is received — this establishes the patient/encounter context and hands us the result.
- A compute node takes that result plus the patient's demographics and returns an eGFR (using the CKD-EPI 2021 equation).
A workflow is a typed dataflow graph: we connect the typed output of one node to the typed input of another, and the editor checks type compatibility as we go. See The dataflow model for the why; here we just follow the steps.
Step 1 — Open the editor on a new workflow
Open the Config Plane and go to Workflows. In the workflow list header, click + New. The creation dialog opens with the Empty starting point selected.
Step 2 — Name the workflow and pick its trigger
In the dialog:
- set Name to
My first workflow— the file below it becomesmy-first-workflow.workflow.yaml, where the workflow will be stored; - set Trigger to Lab result received. The Filter section appears
below it. On the demonstration instance,
bio-result.workflow.yamlalready receives every lab result, and the dialog says so: once published, our workflow starts in addition to it. Our workflow takes only creatinine results: in Guided mode, click Add a condition, set Field toanalyte, keep the operator equals, and typecreatinineas the Value. The generated expression readsevent.analyte == "creatinine".
Click Create. The editor opens on the new workflow. We already see one node
on the canvas: the trigger, labelled Lab result received, with the event it
receives, BioResultReceived, below its label — our starting point. Nothing is
saved yet.
Click the trigger. The inspector shows its Declared event section: the event
it receives, BioResultReceived, declared by the @posos/clinical plugin. Under
Context, patient (patientId · required) and encounter
(encounterId · required); under Fields, analyte (String, required),
value (Decimal, required), unit (String) and the other fields of a lab
result. This is where analyte in our filter comes from: a filter reads these
fields of the event as it arrives. Below comes the Filter section with the
condition we set in the dialog.
Notice the three panels: the palette on the left (trigger selector and a searchable node list), the canvas in the middle with a toolbar and the Build with AI panel, and the inspector on the right. The toolbar shows the name and, next to it, the target file.
Step 3 — Add the compute node
In the palette, type eGFR into the ⌕ Filter nodes… box. One entry appears:
eGFR (CKD-EPI 2021). Click it.
A new node lands on the canvas and becomes selected. In the inspector on the
right we can see its Identifier (auto-generated as ckd_epi_2021) and its
type, clinical.egfr.ckd-epi-2021. This node has two required inputs:
result (an Observation) and demographics.
Step 4 — Add the demographics source
Our compute node needs demographics, so we add a node that provides them.
Clear the filter box, type Load demographics, and click the Load
demographics entry. This node (patient.load-demographics) reads the current
patient's demographics and exposes them on its demographics output.
We now have three nodes on the canvas: the trigger, the eGFR compute node, and the demographics source.
Step 5 — Wire the nodes together
We connect ports by dragging from an output socket to an input socket. Each node shows its output ports on its right edge and its input ports on its left edge.
Make two connections:
- Drag from the trigger's
resultoutput to the eGFR node'sresultinput. - Drag from Load demographics'
demographicsoutput to the eGFR node'sdemographicsinput.
As each connection lands, the editor validates the two port types against each
other and flashes a confirmation such as Connected: result → result. If the
types were incompatible it would refuse the link and tell us why — this is the
same static check the engine runs. For more on ports and this live validation,
see Wire ports.
Both required inputs of the eGFR node are now fed. We have a complete little graph.
Step 6 — Simulate it
Before saving anything, we test the graph on a forged event. Simulation runs the current graph through the same interpreter as production, but with no durable run and no side effects.
Click ▶ Simulate in the toolbar. The inspector turns into the simulation panel, pre-filled for us:
- a Subject field: the kind
patient, chosen among the kinds the event establishes, and an empty identifier; - an Event (JSON) payload built from the trigger's example, with the
placeholder identifier
patient-exemple.
Type patient-42 in the Subject identifier: the simulation will read that
patient's real record, and the event's patientId follows the subject:
{
"type": "BioResultReceived",
"patientId": "patient-42",
"encounterId": "sejour-7",
"analyte": "creatinine",
"value": 160,
"unit": "µmol/L",
"display": "Créatinine",
"loincCode": "14682-9",
"status": "final",
"interpretation": "H",
"referenceLow": 45,
"referenceHigh": 90
}
The simulation runs our graph on this event directly: it does not evaluate the trigger filter. The filter is tested in the trigger's Filter section.
Click ▶ Run the simulation. Within a moment an overlay appears on the graph: each node is marked ✓ executed (or ⤼ skipped, with the reason), and the computed values appear under the output ports. A flash message reports how long it took and how many writes were captured.
Click the eGFR node to open its full detail in the inspector — its inputs and
its egfr output, exactly what it produced. That eGFR value is our first visible
result.
Simulation is the fast feedback loop for authoring; see Simulate a workflow for its options (validation scenarios, captured writes, real vs. mock agents).
Step 7 — Save and publish it
We are happy with the graph, so we persist it. Click Publish, confirm, and leave the version note empty.
The editor first saves the graph as the workflow's draft — the first save
creates the workflow — then publishes it: the server validates it (schema,
dataflow, expressions, codes, terminology, webhook address) and, if it passes,
version 1 becomes active and receives events. The confirmation and the publication result list the other
workflows that start on the same event — here bio-result, with its effects:
each creatinine result now starts both workflows. The editor then reloads on the saved
workflow in editing mode. (Save the draft alone stores the graph without
publishing it: a draft receives no events.)
What the editor stored is plain, readable YAML — this is the single source of truth for our workflow:
apiVersion: meridian/v3
name: My first workflow
context:
- patient
- encounter
trigger:
type: trigger.bio-result-received
selector: event.analyte == "creatinine"
nodes:
load_demographics:
type: patient.load-demographics
ckd_epi_2021:
type: clinical.egfr.ckd-epi-2021
connections:
- from: trigger.result
to: ckd_epi_2021.result
- from: load_demographics.demographics
to: ckd_epi_2021.demographics
For the full grammar of this file, see the Workflow DSL reference.
Step 8 — Run it for real
Now we run the workflow as a durable execution. In the Config Plane, go to Executions and click + Emit an event.
In the composer:
- Under Target workflow, pick
my-first-workflow.workflow.yaml. - The Event payload (JSON) is pre-filled from the trigger's example. A
required context id that the example does not carry gets a placeholder named
<kind>-exemple: replace"patientId": "patient-exemple"with"patientId": "patient-42", the patient we simulated with. - Click ▶ Send the event.
A new run appears in the list and the composer closes onto its detail. The timeline shows the event being received and each node executing, and the run reaches completed. This is the same computation we saw in simulation, now as a real, durable run.
Because we picked a Target workflow, only my-first-workflow started, and
its filter was not evaluated. With None: every workflow that receives the
type, the same event starts every published workflow that receives
BioResultReceived: bio-result as well as ours, whose filter keeps this
creatinine result. Each gets its own run, with the id
<eventId>:<workflow file name without .workflow.yaml>.
To watch runs, filter them, and inspect their timelines in general, see Monitor executions. For why these runs are durable and resumable, see Why durable execution.
What we did
We built a workflow entirely in the editor — a trigger and a compute node, connected by type-checked wires — then simulated it, saved it as YAML, and ran it for real. Every step gave us a visible result.
Where to go next
- Understand which workflows start on an event, and restrict yours: Choose which events start a workflow and Events and triggers.
- Persist the result or notify a clinician by adding more nodes — start from Wire ports and the core primitives reference.
- Add a decision point: Branch with guards and switch.
- Pause a run for a human decision: Add human validation.
- Let an assistant draft a workflow from a plain-language description: Use the AI assistant.
- Learn the editor's affordances in depth: Use the visual editor.
- Unsure what a node or term means? Check the glossary.