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-healthTop-level fields
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Stable workflow identifier |
name | string | Yes | Human-readable name |
version | string | Yes | Semver-style version |
triggers | array | No | Event types that should start the workflow |
steps | array | Yes | Ordered workflow steps |
metadata | object | No | Arbitrary 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 type | Purpose | Typical params |
|---|---|---|
check_biomarker | Compare a biomarker against a threshold | biomarker, threshold, operator |
recommend_supplement | Add a recommendation to workflow output | supplement, dosage |
order_lab | Record a lab order action | test or labTest, priority |
check_contraindication | Compare a medication against the patient’s medications | medication, supplement |
send_notification | Produce a notification action | channel, message |
run_tool | Attempt a tool execution through the workflow runtime | toolId, 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-patientConditions
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_notificationUse 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
nextpointing to an unknown step
WorkflowEngine.load({
id: 'bad-workflow',
name: 'Bad Workflow',
version: '1.0.0',
steps: [],
});
// throws because at least one step is requiredCurrent 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
toolIdis allowed incontext.tools - it returns a stub execution result
- it does not yet delegate to a shared
@loop/tool-registrypackage
- id: calculate-risk
type: run_tool
action:
type: tool
params:
toolId: risk-calculator
scoreType: metabolicThat means you should document and design with two layers in mind:
- DSL compatibility now
- 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_toolas a controlled extension point, not a generic escape hatch.
Next steps
- Read Workflow Examples
- Follow Creating Workflows
- Review the DSL API Reference