Skip to Content
Health Ai PlatformWorkflowsWorkflow DSL Guide

Workflow DSL Guide

The Health AI Platform uses a declarative workflow DSL implemented by @loop/workflow-engine.

Instead of writing orchestration logic directly in application code, you define:

  • workflow identity
  • optional triggers
  • ordered steps
  • step conditions
  • action parameters

That definition can be loaded from YAML, JSON, or a JavaScript object.

DSL shape

Every workflow definition follows the same top-level structure:

id: thyroid-assessment-v1 name: Thyroid Assessment version: 1.0.0 triggers: - type: biomarker_result params: biomarker: TSH steps: - id: check-tsh type: check_biomarker action: type: check_value params: biomarker: TSH threshold: 2.5 operator: ">" metadata: author: loop-health

Top-level fields

FieldTypeRequiredDescription
idstringYesStable workflow identifier
namestringYesHuman-readable name
versionstringYesSemver-style version
triggersarrayNoEvent types that should start the workflow
stepsarrayYesOrdered workflow steps
metadataobjectNoArbitrary metadata for tooling and docs

Trigger types

The current type definitions support four trigger types:

type TriggerType = | 'biomarker_result' | 'schedule' | 'manual' | 'event';

Example: manual workflow

id: provider-review-v1 name: Provider Review version: 1.0.0 triggers: - type: manual steps: - id: notify-provider type: send_notification action: type: notify params: channel: email message: "A provider review has been requested."

Step types

These are the currently supported step types in packages/workflow-engine/src/types.ts:

Step typePurposeTypical params
check_biomarkerCompare a biomarker against a thresholdbiomarker, threshold, operator
recommend_supplementAdd a recommendation to workflow outputsupplement, dosage
order_labRecord a lab order actiontest or labTest, priority
check_contraindicationCompare a medication against the patient’s medicationsmedication, supplement
send_notificationProduce a notification actionchannel, message
run_toolAttempt a tool execution through the workflow runtimetoolId, plus tool params

Step anatomy

Each step has an id, type, and optional condition, action, and next.

- id: recommend-selenium type: recommend_supplement condition: "check-tsh.result === true" action: type: recommend params: supplement: Selenium dosage: 200mcg next: notify-patient

Conditions

Conditions are evaluated with a safe expression parser.

Supported capabilities include:

  • numeric comparisons
  • equality checks
  • && and ||
  • dot-path access into patient data
  • access to prior step results

Condition examples

condition: "biomarker.TSH > 2.5" condition: "age >= 40 && gender === 'female'" condition: "check-tsh.result === true" condition: "genetics.MTHFR === 'C677T/C677T'"

What condition context includes

At execution time, the evaluator merges:

  • context.data
  • prior step results as stepId.result
  • prior step success values as stepId.success
const result = await workflow.execute({ patientId: 'patient-42', data: { biomarker: { TSH: 4.2 }, age: 43, gender: 'female', genetics: { MTHFR: 'C677T/C677T' }, }, });

Actions

The engine dispatches each step by step.type. The action.type is descriptive and helps keep definitions understandable, but the execution behavior currently comes from the step type handlers.

Example:

- id: notify-patient type: send_notification action: type: notify params: channel: email message: "Your thyroid review is ready."

Flow control with next

By default, steps execute sequentially. If you set next, the engine will jump to the named step.

steps: - id: check type: check_biomarker next: notify - id: order type: order_lab - id: notify type: send_notification

Use this for branching flows and skip behavior.

Loading workflows

Load from YAML

import { WorkflowEngine } from '@loop/workflow-engine'; const workflow = WorkflowEngine.load(` id: tsh-review-v1 name: TSH Review version: 1.0.0 steps: - id: check-tsh type: check_biomarker action: type: check_value params: biomarker: TSH threshold: 2.5 operator: ">" `);

Load from an object

const workflow = WorkflowEngine.load({ id: 'manual-review-v1', name: 'Manual Review', version: '1.0.0', steps: [ { id: 'notify', type: 'send_notification', action: { type: 'notify', params: { channel: 'email', message: 'A review is ready.', }, }, }, ], });

Executing workflows

const result = await workflow.execute({ patientId: 'patient-42', data: { biomarker: { TSH: 4.2 }, medications: ['metformin'], }, tools: ['risk-calculator'], }); console.log(result.executedSteps); console.log(result.stepResults); console.log(result.recommendations);

Validation rules

The engine validates definitions when you load them.

Common validation failures include:

  • missing id
  • missing name
  • missing version
  • empty steps
  • invalid step type
  • duplicate step IDs
  • next pointing to an unknown step
WorkflowEngine.load({ id: 'bad-workflow', name: 'Bad Workflow', version: '1.0.0', steps: [], }); // throws because at least one step is required

Current run_tool behavior

The run_tool step type is part of the DSL, but its current executor behavior is deliberately minimal:

  • it checks whether the requested toolId is allowed in context.tools
  • it returns a stub execution result
  • it does not yet delegate to a shared @loop/tool-registry package
- id: calculate-risk type: run_tool action: type: tool params: toolId: risk-calculator scoreType: metabolic

That means you should document and design with two layers in mind:

  1. DSL compatibility now
  2. future runtime delegation later

Best practices

  • Keep step IDs short and stable.
  • Store clinical facts in context.data, not in conditions.
  • Use conditions for branching, not for complex computation.
  • Version workflows whenever you change clinical logic.
  • Treat run_tool as a controlled extension point, not a generic escape hatch.

Next steps