Simulate a workflow
Simulation runs the current graph — even unsaved — against a forged event, through the same interpreter as production, but with no durability and no side effects. Use it to check a workflow's logic before you save or run it.
In the editor
Press ▶ Simulate in the workflow editor. Choose the event and the scenario, then read the result overlaid on the graph.
The event is prefilled from the trigger: its type, the example payload (the declared
event's example for a trigger that references one), and a placeholder identifier
<kind>-exemple for each context kind the example does not carry: the required kinds of a
declared event, otherwise every kind the trigger establishes.
The Subject field names a real subject: its kind, among the kinds the trigger establishes
(every registered kind for a trigger whose context comes with the call), then its
identifier. That subject's memory seeds the simulated memory, and its live record seeds the
record port of its kind when the kind has one. The field is absent when the trigger
establishes no kind. A kind that no loaded plugin registers is refused
(options.subject : sorte de contexte « … » inconnue); a record that cannot be read
answers 502.
When the subject changes, the event's identifier field of that kind follows it, provided
the event is valid JSON and the field is absent, still holds the value the prefill wrote
(the <kind>-exemple placeholder or the value of the event's example), or holds the
previous subject's identifier. A value you typed in the event stays. Clearing the subject
puts the prefilled value back.
The simulation runs this workflow only, and it does not evaluate the trigger filter: the graph runs on the event whatever the filter says. Test the filter in the trigger inspector's Filter section instead, which evaluates it on the example event (Kept by this filter. or Not kept by this filter.). The other workflows that start on the same event are listed there too ({event} events also start:); see Choose which events start a workflow.
What the sandboxes do (each port's sandbox is contributed by the plugin that owns the port):
- Seeded reads — each port reads from its seed: an inline snapshot you
supply per port, or, for a record port, the live record of a real subject
(via the
recordservice, in-memory or FHIR depending on the instance). - Captured writes —
addObservation/addCondition/addNotification… are listed as "what would have been written" but never applied. The sandbox does read back its own writes, so read-after-write within a run stays consistent. - Sandboxed memory — memory nodes read from a
seed and their writes and forgets are captured as effects (
port: memory). The seed holds the workflow's realworkflow-globalitems and, with a subject filled in, that subject's real memory, read-only, versions included; a conditional write is checked against the seeded version. - Scenario-driven validations — each human-validation node is decided per the chosen scenario (accepted / rejected / timed-out). This is how you exercise the escalation branch without waiting for an SLA.
- Simulated agents — a deterministic example output of the declared type, with no LLM call, unless you tick "real agents".
The result appears as an overlay on the graph (✓ ran / ⤼ skipped, with the reason and the inputs/outputs on hover). The side panel lists the captured writes, the validations presented, and the trace.
When the trigger claims a declared event type, the forged event is checked against that
type's contract (gate EVENT_CONTRACTS), whatever the event's type: under enforce a
non-conforming event is refused, under observe the simulation runs; in both cases the
panel lists the issues. The rules are those of the
ingestion check.
Via the API
POST /api/simulate
{
"spec": { … } , // or "file": "my-workflow.workflow.yaml"
"workflow": "my-workflow.workflow.yaml", // optional: identity of an inline spec,
// addresses its memory of scopes workflow
// and workflow-global
"event": { … },
"options": {
"seeds": { // one opaque seed per port name (shape defined by
"patient": { … }, // the owning plugin's sandbox — e.g. a partial
"prescription": [ … ] // PatientProfile, a prescription list…)
},
"subject": { "kind": "patient", "id": "…" }, // a REAL subject of a registered kind:
// its memory seeds the simulated memory,
// its record the record port of its kind
// (when the kind has one)
"memory": [ // memory seed, wins over the real memory
{ "scope": "workflow", "workflow": "my-workflow", "subject": { "kind": "patient", "id": "…" },
"key": "…", "value": …, "type": "Decimal", "version": 12 }, // type and version optional
{ "scope": "workflow-global", "key": "…", "value": … } // no subject for this scope
],
"validations": { "node-id": "accepted" | "rejected" | "timeout" },
"agent": "mock" | "real"
}
}
POST /api/simulate adds an eventId (sim-<id>) when the event has none and an
occurredAt when it has none or a blank one, and nothing else: no placeholder
identifier and no filter evaluation. Include the identifiers of the event's required
context kinds (patientId, encounterId…) yourself; the editor adds them for you.
The response is a report: { status, nodes, effects, validations, logs, durationMs, contract },
where nodes are the per-node observations, effects the captured writes, logs the
engine's structured trace, and contract the event's contract issues ({ issues }, or
null). Under EVENT_CONTRACTS=enforce, a non-conforming event gets
400 { error, issues } instead. A port whose plugin contributes no sandbox is absent in
simulation: a node that needs it fails with Port « … » non branché, visible in the
report.
Next
- Run a workflow and resolve validations once the simulation looks right.
- To understand what "skipped" means, see Branching and absence propagation.
Multiple entries
For a workflow with several entries, choose the entry in the simulation panel. Its filter result is reported separately; the selected branch runs even when the filter is false. The other branches stay inactive. See Start a workflow from several events.