Energy & Industrial
ADL v0.3 for Energy & Industrial
Asset-integrity, inspection, management-of-change, and work-order ADL domains for energy and industrial operations, built to the ADL v0.3 specification.
Industrial operations carry their risk in state transitions: a pressure vessel inspected past its due date, a temporary management-of-change that quietly became permanent, a work order started without a valid permit. This paper works the core energy and industrial domains in the Application Definition Language (ADL) as complete, runnable definitions: asset integrity and inspection, management of change, and an edge/IoT gateway domain.
The emphasis throughout is on making safety-critical rules blocking rather than advisory, and on making time-based conditions (an inspection coming due, a temporary change expiring) fire on their own through scheduled triggers instead of waiting for someone to notice. Each domain below shows how those guarantees are declared in ADL and enforced by the Kalos runtime.
Domain 1: Asset Integrity (assets)
Asset, Inspection, and Workorder share one domain: the workflows that close the inspection→asset loop and the cross-resource guards require co-residence.
domain:
name: assets
version: "1.0.0"
description: >
Asset register, inspection management, and work orders. Inspection results
flow back to the asset via workflow — corrosion rate and wall thickness on
the asset are maintained by the system, not by manual transcription.
Safety-critical checks (permits, LOTO) BLOCK work, they do not warn.
channels:
email:
provider: smtp
host: ${SMTP_HOST}
port: ${SMTP_PORT:587}
from: "integrity-noreply@example.com"
objects:
Timestamp:
createdAt: { type: datetime, autoNow: create }
updatedAt: { type: datetime, autoNow: update }
enums:
AssetStatus:
values:
commissioning: { label: "Commissioning", color: "#3b82f6" }
operational: { label: "Operational", color: "#10b981" }
degraded: { label: "Degraded", color: "#f59e0b" }
out_of_service: { label: "Out of Service", color: "#ef4444" }
maintenance: { label: "Under Maintenance", color: "#8b5cf6" }
decommissioned: { label: "Decommissioned", color: "#64748b", final: true }
AssetCriticality:
values:
critical: { label: "Critical", description: "Failure causes safety hazard or system shutdown", color: "#ef4444" }
high: { label: "High", description: "Failure causes significant production loss", color: "#f97316" }
medium: { label: "Medium", description: "Failure causes moderate operational impact", color: "#f59e0b" }
low: { label: "Low", description: "Failure causes minimal impact", color: "#94a3b8" }
InspectionType:
values:
api_510: { label: "API 510 - Pressure Vessel" }
api_570: { label: "API 570 - Piping" }
api_653: { label: "API 653 - Tank" }
visual: { label: "Visual" }
ut_thickness: { label: "UT Thickness" }
vibration: { label: "Vibration Analysis" }
thermographic: { label: "Thermographic" }
emergency: { label: "Emergency" }
InspectionStatus:
values:
scheduled: { label: "Scheduled", color: "#94a3b8" }
in_progress: { label: "In Progress", color: "#3b82f6" }
completed: { label: "Completed", color: "#8b5cf6" }
reviewed: { label: "Reviewed", color: "#06b6d4" }
approved: { label: "Approved", color: "#10b981", final: true }
cancelled: { label: "Cancelled", color: "#64748b", final: true }
FindingSeverity:
values:
none: { label: "No Findings", color: "#10b981" }
minor: { label: "Minor", color: "#94a3b8" }
moderate: { label: "Moderate", color: "#f59e0b" }
major: { label: "Major", color: "#f97316" }
critical: { label: "Critical", color: "#ef4444" }
immediate_action: { label: "Immediate Action", color: "#7f1d1d" }
FitnessResult:
values:
fit: { label: "Fit for Service" }
fit_with_monitoring: { label: "Fit with Enhanced Monitoring" }
repair_required: { label: "Repair Required" }
replace: { label: "Replace" }
unfit: { label: "Not Fit for Service" }
WorkType:
values: [preventive, corrective, predictive, emergency, modification]
WorkPriority:
values:
low: { label: "Low", color: "#94a3b8" }
normal: { label: "Normal", color: "#3b82f6" }
high: { label: "High", color: "#f59e0b" }
emergency: { label: "Emergency", color: "#ef4444" }
WorkorderStatus:
values:
requested: { label: "Requested", color: "#94a3b8" }
planned: { label: "Planned", color: "#3b82f6" }
approved: { label: "Approved", color: "#6366f1" }
in_progress: { label: "In Progress", color: "#f59e0b" }
on_hold: { label: "On Hold", color: "#8b5cf6" }
completed: { label: "Completed", color: "#10b981" }
closed: { label: "Closed", color: "#059669", final: true }
rejected: { label: "Rejected", color: "#ef4444", final: true }
# ── Time-based obligations: schedules, not fieldChange events ───────────────
schedules:
inspectionDue:
description: "Warn 30 days before an asset's inspection due date; escalate when overdue"
type: relative
source:
resource: Asset
filter: "(status == 'operational' || status == 'degraded') && nextInspectionDue != null"
relativeTo: nextInspectionDue
tiers:
- daysUntil: 30
action: notify
tag: 30day
channels: [email]
recipients: { roles: [maintenancePlanner, inspector] }
template:
subject: "Inspection due in 30 days — {assetTag}"
body: "{name} at {facility}. Due: {nextInspectionDue}. Criticality: {criticality}."
- daysUntil: 0
action: notify
tag: overdue
channels: [email]
recipients: { roles: [integrityEngineer, operationsManager] }
template:
subject: "INSPECTION OVERDUE — {assetTag}"
body: "{name} at {facility} was due {nextInspectionDue}. Criticality: {criticality}."
endOfLife:
description: "End-of-design-life planning horizon (2 years out)"
type: relative
source:
resource: Asset
filter: "status != 'decommissioned' && expectedEndOfLife != null"
relativeTo: expectedEndOfLife
tiers:
- daysUntil: 730
action: notify
tag: 2year
channels: [email]
recipients: { roles: [integrityEngineer, operationsManager] }
template:
subject: "Asset {assetTag} reaches design end-of-life in 2 years"
body: "{name}. Expected end of life: {expectedEndOfLife}. Begin replacement/life-extension planning."
# ── Inspection results maintain the asset record ────────────────────────────
workflows:
prefillInspectionBaseline:
description: >
On inspection creation, copy the asset's current condition data onto the
inspection as the comparison baseline — the manual transcription step
the white paper claimed to eliminate, actually eliminated.
trigger:
resource: Inspection
event: create
steps:
- name: copyBaseline
action: update
resource: Inspection
target: $trigger.record.id
input:
previousThickness: "findOne(Asset, id == $trigger.record.assetId).wallThickness"
previousInspectionDate: "findOne(Asset, id == $trigger.record.assetId).lastInspectionDate"
minRequiredThickness: "findOne(Asset, id == $trigger.record.assetId).minWallThickness"
then: end
applyInspectionResults:
description: >
On inspection approval, write the measured condition back to the asset.
The asset's corrosion rate, wall thickness, and inspection clock are
maintained by the system from approved inspections.
trigger:
resource: Inspection
event: transition
transition: approve
steps:
- name: updateAsset
action: update
resource: Asset
target: $trigger.record.assetId
input:
wallThickness: $trigger.record.measuredThickness
corrosionRate: $trigger.record.calculatedCorrosionRate
lastInspectionDate: $trigger.record.performedDate
nextInspectionDue: $trigger.record.nextInspectionDate
then: end
onError:
action: transition
target: $trigger.record.id
transition: requestReinspection
resources:
# ─────────────────────────────────────────────────────────────────────────
Asset:
object:
id: { type: uuid, primaryKey: true }
assetTag: { type: string, required: true, unique: true, immutable: true, maxLength: 40 }
name: { type: string, required: true, maxLength: 200 }
assetType: { type: string, required: true, maxLength: 80 }
location: { type: string, required: true, maxLength: 200 }
gpsLatitude: { type: float, min: -90, max: 90 }
gpsLongitude: { type: float, min: -180, max: 180 }
facility: { type: string, required: true, maxLength: 120 }
unit: { type: string, maxLength: 60 }
status: { $ref: '#/enums/AssetStatus', default: commissioning }
criticality: { $ref: '#/enums/AssetCriticality' }
manufacturer: { type: string, maxLength: 120 }
modelNumber: { type: string, maxLength: 80 }
serialNumber: { type: string, maxLength: 80 }
installDate: { type: date }
commissionDate: { type: date }
designLifeYears: { type: integer, min: 1 }
lastInspectionDate: { type: date }
nextInspectionDue: { type: date }
operatingPressure: { type: float }
operatingTemp: { type: float }
designPressure: { type: float }
designTemp: { type: float }
wallThickness: { type: float, min: 0 }
minWallThickness: { type: float, min: 0 }
corrosionRate: { type: float, min: 0 }
parentAssetId: { type: uuid }
notes: { type: text }
$merge: { $ref: '#/objects/Timestamp' }
relationships:
parent: { type: belongsTo, target: Asset, foreignKey: parentAssetId, onDelete: restrict }
children: { type: hasMany, target: Asset, foreignKey: parentAssetId }
inspections: { type: hasMany, target: Inspection, foreignKey: assetId }
workorders: { type: hasMany, target: Workorder, foreignKey: assetId }
indexes:
- { fields: [status] }
- { fields: [facility] }
- { fields: [criticality] }
- { fields: [parentAssetId] }
- { fields: [nextInspectionDue] }
computed:
expectedEndOfLife:
type: date
formula: "dateAdd(installDate, designLifeYears, 'years')"
stored: true
updateOn: [installDate, designLifeYears]
# A stored field computed from now() is stale the moment it's written —
# time-based action lives in the endOfLife schedule; this field answers
# the API question only.
remainingWallLifeYears:
type: float
formula: "if(corrosionRate > 0 && wallThickness != null && minWallThickness != null, (wallThickness - minWallThickness) / corrosionRate, null)"
stored: true
updateOn: [wallThickness, minWallThickness, corrosionRate]
stateMachine:
field: status
initial: commissioning
states:
commissioning:
transitions:
commission:
to: operational
guards: [inspector, engineer, admin]
requiredFields: [commissionDate]
operational:
transitions:
degrade:
to: degraded
guards: [inspector, engineer, admin]
takeOutOfService:
to: out_of_service
guards: [engineer, operationsManager, admin]
startMaintenance:
to: maintenance
guards: [maintenancePlanner, admin]
degraded:
transitions:
restore:
to: operational
guards: [inspector, engineer, admin]
takeOutOfService:
to: out_of_service
guards: [engineer, operationsManager, admin]
startMaintenance:
to: maintenance
guards: [maintenancePlanner, admin]
out_of_service:
transitions:
returnToService:
to: operational
guards: [inspector, engineer, admin]
startMaintenance:
to: maintenance
guards: [maintenancePlanner, admin]
decommission:
to: decommissioned
guards: [operationsManager, admin]
maintenance:
transitions:
completeMaintenance:
to: operational
guards: [maintenanceTech, engineer, admin]
failMaintenance:
to: out_of_service
guards: [maintenanceTech, engineer, admin]
decommissioned:
final: true
validation:
wallAboveMinimum:
rule: "wallThickness == null || minWallThickness == null || wallThickness >= 0"
message: "Wall thickness cannot be negative"
designEnvelope:
rule: "operatingPressure == null || designPressure == null || operatingPressure <= designPressure"
message: "Operating pressure cannot exceed design pressure"
features:
audit: true
search: true
audit:
exclude: [updatedAt]
history:
events:
commissioned:
on: transition
transition: commission
capture: [assetTag, name, assetType, location, facility]
label: "Asset {assetTag} commissioned at {facility}"
detail: "{name} ({assetType}), Location: {location}"
icon: lucide:check-circle
severity: success
degraded:
on: transition
transition: degrade
capture: [assetTag, name]
label: "{assetTag} — condition degraded"
icon: lucide:alert-triangle
severity: warning
outOfService:
on: transition
transition: takeOutOfService
capture: [assetTag, name]
label: "{assetTag} taken out of service"
icon: lucide:power-off
severity: error
maintenanceStarted:
on: transition
transition: startMaintenance
capture: [assetTag, name]
label: "{assetTag} — maintenance started"
icon: lucide:wrench
severity: info
maintenanceComplete:
on: transition
transition: completeMaintenance
capture: [assetTag, name]
label: "{assetTag} — maintenance complete, returned to service"
icon: lucide:check
severity: success
returnedToService:
on: transition
transition: returnToService
capture: [assetTag, name]
label: "{assetTag} returned to service"
icon: lucide:power
severity: success
decommissioned:
on: transition
transition: decommission
capture: [assetTag, name, location]
label: "{assetTag} DECOMMISSIONED"
icon: lucide:archive
severity: info
conditionDataUpdated:
# Fired by the applyInspectionResults workflow write.
on: update
fields: [wallThickness, corrosionRate]
capture: [assetTag, wallThickness, corrosionRate]
label: "{assetTag} condition data updated from approved inspection"
detail: "Wall: {wallThickness}. Corrosion rate: {corrosionRate}"
icon: lucide:ruler
severity: info
wallThicknessCritical:
# Legitimate fieldChange: wallThickness genuinely changes on the
# workflow write-back.
on: fieldChange
field: wallThickness
condition: "wallThickness != null && minWallThickness != null && wallThickness <= minWallThickness * 1.1"
capture: [assetTag, wallThickness, minWallThickness, remainingWallLifeYears]
label: "WALL THICKNESS CRITICAL — {assetTag}"
detail: "Current: {wallThickness}, Minimum: {minWallThickness}. Remaining life: {remainingWallLifeYears} years"
icon: lucide:alert-octagon
severity: error
criticalityChanged:
on: fieldChange
field: criticality
capture: [assetTag, criticality]
label: "{assetTag} criticality changed to {criticality}"
icon: lucide:flag
severity: warning
permissions:
list: [authenticated]
get: [authenticated]
create: [engineer, operationsManager, admin]
update: [inspector, engineer, maintenanceTech, maintenancePlanner, operationsManager, admin]
delete: [admin]
fields:
criticality: { write: [engineer, operationsManager, admin] }
corrosionRate: { write: [inspector, engineer, admin] }
wallThickness: { write: [inspector, engineer, admin] }
minWallThickness: { write: [engineer, admin] }
designPressure: { write: [engineer, admin] }
designTemp: { write: [engineer, admin] }
# ─────────────────────────────────────────────────────────────────────────
Inspection:
mode: sheet
description: >
An inspection record. The baseline (previous thickness/date, minimum
required thickness) is prefilled from the asset by workflow, and the
approved results flow back to the asset by workflow — no transcription.
object:
id: { type: uuid, primaryKey: true }
inspectionNumber: { type: string, required: true, unique: true, immutable: true, maxLength: 40 }
assetId: { type: uuid, required: true, immutable: true }
inspectionType: { $ref: '#/enums/InspectionType', required: true }
inspectionCode: { type: string, maxLength: 40 }
status: { $ref: '#/enums/InspectionStatus', default: scheduled }
scheduledDate: { type: date, required: true }
performedDate: { type: date }
inspectorId: { type: string, maxLength: 100 }
inspectorName: { type: string, maxLength: 120 }
inspectorCertification: { type: string, maxLength: 80 }
conditions: { type: text }
weatherTemp: { type: float }
weatherConditions: { type: string, maxLength: 120 }
finding: { $ref: '#/enums/FindingSeverity' }
findingDescription: { type: text }
recommendation: { type: text }
nextInspectionDate: { type: date }
measuredThickness: { type: float, min: 0 }
previousThickness: { type: float, min: 0 } # prefilled by workflow
previousInspectionDate: { type: date } # prefilled by workflow
minRequiredThickness: { type: float, min: 0 } # prefilled by workflow
fitnessForService: { $ref: '#/enums/FitnessResult' }
completedAt: { type: datetime }
photos: { type: json }
attachments: { type: json }
$merge: { $ref: '#/objects/Timestamp' }
relationships:
asset: { type: belongsTo, target: Asset, foreignKey: assetId, onDelete: restrict }
indexes:
- { fields: [assetId] }
- { fields: [status] }
- { fields: [scheduledDate] }
- { fields: [inspectionType] }
computed:
calculatedCorrosionRate:
type: float
formula: >
if(previousThickness != null && measuredThickness != null
&& previousThickness > measuredThickness
&& previousInspectionDate != null && performedDate != null,
(previousThickness - measuredThickness)
/ max(yearsBetween(previousInspectionDate, performedDate), 0.1),
0)
stored: true
updateOn: [previousThickness, measuredThickness, previousInspectionDate, performedDate]
dynamicFields:
enabled: true
allowTypes: [float, integer, string, text, boolean, date]
maxColumns: 50
permissions:
addColumn: [inspector, engineer, admin]
deleteColumn: [admin]
stateMachine:
field: status
initial: scheduled
states:
scheduled:
transitions:
begin:
to: in_progress
guards: [inspector, admin]
sets:
inspectorId: "$user.id"
inspectorName: "$user.username"
in_progress:
transitions:
complete:
to: completed
guards: [inspector, admin]
requiredFields: [performedDate, measuredThickness]
sets:
completedAt: "now()"
cancel:
to: cancelled
guards: [engineer, operationsManager, admin]
completed:
transitions:
review:
to: reviewed
guards: [engineer, admin]
requiredFields: [finding, fitnessForService]
requestReinspection:
to: scheduled
guards: [engineer, admin]
reviewed:
transitions:
approve:
to: approved
# SoD: the approving integrity engineer is not the performing
# inspector. No admin bypass on the approval itself.
guard: "hasRole('integrityEngineer') && field('inspectorId') != userId()"
requiredFields: [recommendation, nextInspectionDate]
requestReinspection:
to: scheduled
guards: [integrityEngineer, admin]
approved:
final: true
cancelled:
final: true
validation:
performedNotFuture:
rule: "performedDate == null || performedDate <= now()"
message: "Performed date cannot be in the future"
unfitCannotApproveNext:
rule: "status != 'approved' || fitnessForService != 'unfit'"
message: "An asset assessed 'not fit for service' cannot have an approved routine inspection — raise a work order and re-inspect"
features:
audit: true
versioning: true # full measurement snapshots
history:
events:
scheduled:
on: create
capture: [inspectionNumber, inspectionType, scheduledDate]
label: "Inspection {inspectionNumber} scheduled"
detail: "Type: {inspectionType}. Date: {scheduledDate}"
icon: lucide:calendar
severity: info
started:
on: transition
transition: begin
capture: [inspectorName, inspectorCertification]
label: "Inspection started by {inspectorName}"
detail: "Certification: {inspectorCertification}"
icon: lucide:play
severity: info
completed:
on: transition
transition: complete
capture: [inspectorName, performedDate, measuredThickness, conditions]
label: "Inspection completed by {inspectorName}"
detail: "Performed: {performedDate}. Thickness: {measuredThickness}. Conditions: {conditions}"
icon: lucide:check
severity: info
reviewed:
on: transition
transition: review
capture: [finding, fitnessForService, findingDescription]
label: "Reviewed — {finding}, fitness: {fitnessForService}"
detail: "{findingDescription}"
icon: lucide:eye
severity: info
approved:
on: transition
transition: approve
capture: [recommendation, nextInspectionDate]
label: "Inspection approved"
detail: "Recommendation: {recommendation}. Next inspection: {nextInspectionDate}"
icon: lucide:check-circle
severity: success
criticalFinding:
on: fieldChange
field: finding
condition: "finding == 'critical' || finding == 'immediate_action'"
capture: [inspectionNumber, finding, findingDescription]
label: "CRITICAL FINDING — {inspectionNumber}"
detail: "{findingDescription}"
icon: lucide:alert-octagon
severity: error
thicknessBelowMinimum:
# Compares against the required minimum (prefilled from the asset)
# with a 10% approach band.
on: fieldChange
field: measuredThickness
condition: "measuredThickness != null && minRequiredThickness != null && measuredThickness <= minRequiredThickness * 1.1"
capture: [measuredThickness, minRequiredThickness]
label: "THICKNESS AT OR APPROACHING MINIMUM"
detail: "Measured: {measuredThickness}. Required minimum: {minRequiredThickness}"
icon: lucide:alert-octagon
severity: error
highCorrosionRate:
on: fieldChange
field: calculatedCorrosionRate
condition: "calculatedCorrosionRate > 0.5"
capture: [calculatedCorrosionRate, measuredThickness]
label: "HIGH CORROSION RATE — {calculatedCorrosionRate} mm/yr"
icon: lucide:alert-triangle
severity: error
permissions:
list: [authenticated]
get: [authenticated]
create: [inspector, engineer, maintenancePlanner, admin]
update: [inspector, engineer, integrityEngineer, admin]
delete: [admin]
fields:
finding: { write: [engineer, integrityEngineer, admin] }
fitnessForService: { write: [integrityEngineer, admin] }
recommendation: { write: [engineer, integrityEngineer, admin] }
minRequiredThickness: { write: [admin] } # workflow-set from the asset
# ─────────────────────────────────────────────────────────────────────────
Workorder:
description: >
Maintenance work order. Safety documentation is BLOCKING: work cannot
start with a required permit or LOTO undocumented.
object:
id: { type: uuid, primaryKey: true }
woNumber: { type: string, required: true, unique: true, immutable: true, maxLength: 40 }
assetId: { type: uuid, required: true, immutable: true }
title: { type: string, required: true, maxLength: 200 }
description: { type: text, required: true }
workType: { $ref: '#/enums/WorkType' }
priority: { $ref: '#/enums/WorkPriority', default: normal }
status: { $ref: '#/enums/WorkorderStatus', default: requested }
requestedById: { type: string, required: true, immutable: true, maxLength: 100 }
requestedByName: { type: string, required: true, maxLength: 120 }
assignedToId: { type: string, maxLength: 100 }
assignedToName: { type: string, maxLength: 120 }
plannedStartDate: { type: date }
plannedEndDate: { type: date }
actualStartDate: { type: datetime }
actualEndDate: { type: datetime }
estimatedHours: { type: float, min: 0 }
actualHours: { type: float, min: 0 }
partsUsed: { type: json }
estimatedCost: { type: money }
actualCost: { type: money }
completionNotes: { type: text }
failureCode: { type: string, maxLength: 40 }
failureCause: { type: string, maxLength: 200 }
rejectionReason: { type: text }
closedAt: { type: datetime }
safetyPermitRequired: { type: boolean, default: false }
safetyPermitNumber: { type: string, maxLength: 40 }
lockoutTagoutRequired: { type: boolean, default: false }
lotoTagNumber: { type: string, maxLength: 40 }
$merge: { $ref: '#/objects/Timestamp' }
relationships:
asset: { type: belongsTo, target: Asset, foreignKey: assetId, onDelete: restrict }
indexes:
- { fields: [assetId] }
- { fields: [status] }
- { fields: [priority] }
- { fields: [assignedToId] }
stateMachine:
field: status
initial: requested
states:
requested:
transitions:
plan:
to: planned
guards: [maintenancePlanner, admin]
requiredFields: [assignedToId, plannedStartDate]
reject:
to: rejected
guards: [maintenancePlanner, operationsManager, admin]
requiredFields: [rejectionReason]
planned:
transitions:
approve:
to: approved
guards: [operationsManager, admin]
hold:
to: on_hold
guards: [maintenancePlanner, operationsManager, admin]
on_hold:
transitions:
release:
to: planned
guards: [maintenancePlanner, admin]
approved:
transitions:
start:
to: in_progress
# The assignee starts the work: requestedById isn't an
# owner-convention FK, and the requester isn't who executes
# anyway, so the guard checks the assignee directly.
guard: "field('assignedToId') == userId() || hasRole('maintenanceTech') || hasRole('admin')"
sets:
actualStartDate: "now()"
in_progress:
transitions:
complete:
to: completed
guard: "field('assignedToId') == userId() || hasRole('maintenanceTech') || hasRole('admin')"
requiredFields: [completionNotes, actualHours]
sets:
actualEndDate: "now()"
hold:
to: on_hold
guards: [maintenanceTech, admin]
completed:
transitions:
close:
to: closed
guards: [maintenancePlanner, operationsManager, admin]
sets:
closedAt: "now()"
closed:
final: true
rejected:
final: true
validation:
# BLOCKING safety controls.
permitBeforeStart:
rule: "status != 'in_progress' || safetyPermitRequired == false || safetyPermitNumber != null"
message: "A required safety permit must be documented before work starts"
lotoBeforeStart:
rule: "status != 'in_progress' || lockoutTagoutRequired == false || lotoTagNumber != null"
message: "A required Lockout/Tagout must be documented (tag number) before work starts"
plannedDatesSane:
rule: "plannedEndDate == null || plannedStartDate == null || plannedEndDate >= plannedStartDate"
message: "Planned end date must not precede the planned start date"
features:
audit: true
history:
events:
requested:
on: create
capture: [woNumber, title, workType, priority, requestedByName]
label: "WO {woNumber} — {title}"
detail: "Type: {workType}. Priority: {priority}. Requested by: {requestedByName}"
icon: lucide:clipboard-list
severity: info
planned:
on: transition
transition: plan
capture: [assignedToName, plannedStartDate, estimatedHours]
label: "Planned — assigned to {assignedToName}"
detail: "Start: {plannedStartDate}. Est. hours: {estimatedHours}"
icon: lucide:calendar
severity: info
started:
on: transition
transition: start
capture: [assignedToName, safetyPermitNumber, lotoTagNumber]
label: "Work started by {assignedToName}"
detail: "Permit: {safetyPermitNumber}. LOTO: {lotoTagNumber}"
icon: lucide:play
severity: info
completed:
on: transition
transition: complete
capture: [completionNotes, actualHours, partsUsed]
label: "Work completed — {actualHours} hours"
detail: "{completionNotes}"
icon: lucide:check
severity: success
closed:
on: transition
transition: close
label: "Work order closed"
icon: lucide:check-circle
severity: success
rejected:
on: transition
transition: reject
capture: [rejectionReason]
label: "Work order rejected"
detail: "{rejectionReason}"
icon: lucide:x-circle
severity: warning
emergencyWork:
on: create
condition: "priority == 'emergency'"
capture: [woNumber, title]
label: "EMERGENCY WORK ORDER — {woNumber}: {title}"
icon: lucide:alert-octagon
severity: error
# Without an explicit permissions block, every operation defaults to
# admin-only, so no technician could touch a work order.
permissions:
list: [authenticated]
get: [authenticated]
create: [maintenanceTech, maintenancePlanner, engineer, operationsManager, admin]
update: [maintenanceTech, maintenancePlanner, engineer, operationsManager, admin]
delete: [admin]
fields:
safetyPermitNumber: { write: [safetyEngineer, maintenancePlanner, admin] }
lotoTagNumber: { write: [safetyEngineer, maintenanceTech, admin] }
actualCost: { write: [maintenancePlanner, operationsManager, admin] }
Domain 2: Management of Change (moc)
domain:
name: moc
version: "1.0.0"
description: >
OSHA PSM 29 CFR 1910.119(l) Management of Change. Training completion and
PSSR are BLOCKING gates, not warnings; the originator cannot approve their
own change; temporary MOCs are reverted by schedule — the Complete
Reference's own §24 example, applied.
channels:
email:
provider: smtp
host: ${SMTP_HOST}
port: ${SMTP_PORT:587}
from: "moc-noreply@example.com"
objects:
Timestamp:
createdAt: { type: datetime, autoNow: create }
updatedAt: { type: datetime, autoNow: update }
enums:
ChangeType:
values:
equipment: { label: "Equipment" }
process_technology: { label: "Process Technology" }
procedure: { label: "Procedure" }
facility: { label: "Facility" }
config_change: { label: "Configuration Change" }
MocStatus:
values:
initiated: { label: "Initiated", color: "#94a3b8" }
submitted: { label: "Submitted", color: "#3b82f6" }
under_review: { label: "Under Review", color: "#8b5cf6" }
approved: { label: "Approved", color: "#10b981" }
implementing: { label: "Implementing", color: "#f97316" }
pssr_pending: { label: "PSSR Pending", color: "#06b6d4" }
closed: { label: "Closed", color: "#059669" }
reverting: { label: "Reverting", color: "#f59e0b" }
reverted: { label: "Reverted", color: "#64748b", final: true }
rejected: { label: "Rejected", color: "#ef4444", final: true }
RiskLevel:
values:
low: { label: "Low", color: "#94a3b8" }
medium: { label: "Medium", color: "#f59e0b" }
high: { label: "High", color: "#f97316" }
critical: { label: "Critical", color: "#ef4444" }
schedules:
# The spec's own §24 example, finally used in the domain it was written for.
temporaryMocExpiry:
description: "Warn 7 days before a temporary MOC's expiry; execute the revert on the date"
type: relative
source:
resource: Change
filter: "isTemporary == true && status == 'closed'"
relativeTo: temporaryExpiry
tiers:
- daysUntil: 7
action: notify
tag: 7day
channels: [email]
recipients: { roles: [operationsManager, safetyEngineer] }
template:
subject: "TEMPORARY MOC EXPIRING — {mocNumber}"
body: "{title}. Expires {temporaryExpiry}. Revert plan must be executed."
- daysUntil: 0
action: transition
transition: revert
tag: revert
resources:
Change:
object:
id: { type: uuid, primaryKey: true }
mocNumber: { type: string, required: true, unique: true, immutable: true, maxLength: 40 }
title: { type: string, required: true, maxLength: 200 }
changeType: { $ref: '#/enums/ChangeType', required: true }
facility: { type: string, required: true, maxLength: 120 }
unit: { type: string, maxLength: 60 }
status: { $ref: '#/enums/MocStatus', default: initiated }
description: { type: markdown, required: true }
justification: { type: markdown, required: true }
safetyImpact: { type: text }
environmentalImpact: { type: text }
operationalImpact: { type: text }
hazardAssessment: { type: text }
riskLevel: { $ref: '#/enums/RiskLevel' }
rejectionReason: { type: text }
affectedAssets: { type: json }
affectedProcedures: { type: json }
affectedPnids: { type: json }
createdById: { type: string, required: true, immutable: true, maxLength: 100 }
originatorName: { type: string, required: true, maxLength: 120 }
reviewerId: { type: string, maxLength: 100 }
reviewerName: { type: string, maxLength: 120 }
approvedById: { type: string, maxLength: 100 }
approvedByName: { type: string, maxLength: 120 }
isTemporary: { type: boolean, default: false }
temporaryDuration: { type: integer, min: 1 }
temporaryExpiry: { type: date }
revertPlan: { type: text }
trainingRequired: { type: boolean, default: false }
trainingComplete: { type: boolean, default: false }
preStartupReviewComplete: { type: boolean, default: false }
closedAt: { type: datetime }
revertedAt: { type: datetime }
$merge: { $ref: '#/objects/Timestamp' }
indexes:
- { fields: [status] }
- { fields: [facility] }
- { fields: [riskLevel] }
- { fields: [createdById] }
stateMachine:
field: status
initial: initiated
states:
initiated:
transitions:
submit:
to: submitted
guards: [owner, engineer, admin]
requiredFields: [description, justification, changeType]
submitted:
transitions:
review:
to: under_review
guards: [safetyEngineer, processEngineer, admin]
sets:
reviewerId: "$user.id"
reviewerName: "$user.username"
under_review:
transitions:
approve:
to: approved
# SoD: the approver is not the originator and not the safety
# reviewer. No admin bypass on approval.
guard: >
(hasRole('operationsManager') || hasRole('plantManager'))
&& field('createdById') != userId()
&& field('reviewerId') != userId()
requiredFields: [hazardAssessment, riskLevel, safetyImpact]
sets:
approvedById: "$user.id"
approvedByName: "$user.username"
reject:
to: rejected
guards: [operationsManager, plantManager, admin]
requiredFields: [rejectionReason]
requestRevision:
to: initiated
guards: [safetyEngineer, admin]
approved:
transitions:
implement:
to: implementing
guards: [engineer, operationsManager, admin]
implementing:
transitions:
completeImplementation:
to: pssr_pending
guards: [engineer, admin]
pssr_pending:
transitions:
completePssr:
to: closed
guards: [safetyEngineer, operationsManager, admin]
sets:
closedAt: "now()"
failPssr:
to: implementing
guards: [safetyEngineer, admin]
closed:
transitions:
revert:
to: reverting
guards: [operationsManager, admin]
reverting:
transitions:
completeRevert:
to: reverted
guards: [engineer, admin]
sets:
revertedAt: "now()"
reverted:
final: true
rejected:
final: true
validation:
# BLOCKING gates. A requiredFields check on a boolean is satisfied by
# `false` (non-null); these rules check the VALUE.
trainingBeforeImplementation:
rule: "status != 'implementing' || trainingRequired == false || trainingComplete == true"
message: "Required training must be complete before implementation (29 CFR 1910.119(l)(3))"
pssrMustActuallyPass:
rule: "status != 'closed' || preStartupReviewComplete == true"
message: "The Pre-Startup Safety Review must be complete before the MOC closes"
temporaryNeedsExpiryAndPlan:
rule: "isTemporary == false || (temporaryExpiry != null && revertPlan != null)"
message: "A temporary MOC must carry an expiry date and a revert plan"
revertNeedsPlan:
rule: "status != 'reverting' || revertPlan != null"
message: "Reverting requires a documented revert plan"
features:
audit: true
versioning: true
history:
events:
initiated:
on: create
capture: [mocNumber, title, changeType, facility, originatorName]
label: "MOC {mocNumber} initiated — {title}"
detail: "Type: {changeType}. Facility: {facility}. Originator: {originatorName}"
icon: lucide:file-plus
severity: info
submitted:
on: transition
transition: submit
label: "MOC submitted for safety review"
icon: lucide:send
severity: info
safetyReviewStarted:
on: transition
transition: review
capture: [reviewerName]
label: "Safety review started by {reviewerName}"
icon: lucide:shield
severity: info
approved:
on: transition
transition: approve
capture: [approvedByName, hazardAssessment, riskLevel, safetyImpact]
label: "MOC APPROVED by {approvedByName} — risk level {riskLevel}"
detail: "Safety impact: {safetyImpact}. Hazard assessment: {hazardAssessment}"
icon: lucide:check-circle
severity: success
rejected:
on: transition
transition: reject
capture: [rejectionReason]
label: "MOC REJECTED"
detail: "{rejectionReason}"
icon: lucide:x-circle
severity: error
implementationStarted:
on: transition
transition: implement
label: "Implementation started"
icon: lucide:hammer
severity: info
pssrCompleted:
on: transition
transition: completePssr
label: "Pre-Startup Safety Review PASSED — MOC closed"
icon: lucide:shield-check
severity: success
pssrFailed:
on: transition
transition: failPssr
label: "Pre-Startup Safety Review FAILED — returned for rework"
icon: lucide:shield-x
severity: error
reverted:
on: transition
transition: completeRevert
capture: [mocNumber, revertedAt]
label: "Temporary MOC {mocNumber} reverted"
icon: lucide:undo
severity: info
highRiskMoc:
on: fieldChange
field: riskLevel
condition: "riskLevel == 'high' || riskLevel == 'critical'"
capture: [mocNumber, title, riskLevel, facility]
label: "HIGH RISK MOC — {mocNumber}: {title}"
detail: "Facility: {facility}. Risk: {riskLevel}. Enhanced review required."
icon: lucide:alert-octagon
severity: error
roles: [safetyEngineer, operationsManager, plantManager, admin]
permissions:
list: [authenticated]
get: [authenticated]
create: [engineer, processEngineer, operationsManager, admin]
update: [engineer, processEngineer, safetyEngineer, operationsManager, plantManager, admin]
delete: [admin]
fields:
hazardAssessment: { write: [safetyEngineer, processEngineer, admin] }
riskLevel: { write: [safetyEngineer, operationsManager, admin] }
preStartupReviewComplete: { write: [safetyEngineer, operationsManager, admin] }
trainingComplete: { write: [safetyEngineer, operationsManager, admin] }
approvedById: { write: [admin] } # normally set only via the approve transition
reviewerId: { write: [admin] }
Edge / IoT domain (edge)
# Runs on an embedded-LINUX edge gateway (ARM SBC-class hardware). The white
# paper claimed ESP32/STM32 targets: a C++20 microkernel with dlopen-based
# plugins, SQLite, and JWT does not run on a ~520KB-RAM microcontroller with no
# dynamic loader. Sensor MCUs publish raw readings (MQTT/serial) to the gateway;
# Kalos evaluates the domain there. Do not claim bare-metal MCU support.
domain:
name: edge
version: "1.0.0"
resources:
Reading:
object:
id: { type: uuid, primaryKey: true }
assetTag: { type: string, required: true, maxLength: 40 }
pressure: { type: float, required: true }
temp: { type: float }
# Device-configured limits, defined on the resource so the event
# conditions below can reference them.
limitPressure: { type: float, required: true }
limitTemp: { type: float }
recordedAt: { type: datetime, autoNow: create }
indexes:
- { fields: [assetTag] }
history:
events:
overpressure:
on: create
condition: "pressure > limitPressure * 1.1"
capture: [assetTag, pressure, limitPressure]
label: "OVERPRESSURE — {assetTag}: {pressure} psi (limit {limitPressure})"
icon: lucide:alert-octagon
severity: error
temperatureAnomaly:
on: create
condition: "limitTemp != null && temp != null && temp > limitTemp * 1.05"
capture: [assetTag, temp, limitTemp]
label: "TEMPERATURE ANOMALY — {assetTag}: {temp}°F (limit {limitTemp})"
icon: lucide:thermometer
severity: warning
permissions:
list: [authenticated]
get: [authenticated]
create: [sensorGateway, admin]
update: [admin]
delete: [admin]
Investigator queries
# PHMSA — complete asset history (resolve the tag, then the timeline)
curl -s "$BASE/api/v1/assets/assets?filter[assetTag]=PL-2019-0847" \
-H "Authorization: Bearer $TOKEN" | jq -r '.data.items[0].id'
curl -s "$BASE/api/v1/assets/assets/{id}/history" -H "Authorization: Bearer $TOKEN"
# Related inspections and work orders via the relationship endpoints
curl -s "$BASE/api/v1/assets/assets/{id}/inspections" -H "Authorization: Bearer $TOKEN"
curl -s "$BASE/api/v1/assets/assets/{id}/workorders" -H "Authorization: Bearer $TOKEN"
# OSHA PSM — MOCs for a facility (resource filter; facility is not a history param)
curl -s "$BASE/api/v1/moc/changes?filter[facility]=Unit-7&sort=-createdAt" \
-H "Authorization: Bearer $TOKEN"
# Training/PSSR compliance is now structural: an implemented MOC with incomplete
# training CANNOT EXIST — the validation rule blocks it at 422. The audit answer
# is the YAML rule plus the absence of counterexamples, not a warning log.
# API 510 — critical findings and corrosion trend events across the fleet
curl -s "$BASE/api/v1/assets/inspections/history?event=criticalFinding&after=2025-01-01" \
-H "Authorization: Bearer $TOKEN"
curl -s "$BASE/api/v1/assets/inspections/history?event=highCorrosionRate" \
-H "Authorization: Bearer $TOKEN"
# Evidence the inspection-due engine ran (scheduler log)
curl -s "$BASE/api/v1/assets/schedules/inspectionDue/log?since=2026-01-01" \
-H "Authorization: Bearer $TOKEN"
# Workflow evidence: every write-back of inspection results to an asset
curl -s "$BASE/api/v1/assets/workflows/applyInspectionResults/instances?status=completed" \
-H "Authorization: Bearer $TOKEN"