Creating a Device Type
This page walks through authoring a device type end to end, using the real
consystence.pump.centrifugal package that ships in the Consystence equipment
library. A type is a reusable definition — its tags, states, calculations,
faceplates, alarms and commands are written once and then bound to many
per-site instances (each with its own AssetNumber, e.g. PP-01). For the
type-versus-instance distinction, see the type system.
Anatomy of a type package
Section titled “Anatomy of a type package”A type is a folder of YAML, keyed by its typeId. The centrifugal pump looks
like this:
consystence.pump.centrifugal/├── manifest.yaml # typeId, tags, states, calculations, sceneBindings, aiContext├── simulation.yaml # optional deterministic sim profile (never writes Output tags)├── components/│ ├── faceplate.yaml # full operator control + monitoring screen│ └── status-card.yaml # compact overview card├── alarms/│ └── alarms.yaml└── commands/ └── commands.yamlmanifest.yaml is the spine — every other file references the tags, states and
calculations it declares. We will build the manifest first, then layer the
components, commands and alarms on top.
Step 1 — Manifest header
Section titled “Step 1 — Manifest header”Open manifest.yaml and declare the identity and metadata. The typeId is the
dotted, reverse-domain identifier; it is the stable key the platform uses (as
will the planned marketplace), so it never changes once published.
typeId: consystence.pump.centrifugalcontrolModel: peer # control-routing model for this typeversion: "1.0.0"name: Centrifugal Pumpdescription: > A general-purpose centrifugal pump with VFD speed control, including monitoring of discharge pressure, flow rate, and motor parameters. Supports start/stop control with interlock checking and fault handling.publisher: Consystencelicense: MITcategory: Pumpsplatform: ">=1.0.0"Quote version and platform so YAML reads them as strings, not numbers.
controlModel: peer declares how the device participates in control resolution —
a property of the type, not a per-instance choice. The field is live on the
manifest: peer routes instances to the peer control model, while omitting it
(or declaring legacy) keeps the combined routing — changing it is a manifest
edit, with no platform code change.
Step 2 — Declaring tags
Section titled “Step 2 — Declaring tags”Tags are the data points the type exposes. Casing matters: dataType and
access use the platform’s canonical capitalisation. Declare a physicalRole
on every tag — it defaults to Input when omitted, but declaring it explicitly
keeps the ownership split below legible. Here are real tags from the manifest,
covering each shape you will need:
tags: - name: Running dataType: Bool access: Read physicalRole: Output description: Pump is running (motor energised). Latched by the vPLC's start logic from StartCommand + interlocks.
- name: Speed dataType: Real access: ReadWrite physicalRole: Output unit: rpm rangeMin: 0 rangeMax: 3600 description: Current motor speed. Ramped by the vPLC's control logic toward SpeedSetpoint when Running.
- name: SpeedSetpoint dataType: Real access: ReadWrite physicalRole: Input unit: rpm rangeMin: 0 rangeMax: 3600 description: Speed setpoint for VFD. Operator writes; vPLC reads to drive ramp logic.
- name: BearingTemp dataType: Real access: Read physicalRole: Input unit: degC description: Bearing temperature. Simulator computes from thermal model; vPLC sees as a sensor reading.
- name: StartCmd dataType: Bool access: Write physicalRole: Input description: >- Start command port. The command writes this; the block consumes it as a rising edge and self-clears. Writing the Running STATUS tag instead cannot start or stop anything — the controller drives it.
- name: StopCmd dataType: Bool access: Write physicalRole: Input description: >- Stop command port. The command writes this; the block consumes it as a rising edge and self-clears. Writing the Running STATUS tag instead cannot start or stop anything — the controller drives it.
- name: ResetCmd dataType: Bool access: Write physicalRole: Input description: >- Fault-reset command port. reset-fault writes this; the block consumes it as a rising edge and self-clears. Writing the Fault/FaultCode STATUS tags instead cannot clear anything — the controller drives them.
- name: FaultCode dataType: Int access: Read physicalRole: Output description: Fault code (0=none, 1=overload, 2=bearing, 3=seal, 4=cavitation). Written by vPLC when fault active.StartCmd, StopCmd and ResetCmd are command ports (access: Write,
physicalRole: Input). Commands write command ports, never status tags: the
control block consumes the port and drives Running, Fault and FaultCode
itself. The shipped pump declares EnableAutoCmd and DisableAutoCmd the same
way for its mode commands.
Field-by-field:
| Field | Allowed values (this manifest) | Notes |
|---|---|---|
dataType | Bool, Int, Real (also DInt, String) | Capitalised; mirrors the controller types |
access | Read, ReadWrite, Write | The UI-facing read/write capability; command ports use Write |
physicalRole | Input, Output (also Internal, System) | Recommended on every tag (defaults to Input if omitted) — direction from the controller’s point of view |
unit | rpm, bar, m3/h, A, degC | Engineering unit for display and trending |
rangeMin / rangeMax | e.g. 0 / 3600 | Drives gauge scaling and validation |
The key idea is that physicalRole is independent of access. Speed is
ReadWrite in the UI but physicalRole: Output because the control logic owns
it; SpeedSetpoint is also ReadWrite in the UI but physicalRole: Input
because the controller reads it. Input tags are sensor readings, external
signals, setpoints and command ports (DischargePressure, FlowRate,
MotorCurrent, BearingTemp, Interlock, SpeedSetpoint, StartCmd,
StopCmd, ResetCmd); output tags are owned by the control logic
(Running, Speed, Fault, FaultCode, LocalMode, AutoMode,
SetpointRangeFault). This ownership split is what lets a simulation profile or
a real controller drive the same type without conflict:
flowchart LR
Field["Field sensors / simulator<br/>physicalRole: Input"] -->|readings + signals| DI["Device instance<br/>tag cache"]
vPLC["Control logic / vPLC<br/>physicalRole: Output"] -->|Running, Speed, Fault| DI
DI -->|"reads via {tags.X}"| UI["Faceplate + scene<br/>UI session"]
UI -->|"commands -> tagWrites"| DI
For the full data-type and access reference, see Tags. How those tags are read from a controller (through an edge, or for a ControlLogix directly by a site server in early access) is covered in PLC communication.
Step 3 — States
Section titled “Step 3 — States”States are derived labels evaluated from tag conditions. Each state lists one or
more boolean conditions (all must hold) and a color. States are evaluated in
order and the first match wins, so list off-normal states (Faulted, Interlocked,
Local) before the run states. The platform rejects a type that declares a state
no tag combination can reach:
states: - name: Faulted conditions: - "{tags.Fault}" color: red
- name: Interlocked conditions: - "{tags.Interlock}" - "!{tags.Running}" color: amber
- name: Running (Local) conditions: - "{tags.LocalMode}" - "{tags.Running}" - "!{tags.Fault}" color: blue
- name: Local conditions: - "{tags.LocalMode}" color: blue
- name: Running conditions: - "{tags.Running}" - "!{tags.Fault}" color: emerald
- name: Stopped conditions: - "!{tags.Running}" - "!{tags.Fault}" color: slateThe resolved state surfaces in the UI as {state.name} and {state.color},
which the faceplate uses for its status banner.
Step 4 — Calculations
Section titled “Step 4 — Calculations”Calculations are read-only derived values defined by an expression over tag
bindings. They are addressable from components as {calculations.X}:
calculations: - name: HydraulicPower expression: "({tags.FlowRate} * {tags.DischargePressure}) / 36" unit: kW description: Calculated hydraulic power output
- name: Efficiency expression: "(({tags.FlowRate} * {tags.DischargePressure}) / 36) / ({tags.MotorCurrent} * 0.4) * 100" unit: "%" description: Estimated pump efficiencyQuote "%" so YAML does not treat it specially.
Step 5 — Scene bindings
Section titled “Step 5 — Scene bindings”sceneBindings are per-type aliases consumed by the process-scene resolver. Each
binding gives the scene layout a stable name that the resolver joins with a
placement prefix to form the full {tags.<prefix><name>} expression — so a P&ID
scene can reference “the pump’s running indicator” without hard-coding tag names:
sceneBindings: - name: Running tag: Running description: Pump running indicator for the P&ID body colour - name: Speed tag: Speed description: Pump speed readout - name: Fault tag: Fault description: Fault banner visibility - name: Current tag: MotorCurrent description: Motor current readout - name: AutoMode tag: AutoMode description: Auto/manual mode indicatorStep 6 — Component faceplate
Section titled “Step 6 — Component faceplate”Components live under components/ and describe a semantic UI tree — you
author the meaning, not the markup. The tree is designed to project
to multiple renderers from one definition: the browser and the CLI are peer
renderers of the same JSON wire format, and WinUI3 is a planned target.
components/faceplate.yaml binds {tags.X} (and {tags.X.range}),
{calculations.X}, {state.X}, {instance.X} and alarm limits
({alarms.<id>.limit}):
componentType: faceplatename: Pump Faceplatedescription: Full control and monitoring faceplate for centrifugal pump
layout: type: card props: title: "{instance.name}" subtitle: "Centrifugal Pump" statusColor: "{state.color}" statusText: "{state.name}" children: - type: gauge props: value: "{tags.Speed}" range: "{tags.Speed.range}" unit: rpm label: Speed
- type: metric props: value: "{calculations.HydraulicPower}" unit: kW label: Power precision: 1
- type: slider props: value: "{tags.SpeedSetpoint}" range: "{tags.SpeedSetpoint.range}" step: 100 unit: rpm label: Speed Setpoint disabled: "{!tags.AutoMode}"
- type: button-group props: justify: center children: - type: button props: label: Start command: start variant: primary disabled: "{tags.Running || tags.Fault || tags.Interlock}"
- type: button props: label: Stop command: stop variant: secondary disabled: "!{tags.Running}"
- type: button props: label: Reset command: reset-fault variant: outline disabled: "!{tags.Fault}"{tags.Speed.range} reads the rangeMin/rangeMax declared on the tag in
Step 2, so the gauge and slider scale from the model rather than from numbers
typed into the faceplate.
Bindings can be inverted ({!tags.AutoMode}) and combined with boolean
operators ({tags.Running || tags.Fault || tags.Interlock}) directly inside
prop expressions. Whether a command button is enabled follows its command’s
preconditions (Step 7). On the web, setpoints are shown read-only for now:
setpoint entry is not yet available there. The compact status-card.yaml
follows the same model with clickAction: open-faceplate to drill into the full
faceplate.
Step 7 — Commands
Section titled “Step 7 — Commands”Commands live under commands/ and are the only sanctioned way the UI
writes tags. Each command declares a class (operate, mode, reset,
maintenance or jog), preconditions (guards with operator-facing
messages), tagWrites, and an audit flag. Parameterised commands reference
their arguments with {params.X}:
- id: start name: Start Pump class: operate description: Starts the pump at current speed setpoint confirm: true confirmMessage: "Start {instance.name}?" audit: true preconditions: - condition: "!{tags.Running}" message: Pump is already running - condition: "!{tags.Fault}" message: Clear fault before starting - condition: "!{tags.Interlock}" message: Safety interlock is active - condition: "!{tags.LocalMode}" message: Pump is in local mode tagWrites: # The command PORT, not the Running status tag (the controller drives that). - tagName: StartCmd value: "true"
- id: stop name: Stop Pump class: operate # A stop-class command must declare its direction, or the type is refused at load. direction: safe description: Stops the pump confirm: true confirmMessage: "Stop {instance.name}?" audit: true preconditions: - condition: "{tags.Running}" message: Pump is not running - condition: "!{tags.LocalMode}" message: Pump is in local mode tagWrites: - tagName: StopCmd value: "true"
- id: reset-fault name: Reset Fault class: reset description: Resets the fault condition confirm: true confirmMessage: "Reset the fault on {instance.name}? The cause must be cleared first." audit: true preconditions: - condition: "{tags.Fault}" message: No fault to reset - condition: "!{tags.LocalMode}" message: Pump is in local mode tagWrites: - tagName: ResetCmd value: "true"
- id: set-speed name: Set Speed class: operate description: Sets the speed setpoint confirm: false audit: true parameters: - name: speed dataType: Real min: 0 max: 3600 unit: rpm description: Target speed in RPM preconditions: - condition: "!{tags.LocalMode}" message: Pump is in local mode tagWrites: - tagName: SpeedSetpoint value: "{params.speed}"The class decides where a command is offered. On the web, only operate,
mode and reset commands are drawn as faceplate controls, and a command with
no class never is. A reset-class command must declare confirm: true, or the
type is refused at load. A stop-class command (stop, stop-rakes,
stop-loadout, close, close-loadout or abort) must declare
direction: safe or direction: not-safe, and no other command may declare
one. A safe stop takes no parameters, and Consystence never holds it back on
its preconditions: the controller’s own logic decides whether it acts, so each
of its preconditions must read plant state. Commands that take parameters, such
as set-speed, are not yet offered.
Note that start writes only the StartCmd command port, never the Running
status tag, and relies on preconditions plus the controller’s start logic for
interlocks. A command the faceplate offers must write only command ports and
setpoints; a faceplate button bound to a command that writes a status tag is
refused at load. Apart from a safe stop’s, preconditions are server-enforced
operability gates, evaluated fail-closed against good-quality tag data (missing
or degraded data rejects the command) — but still not a safety function;
interlocks remain in the controller.
Alarms follow the same pattern under alarms/ (condition + priority + message);
they are covered in detail in Alarms and commands.
Step 8 — Simulation profile (optional)
Section titled “Step 8 — Simulation profile (optional)”simulation.yaml is an optional deterministic profile that lets the type run
without hardware. By design it never writes output tags — those stay owned
by the control logic; the profile drives input tags (sensor readings and
external signals) and, where declared, simulator-only Internal tags:
schemaVersion: 1deviceType: consystence.pump.centrifugaldeviceTypeVersion: ">=1.0.0"
initialValues: SpeedSetpoint: 75 MotorCurrent: 0 BearingTemp: 32 FlowRate: 0 DischargePressure: 0 Interlock: false
stateScopedDynamics: - state: Running tag: FlowRate model: firstOrderLag parameters: steadyState: "Speed / 3600 * 120" timeConstant: 2.0Every tag a profile writes must not be declared physicalRole: Output in
the manifest — the validator rejects simulator writes to controller-owned
outputs. Profiles write Input tags (and Internal tags, for model-internal
state); that alignment with Step 2 is what makes this profile valid.
For the full profile schema — dynamics models, disturbances, failure modes and validation rules — see Simulation profiles.
Validate and next steps
Section titled “Validate and next steps”Once the package is assembled, check it against this list:
- Confirm
dataType,accessandphysicalRolecasing on every tag. - Check that all
{tags.X},{calculations.X}andsceneBindingsnames resolve to declared entries, and that faceplate ranges and limits bind to the model, never to numeric literals. - Check state order: every state must be reachable (the loader rejects one that earlier states hide).
- Verify that commands write only command ports and setpoints that exist, that
every command declares a class, that every stop-class command declares a
direction, and that simulation profiles never writeOutputtags.
To go deeper:
- Tags — data types, access levels and ranges in full.
- Alarms and commands — alarm rules and the command model.
- Type system — types versus instances, and the engineering model.