Documentation / Conception / Guides pratiques / Run a workflow and resolve validations

Run a workflow and resolve validations

This recipe drives one real run end to end against a running instance: emit the event a workflow listens for, follow it to a human validation, and resolve that validation — from the command line and from the console. It assumes the workflow already exists and the instance is up; see Stand up your first instance if it isn't yet, and Your first workflow if you haven't authored one before.

Prerequisites

  • A running instance with the REST API reachable — http://localhost:4000 for a local pnpm run dev/docker compose up -d, or whatever Topology maps to in your deployment.
  • A loaded workflow whose trigger.type you know, and the eventType it claims (GET /api/workflows lists both, plus a ready-to-send example payload per workflow).
  • At least one human-task node on the path you're about to exercise — see Add human validation if the workflow doesn't have one yet.

Trigger a run

A run starts when an event matching a registered trigger's eventType is ingested — the event model is open, so nothing is rejected unless no trigger claims that type (apps/api/src/server/api.ts, ingestEvent).

From the command line

curl -X POST localhost:4000/api/events -H 'content-type: application/json' \
  -d '{"type":"BioResultReceived","patientId":"patient-42","encounterId":"sejour-7","analyte":"creatinine","value":160,"unit":"µmol/L"}'

The response is the run's view (201 for a new run, see below). A few things the ingestion path does for you, all in ingestEvent:

  • eventId and occurredAt are optional — if you omit them, the server generates an id (evt-xxxxxxxx) and stamps the current time.
  • Ingestion is idempotent by eventId — POSTing the same eventId twice doesn't start a second run: you get back the existing run's view with 200 instead of 201 (created: false).
  • An unclaimed type is rejected with 400Aucun déclencheur ne revendique l'événement « <type> ». (or Événement sans type. if type is missing/empty). Check GET /api/workflows for the eventType each loaded workflow's trigger actually claims.
  • To target a specific workflow file instead of relying on eventType matching, add "__workflowFile": "<file>.workflow.yaml" to the body — it's stripped before the event is stored.
  • Pinning is also how you run a stored draft (a spec saved but never published — bench fixtures seeded from workflows/drafts/, work in progress), but the fallback is opt-in: add "__allowDraft": true next to "__workflowFile" (the console's composer sets it for you). With the flag, a pinned file that matches no active version falls back to the slug's draft, after running the full publication-gate validation — an invalid draft is rejected with 400 and the list of errors, never started. The run is indexed with specVersion: null and the draft's specHash: history shows exactly what code ran, without inventing a published version number. Without the flag a missed pin stays a 400 — that is what lets machine callers (the scheduler's orphan-schedule self-healing) keep trusting "workflow introuvable". Automatic eventType routing never sees drafts either way.

Then find the run:

curl localhost:4000/api/runs                       # most recent first: id, workflowName, subjects, status, createdAt
curl localhost:4000/api/runs/<id>                   # full detail: status, pending, nodeStatus, trace, observations, output

From the console

  1. Open the config-plane at http://localhost:3000 and go to Executions.
  2. Click + Emit an event (or Close if the composer is already open).
  3. Under Target workflow, pick the workflow. The Event payload (JSON) box fills in with that workflow's example payload.
  4. Edit the payload if needed, then ▶ Send the event. The composer closes and the new run appears at the top of the Runs list, selected.

(apps/config-plane/app/executions/ExecutionsView.tsx.)

Follow the run to its pause

A run's status is one of running, suspended, completed, failed. Poll GET /api/runs/<id> (or watch the console, which refreshes every couple of seconds while a run is running/suspended) until status turns suspended — that's a human-task node blocking on awaitValidation. The run's pending field is then non-null:

{
  "id": "confirm_condition:evt-a1b2c3d4",
  "kind": "confirm_condition",
  "title": "Add the condition \"Chronic kidney disease\"?",
  "payload": { "code": "N18", "display": "Chronic kidney disease", "note": "eGFR 34 mL/min" }
}

(this particular shape comes from human.confirm-condition's propose in external-plugins/clinical/behaviors/human.ts — another node's title/payload will differ, but the envelope is the same.)

pending.id is the durable promise's key (<nodeId>:<eventId> — see libs/engine-core/src/engine/interpreter.ts); you don't need to construct or pass it yourself, the resolve endpoint reads it off the run. For the full timeline view (trace, per-node observations, skip reasons), see Monitor executions.

Resolve the pending validation

From the command line

curl -X POST localhost:4000/api/runs/<id>/resolve -H 'content-type: application/json' \
  -d '{"decision":"accepted","by":"Dr Martin"}'

Body fields (apps/api/src/server/api.ts, opResolve):

  • decision — only the exact string "rejected" resolves as rejected; anything else (including a typo, or omitting the field) resolves as "accepted". Send one of the two literal values.
  • by — free text recorded with the decision (shown in the trace and, where the workflow forwards it, in persisted content). Defaults to "Utilisateur" if omitted — pass a real value for an auditable trail.
  • comment — optional free text, also recorded.

The response is the run's refreshed view — pending is now null and the run resumes past the validation (to completion, or to its next node/pause). Failure modes:

  • 404{"error":"run inconnu"}: no run with that id.
  • 409{"error":"aucune validation en attente"}: the run has no pending validation right now (already resolved, not yet suspended, or timed out — see below).

From the console

On a suspended run, the detail panel shows Human validation required, the pending payload, and ✓ Accept / ✗ Reject buttons. With the run selected and no composer open, the keyboard shortcuts A (accept) and R (reject) do the same thing. Both send by as the current UI locale's "resolved by" label — pass a real reviewer name via the API if you need one recorded.

Handle a deadline that expires before you decide

A human-task node can carry config.deadlineSeconds; past that many seconds without a decision, the durable timer wins the race and the run takes its escalation branch instead — resolve no longer has anything to act on (409, aucune validation en attente). This is a real gate from content/clinical-demo/workflows/critical-result-sla.workflow.yaml:

nodes:
  acknowledge: { type: human.acknowledge-critical, config: { deadlineSeconds: 20 } }

Check pending (is there still one to resolve?) and trace (look for a validation-timed-out entry) before retrying a resolve call on a run you suspect may have escalated.

Verify the outcome

curl localhost:4000/api/runs/<id>
  • statuscompleted (or failed if a later step errored) once every reachable node has run.
  • trace — a validation-resolved entry (with your decision/by) or a validation-timed-out one, followed by whatever ran next.
  • nodeStatus / observationsdone for the validation node and every node downstream that received data; skipped for the branch absence propagation cut (e.g. the escalation branch on a normal accept).
  • output — the run's final per-node outputs, once completed.
75 documents11 sectionssource : /docs · généré au build