Creating Workflows
This guide shows the smallest practical authoring loop for a workflow in the Loop platform:
- Define the workflow in YAML or JSON
- Load it through
WorkflowEngine - Execute it with patient context
- Assert on recommendations and step results
Start with a narrow use case
A good first workflow has:
- One trigger
- Two or three steps
- One decision
- One clear output
Example: detect elevated TSH and recommend a follow-up action.
id: tsh-review-v1
name: TSH Review
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: ">"
- id: recommend-selenium
type: recommend_supplement
condition: "check-tsh.result === true"
action:
type: recommend
params:
supplement: Selenium
dosage: 200mcgLoad and register the workflow
WorkflowEngine.load() both validates and registers the definition.
import { WorkflowEngine } from '@loop/workflow-engine';
const definition = `
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: ">"
`;
const workflow = WorkflowEngine.load(definition);Execute with real context
In practice, context usually starts from the Patient Graph and is then normalized into the shape expected by the workflow.
import { PatientGraphClientImpl } from '@loop/patient-graph-client';
import { WorkflowEngine } from '@loop/workflow-engine';
const patientGraph = new PatientGraphClientImpl({
baseUrl: process.env.PATIENT_GRAPH_API_URL!,
getAuthToken: async () => process.env.PATIENT_GRAPH_API_KEY!,
});
const profileResult = await patientGraph.getPatientContext('user_123');
if (!profileResult.ok) {
throw new Error(profileResult.error.message);
}
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: ">"
- id: notify
type: send_notification
condition: "check-tsh.result === true"
action:
type: notify
params:
channel: email
message: "Your TSH result may need review."
`);
const result = await workflow.execute({
patientId: profileResult.data.externalId,
data: {
biomarker: { TSH: 4.2 },
medications: ['metformin'],
},
});Understand the result shape
The runtime returns a WorkflowResult with execution details.
console.log(result);
/*
{
success: true,
workflowId: 'tsh-review-v1',
executedSteps: ['check-tsh', 'notify'],
stepResults: {
'check-tsh': {
stepId: 'check-tsh',
success: true,
result: {
checked: true,
biomarker: 'TSH',
value: 4.2,
threshold: 2.5,
operator: '>',
result: true
}
},
notify: {
stepId: 'notify',
success: true,
result: {
sent: true,
patientId: 'user_123',
channel: 'email',
message: 'Your TSH result may need review.'
}
}
},
recommendations: [],
errors: []
}
*/Use explicit naming
Prefer:
- Stable workflow IDs such as
thyroid-review-v1 - Descriptive step IDs such as
check-tshandorder-thyroid-panel - Versioned definitions instead of mutating the meaning of an old workflow
Keep context shallow and predictable
The executor reads values from context.data. Use a shape that is easy to inspect and easy to test.
const context = {
patientId: 'user_123',
data: {
biomarker: {
TSH: 4.2,
'testosterone-total': 650,
},
medications: ['metformin', 'warfarin'],
age: 38,
genetics: {
MTHFR: 'C677T/C677T',
},
},
};Account for current platform boundaries
Two boundaries matter when authoring:
run_tool is an extension point
The workflow engine validates run_tool steps and checks tool availability from context.tools, but the actual handler is still a stub.
- id: call-risk-tool
type: run_tool
action:
type: tool
params:
toolId: risk-calculatorconst result = await workflow.execute({
patientId: 'user_123',
tools: ['risk-calculator'],
});Today this returns an execution placeholder rather than delegating into a shared @loop/tool-registry package.
Triggers are definitions, not schedulers
The triggers block describes when a workflow should run, but another system still has to decide when to execute it.
Recommended authoring checklist
- Validate required fields:
id,name,version,steps - Keep each step independently understandable
- Use
conditioninstead of duplicating whole workflows - Keep side effects obvious
- Test both positive and negative branches
- Document the patient data shape next to the workflow
Example authoring loop
# 1. Update workflow definition in your code or fixture
# 2. Run tests for the workflow engine
pnpm --filter @loop/workflow-engine test
# 3. Run broader checks if the workflow integrates with an app
pnpm typecheckNext steps
- Continue to Testing Workflows
- Browse Workflow Examples
- Keep the field-level reference handy in DSL Reference