Documentation / Conception / Guides pratiques / Add human validation and SLAs

Add human validation and SLAs

A human-task node suspends a run until a person decides, then resumes on whichever output port that decision feeds. Give it a deadlineSeconds config and the wait races a durable timer: past the deadline the run resumes on an escalation port instead, with no human input needed. This recipe assumes you can already wire nodes port-to-port — see Your first workflow and Wire ports if not — and covers using an existing human-task node from your plugins' catalog. To write a new one, see Implement a node behavior.

1. Wire the node like any other

A human-task node is declared and wired exactly like a compute or sink node — nothing in the YAML marks it as special:

nodes:
  detect_condition: { type: clinical.condition.detect-from-egfr }
  confirm_condition: { type: human.confirm-condition }
  record_condition: { type: patient.append-condition }
connections:
  - { from: detect_condition.proposal, to: confirm_condition.proposal }
  - { from: confirm_condition.condition, to: record_condition.condition }

(Real example: content/clinical-demo/workflows/bio-result.workflow.yaml.) human.confirm-condition outputs both decision (the raw verdict) and condition (present only if accepted) — wire whichever ports your downstream nodes need; an unwired output is simply never consumed.

Two things to check before this validates:

  • Ambient context. A human-task node can require context keys (human.confirm-condition requires patient) exactly like any other node — they must already be in the workflow's context: list, or static validation rejects the workflow with a missing-context error.
  • What the human sees. The title and payload shown to the validator come from the node's propose behavior, not from the workflow YAML — you pick which human-task node to use, a plugin developer defines what it asks. See Implement a node behavior §7.

2. Add an SLA deadline

Some human-task nodes declare a deadlineSeconds config field for this purpose — check the node's config: entry in its plugin.yaml (or the generated nodes.ts) to see if it accepts one; not every human-task node does (human.confirm-condition has no deadline at all). Where it's declared, set it under the node's config: like any other config field:

acknowledge:
  type: human.acknowledge-critical
  config: { deadlineSeconds: 900 } # 15 minutes, then escalate

deadlineSeconds is a plain number of seconds. Under the hood the wait is a durable timer — it survives process restarts and replays deterministically, it isn't a setTimeout that resets on crash. See Why durable execution for that guarantee. Leave the config out (or the node has no such field) and the run waits for a decision indefinitely — there is no default timeout.

3. Route the escalation branch separately

A human-task node with a deadline exposes (at least) two outputs: one fed when a human decides in time, one fed when the deadline lapses first. Only one of the two is ever populated per run — absence propagation cuts the other branch, so no guard node is needed to keep them mutually exclusive. Wire each to its own downstream consumer:

# content/clinical-demo/workflows/critical-result-sla.workflow.yaml
nodes:
  acknowledge: { type: human.acknowledge-critical, config: { deadlineSeconds: 900 } }
  record_ack: { type: notify.ack-recorded }
  escalate: { type: notify.escalate-on-call }
connections:
  - { from: trigger.result, to: acknowledge.result }
  - { from: acknowledge.acknowledged, to: record_ack.decision } # in time
  - { from: acknowledge.escalation, to: escalate.result } # deadline expired

acknowledged and escalation are typed like any other ports (ValidationDecision and Observation here) and connection compatibility is checked the same way. For the general model behind "only one branch fires," see Branching and absence.

4. Check it statically

Nothing about a human-task node is exempt from the usual static pass — required context, existing ports, compatible connection types on both the timely and the escalation branch. Run it before you rely on either branch:

pnpm plugin validate content/clinical-demo/workflows/critical-result-sla.workflow.yaml \
  --plugin external-plugins/clinical --plugin external-plugins/demo

See Validate a workflow. The value a human produces (decision, acknowledged, …) is itself checked again at runtime against its declared port type, the same as any computed value — see Two-level validation for how the static and runtime passes split the work.

5. Test both branches without waiting on the clock

POST /api/simulate (and the editor's ▶ Simulate) never suspends: each human-task node is decided according to a per-node scenario instead of a real wait, so you can force the escalation branch without sitting through the deadline. Pass the decision as "accepted", "rejected", or "timeout", keyed by node id:

curl -X POST localhost:4000/api/simulate -H 'content-type: application/json' -d '{
  "file": "critical-result-sla.workflow.yaml",
  "event": {"type":"CriticalResultReceived","encounterId":"sejour-7","analyte":"potassium","value":6.8,"unit":"mmol/L"},
  "options": {"subject":{"kind":"patient","id":"patient-42"},"validations":{"acknowledge":"timeout"}}
}'

The report's validations array lists every proposal a human would have seen (title, payload, deadline) plus the forced decision, and effects shows what escalate would have written — nothing is actually applied. "timeout" is a simulation-only scenario value; it is not a decision you can ever POST to a real run (see next step). Details in Simulate a workflow.

6. Resolve real runs, or let the deadline fire

A real run suspends at the human-task node and shows up with status: "suspended" and a pending request when polled (GET /api/runs/<id>) — see Monitor executions. A human resolves it explicitly:

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

decision is normalized server-side: anything other than the literal string "rejected" counts as acceptance. Resolving a run with nothing pending (already decided, or already timed out) returns 409. If a deadline is configured and nobody resolves it in time, the timer fires on its own and the run proceeds on the escalation branch — no API call needed. Full mechanics (idempotency, polling) in Run and resolve.

Rather than a manual call, an instance can also resolve validations automatically from an external signal — e.g. a FHIR Task flipped to completed in the EHR — by declaring an event binding with resolve: instead of event:. That's an operator/plugin concern, not a workflow-authoring one: see Connect an event source and Contribute an event source and binding.

75 documents6 sectionssource : /docs · généré au build