Skip to content

Adaptation Guide

This worked end-to-end walkthrough takes a phase template that ships in the library, clones it for a different unit, changes one of the actuator roles from a discrete valve to a modulating valve, deploys the new template, and runs a batch against it on the simulator.

This is the example you'd use to onboard a new colleague onto the library. Every step is concrete and runnable on a fresh reference-plant install.

What we're going to do

Take tank-mix (the reference plant's mix-tank fill / agitate / drain phase) and produce a variant called tank-mix-modulating that:

  • Uses a modulating inlet valve (analog-control with a fill percentage parameter) in place of the original on/off valve. This is the most common real-world adaptation. The reference plant uses cheap on/off valves, while production plants almost always have modulating valves on inlets so flow can be ramped.
  • Runs against a hypothetical larger vessel with a higher fill capacity (1500 L, up from the reference plant's ~500 L).
  • Adds a maximum-fill safeguard by lowering the valve command to 50 % near the target level for a controlled top-off.

Then wire the new template into a one-step recipe and run a batch against it.

Prerequisites

You should have:

  • A working install with the reference plant deployed and dcs apply access to it. YAML examples below use namespace: site-riverbend as a placeholder. Substitute your own site name.
  • Read Mental Models once — you should know what a PhaseTemplate, ControlModule, and MasterRecipe are.
  • A skim of Phases so the YAML structure isn't a mystery.

If you can run dcs get batches and see batches, you're ready.

Before you start, note that tank-mix is used by three recipes today: sim-tank-blend-m1, sim-reaction-full-m1, and sim-api-isolation-m1 (see tank-mix). This walkthrough is careful not to break them. If you'd rather not worry about the existing reference-plant batches, skim the notes under Step 7 about the breaking vs. non-breaking paths.

Which interface to use

Every step is presented as tabs. Pick the interface that fits how you work:

  • UI/system → Equipment Library / Recipes, and /hmi → Batch Execution.
  • CLIdcs subcommands (dcs recipe approve, dcs simulate, …).
  • YAML — author locally and apply with dcs apply -f.

The UI flow for Steps 1–6 runs entirely inside the SFC editor. Step 1's Duplicate action opens it pre-populated with the source's General, CM Roles, Parameters, and action chart. Steps 2–5 edit Name, CM Roles, Parameters, and the action chart in place. Step 6 saves. The YAML flow edits a single file end-to-end before applying it in Step 6. Either path produces the same PhaseTemplate resource.

Which tabs appear on a given step depends on the action. Create/edit shows UI + YAML, imperative commands (approve, run) show UI + CLI, and workflow steps like dcs simulate that do more than a plain apply show all three. See STYLE-GUIDE § Tabbed Alternatives for the full rules. Integrators can find the REST equivalents in the API Reference.

Step 1: open a blank template to copy into

You need a new template that starts from tank-mix and diverges in a few fields. You build it up through Steps 2–5.

  1. Open /system → Equipment Library → Phases (site selector: your site riverbend).
  2. On the tank-mix row, open the three-dot menu and click Duplicate. The SFC editor opens pre-populated with the source's General fields, CM roles, parameters, and action chart. Name is left empty for you to fill in.

Leave the editor open. You'll fill in Name in Step 2, adjust the inlet-valve role in Step 3, widen parameter ranges in Step 5, and edit the action steps in Step 4.

Equipment Library → Phases list with + New Phase Template button in the System app, the starting point for authoring a new PhaseTemplate

The editor Steps 1–6 run inside, from the Phases list through Save. The clip builds a template from + New, and Duplicate opens the same editor pre-populated with the source's fields.

Export the existing tank-mix template to a local file so you can edit it. Two ways to do this:

# Via the CLI:
dcs get phasetemplate tank-mix -o yaml > tank-mix-modulating.yaml

# Or, if you cloned the repo, copy directly:
cp examples/riverbend/11-phase-templates.yaml /tmp/tank-mix-modulating.yaml
# ...then edit out everything except the tank-mix PhaseTemplate document.

Either way you should end up with a file that starts:

apiVersion: procedural.dcs.io/v1alpha1
kind: PhaseTemplate
metadata:
  name: tank-mix
  namespace: site-riverbend
spec:
  description: "Fill mixing tank to target level, agitate, and drain"
  capability: mixing
  cmRoles:
    - role: inlet_valve
      moduleType: discrete-valve
      description: "Tank inlet valve (on/off) for filling"
    - role: outlet_valve
      moduleType: discrete-valve
    # ... etc

Step 2: rename it

A new template needs a unique name. PhaseTemplate names are namespace-scoped, so tank-mix-modulating won't collide with the existing tank-mix.

In the SFC editor toolbar (top of the page):

  • Name: tank-mix-modulating
  • Capability: mixing (pre-filled from the source)
  • Timeout: leave at the source's value

Description is set later via the detail view if you need to refine it.

SFC editor: phase-template toolbar with Name, Capability, and Timeout fields above the canvas

metadata:
  name: tank-mix-modulating         # was: tank-mix
  namespace: site-riverbend
spec:
  description: "Fill mixing tank with a modulating inlet valve, agitate, drain"

Both templates can coexist. Recipes pick the one they want by name. This is exactly the "templates are class-like" pattern from Mental Models. Your new template is a new class. It is not a new version of the old one.

Step 3: change the inlet valve role from discrete to analog

The original tank-mix declares its inlet valve as a discrete-valve role. The modulating variant needs an analog-control role instead, so the phase can issue percentage commands.

Click CM Roles (N) in the toolbar. The CM Roles panel expands below it, with the chart still on screen. All four roles from tank-mix (inlet_valve, outlet_valve, agitator, level_sensor) are already pre-populated from the source.

Edit the inlet_valve row:

  • Module Type: analog-control (source has discrete-valve)
  • Description: "Tank modulating inlet valve (0-100% command)"

Leave the other three rows unchanged. Each field takes effect as you type it. Close puts the panel away and commits nothing, because there is nothing left to commit. Save in the toolbar is what writes the template.

SFC editor: CM Roles panel expanded under the toolbar, one editable row per role the phase template contracts for, each carrying role name, module type and description on a single line, with the chart still on screen below

Exactly this step's edit, on camera: a valve role's Module Type moved from the on/off type to analog-control in the CM Roles panel, and the changed contract read back after the panel was closed and reopened.

The original:

cmRoles:
  - role: inlet_valve
    moduleType: discrete-valve         # ← change this
    description: "Tank inlet valve (on/off) for filling"

Change to:

cmRoles:
  - role: inlet_valve
    moduleType: analog-control          # ← was discrete-valve
    description: "Tank modulating inlet valve (0-100% command)"

This is a role contract change. The phase template now requires the inlet_valve role to be bound to an analog-control instance, and a discrete-valve instance no longer satisfies it. Any recipe that uses this template must bind inlet_valve to an analog-control ControlModule on its target unit.

For the rest of the reference plant to keep working, leave the existing tank-mix template alone. Only the new tank-mix-modulating template requires the analog-control binding.

Step 4: rewrite the action steps to use a percentage command

The original phase wrote 1.0 to a discrete valve's CMD tag (open) and 0.0 (closed). For the modulating variant, the phase writes a percentage instead: a fast fill while far from target, then a slower top-off near target.

Duplicate pre-populated the action chart with a deep copy of tank-mix's steps and transitions, so you're editing. Nothing here is built from scratch. Make these adaptations on the canvas:

  • Step open-inlet → action ST:

    MESSAGE('Opening inlet valve to 100%');
    WRITE('inlet_valve.CMD', 100.0);
    
  • New step slow-fill (insert between filling and close-inlet) → action ST:

    MESSAGE('Approaching target — slowing inlet to 50%');
    WRITE('inlet_valve.CMD', 50.0);
    
  • Step close-inlet → action ST:

    MESSAGE('Target reached — closing inlet');
    WRITE('inlet_valve.CMD', 0.0);
    
  • Transition open-inletfilling condition: READ('inlet_valve.POS') > 5.0

  • New transition fillingslow-fill condition: READ('level_sensor.PV') >= fill_level - 50
  • New transition slow-fillclose-inlet condition: READ('level_sensor.PV') >= fill_level
  • Transition close-inletconfirm-fill condition: READ('inlet_valve.POS') < 2.0
  • All other transitions unchanged from tank-mix.

Save the SFC editor when done.

SFC editor: step selected in Edit mode, Action ST editor open with percentage-command WRITE() calls

Original open-inlet step:

- name: open-inlet
  actionST: |
    MESSAGE('Opening inlet valve');
    WRITE('inlet_valve.CMD', 1.0);

Adapted open-inlet step, opening to 100 %:

- name: open-inlet
  actionST: |
    MESSAGE('Opening inlet valve to 100%');
    WRITE('inlet_valve.CMD', 100.0);

Add a new slow-fill step between filling and close-inlet that ramps down to 50 % once the level reaches fill_level - 50:

- name: slow-fill
  description: "Reduce inlet to 50% for controlled top-off"
  actionST: |
    MESSAGE('Approaching target — slowing inlet to 50%');
    WRITE('inlet_valve.CMD', 50.0);

Adapted close-inlet step, writing 0 % (which is "fully closed" for a modulating valve, the same as for a discrete valve):

- name: close-inlet
  actionST: |
    MESSAGE('Target reached — closing inlet');
    WRITE('inlet_valve.CMD', 0.0);

Wire the new step into the transition table. The original tank-mix has feedback-guarded transitions around open-inlet and close-inlet (READ('inlet_valve.FB') = TRUE/FALSE). Keep those. They confirm the actuator has actually moved before the SFC advances. The analog valve exposes position as POS, so the transitions guard on POS > 0 / POS < 2.0 where the original guarded on the discrete FB boolean:

transitions:
  - fromStep: open-inlet
    toStep: filling
    conditionST: "READ('inlet_valve.POS') > 5.0"   # valve has actually opened
  - fromStep: filling
    toStep: slow-fill
    conditionST: "READ('level_sensor.PV') >= fill_level - 50"
  - fromStep: slow-fill
    toStep: close-inlet
    conditionST: "READ('level_sensor.PV') >= fill_level"
  - fromStep: close-inlet
    toStep: confirm-fill
    conditionST: "READ('inlet_valve.POS') < 2.0"   # valve has actually closed
  # ... rest of the transitions unchanged

The fast-fill phase advances to slow-fill 50 L before the target. Slow-fill advances to close-inlet exactly at the target. If the process is too noisy near the boundary, increase the gap from 50 L to 100 L, or add a deadband to the second guard.

Step 5: widen the parameter ranges for the larger vessel

The original tank-mix caps fill_level at 500 L. The 1500 L vessel needs a wider range.

Click Parameters (N) in the SFC editor toolbar. The Parameters panel expands below it, with the chart still on screen. All parameters from tank-mix are pre-populated. Edit the fill_level row:

  • Min: 50 (source has 10 — mechanically can't fill below 50 L)
  • Max: 1400 (source has 500 — leave 100 L headspace below the brim)

Leave the other parameters unchanged. Each field takes effect as you type it. Close puts the panel away and commits nothing, because there is nothing left to commit. Save in the toolbar is what writes the template.

SFC editor: Parameters panel expanded under the toolbar with name / type / default / min / max / unit columns for each declared parameter, and the chart the parameters feed still on screen below it

The Parameters panel live in the SFC editor: a chargeTime parameter declared and added, then referenced downstream. The clip adds a new row, and this step edits an existing row's min/max. The panel and its column grammar are the same either way.

The original tank-mix caps fill_level at 500 L:

- name: fill_level
  type: REAL
  required: true
  minValue: "10"
  maxValue: "500"            # ← change this
  unit: "L"

For the 1500 L vessel:

- name: fill_level
  type: REAL
  required: true
  minValue: "50"             # mechanically can't fill below 50 L
  maxValue: "1400"           # leave 100 L headspace below the brim
  unit: "L"

These ranges are captured in the PhaseTemplate for downstream tools such as recipe-authoring UI hints and validation reports. The MasterRecipe controller today validates that required parameters are bound and that referenced templates exist, but it does NOT enforce minValue/maxValue at recipe approval. If you need a hard stop, pair the range with an AlarmDefinition on level_sensor.PV so the runtime catches an over-fill in real time. See Parameter Binding for the current enforcement boundary.

Step 6: apply the new template

Click Save in the SFC editor toolbar. To verify, return to /system → Equipment Library → Phases (site: riverbend). Both tank-mix and tank-mix-modulating should be in the list. Hard-refresh (Ctrl+Shift+R) if tank-mix-modulating isn't showing.

Equipment Library → Phases list in the System app showing tank-mix and the new tank-mix-modulating after Save

The fully-edited tank-mix-modulating.yaml file from Steps 1–5:

apiVersion: procedural.dcs.io/v1alpha1
kind: PhaseTemplate
metadata:
  name: tank-mix-modulating
  namespace: site-riverbend
spec:
  description: "Fill mixing tank with a modulating inlet valve, agitate, drain"
  capability: mixing
  cmRoles:
    - role: inlet_valve
      moduleType: analog-control
      description: "Tank modulating inlet valve (0-100% command)"
    # ... outlet_valve, agitator, level_sensor unchanged from tank-mix ...
  parameters:
    - name: fill_level
      type: REAL
      required: true
      minValue: "50"
      maxValue: "1400"
      unit: "L"
  # ... actionChart with open-inlet / filling / slow-fill / close-inlet /
  # confirm-fill / ... / done steps and the transition table from Step 4 ...

Apply:

dcs apply -f tank-mix-modulating.yaml

The Equipment Library → Phases view will show both tank-mix and tank-mix-modulating immediately. Hard-refresh the browser tab if it doesn't.

Step 7: provide an inlet_valve binding the new template can use

This is the step people miss most often. The new template requires inlet_valve to be an analog-control instance, but the existing reference-plant unit mix-tank-1 has its inlet valve wired to a discrete-valve ControlModule (mt1-inlet-valve). Role binding happens exactly once per unit. Unit.spec.cmRoles is a single role → ControlModule map, so the new template can't coexist with the old one on the same unit without swapping the binding or creating a new unit.

Two options:

Option A: rebind mix-tank-1 (breaking). Create a new analog-control ControlModule, then update mix-tank-1.spec.cmRoles to point inlet_valve at it. The original tank-mix template now fails its role-contract check on this unit, so sim-tank-blend-m1 and other recipes that use tank-mix on mix-tank-1 will no longer run. This is fine if you're done with the reference-plant recipes, but don't do it on a shared cluster.

Option B: clone the unit (non-breaking). Create a new unit (mix-tank-1b) with its own cmRoles, including an analog-control inlet valve. The original mix-tank-1 is untouched and existing recipes keep working. The new recipe targets the clone unit. This is the path used here.

Two new resources are needed: an analog-control ControlModule whose tagBindings point at unused simulator channels, and a cloned Unit whose cmRoles.inlet_valve references the new CM. Use the tag keys the analog-control template declares: position_fb (input) and valve_out (output). See analog-control.

  1. Create the ControlModule. /system → Equipment Library → Control Modules+ New Control Module. Fill in:

    • Name: mt1b-inlet-valve
    • Template: analog-control
    • Parent Unit: mix-tank-1b (you'll create it next, and the field lets you forward-reference)
    • Tag bindings: position_fbtank-sim:analog.9, valve_outtank-sim:analog.10

    Click Save.

  2. Create the Unit.Units+ New Unit:

    • Name: mix-tank-1b
    • Process cell: sim-cell
    • Mode: Automatic
    • Node selector: kubernetes.io/hostname=dcs-demo
    • Runtime ports: health 61063, gRPC 61163, metrics 61263 (unique per unit, so this clone can't reuse mix-tank-1's ports)
    • Capabilities: add mixing with maxRPM=100, maxVolume=1500
    • CM roles: inlet_valve=mt1b-inlet-valve, outlet_valve=mt1-outlet-valve, agitator=mt1-agitator, level_sensor=mt1-level-sensor

    Click Save.

System: + Add Control Module form with templateRef, parent unit, and tag-binding fields. The Unit form that follows uses the same column layout

+ Add Control Module live: template picked, the tag binding typed as an iomodule:channel string, and the instance polling real values seconds after Create. The clip instantiates an analog sensor where this step creates a valve, and it creates no Unit. The Unit half of this step stays with the form above.

# tank-mix-modulating-cm.yaml
apiVersion: physical.dcs.io/v1alpha1
kind: ControlModule
metadata:
  name: mt1b-inlet-valve
  namespace: site-riverbend
spec:
  parentName: mix-tank-1b
  parentKind: Unit
  description: "Mix-tank-1b modulating inlet valve"
  templateRef: analog-control
  tagBindings:
    position_fb: "tank-sim:analog.9"   # unused analog input
    valve_out:   "tank-sim:analog.10"  # unused analog output

In the cloned Unit's role map, the only role that changes is inlet_valve. Everything else points at a fresh set of CMs you'd define alongside (mt1b-level-sensor, etc.), omitted here for brevity. A minimal definition for just the role change:

# mix-tank-1b.yaml
apiVersion: physical.dcs.io/v1alpha1
kind: Unit
metadata:
  name: mix-tank-1b
  namespace: site-riverbend
spec:
  description: "Mix-tank clone with modulating inlet valve"
  processCellName: sim-cell
  mode: Automatic
  nodeSelector:
    kubernetes.io/hostname: dcs-demo
  runtimeHealthPort: 61063       # unique per unit (hostNetwork pod)
  runtimeGRPCPort:   61163       # unique per unit
  runtimeMetricsPort: 61263      # unique per unit
  capabilities:
    - name: mixing
      type: mixing
      parameters:
        maxRPM: "100"
        maxVolume: "1500"
  cmRoles:
    inlet_valve:  mt1b-inlet-valve   # analog-control, the new one
    outlet_valve: mt1-outlet-valve   # reusing the original reference-plant CMs
    agitator:     mt1-agitator       # for the rest of the roles
    level_sensor: mt1-level-sensor

Apply both (Unit first so the CM's parentName forward-reference resolves):

dcs apply -f mix-tank-1b.yaml
dcs apply -f tank-mix-modulating-cm.yaml

Verify:

Equipment Library → Control Modules shows mt1b-inlet-valve with templateRef: analog-control. Units shows mix-tank-1b alongside mix-tank-1.

Equipment Library: Control Modules list with the new clone appearing alongside existing templates

dcs get controlmodules
dcs get units

You should see mix-tank-1b alongside mix-tank-1, and mt1b-inlet-valve with templateRef: analog-control.

Step 8: write a recipe that uses the new template

This step writes a minimal one-step master recipe that calls the new phase. Note the field shapes: the procedure lives under procedure.chart (not sfcChart), steps use templateRef + templateKind, and unit selection is driven by targetUnits[capability] at the recipe level. A step just references the capability via targetCapability.

/systemRecipes+ New Master Recipe. Fill in each section of the edit form:

  • Metadata header:
    • Name: tank-mix-mod-test
    • Description: Test the modulating tank-mix variant
    • Product name / ID: Test Mix / TEST-MOD-001
    • Version: 1.0.0
  • Header: batch size 800 L, min 100, max 1400.
  • Target: process cell sim-cell, capability mixing → unit mix-tank-1b (the clone from Step 7).
  • Parameters (formal parameters the operator sets per batch):
    • fillLevel REAL, default 800, range 501400, unit L
    • agitatorSpeed REAL, default 50
    • mixDuration TIME, default 1m30s
    • drainLevel REAL, default 5
  • Procedure chart: initial step mix, one step mix that references tank-mix-modulating / PhaseTemplate / capability mixing. Under Parameter bindings map each formal parameter to the phase template's template parameter:

    Template parameter Bound to
    fill_level fillLevel
    agitator_speed agitatorSpeed
    mix_duration mixDuration
    drain_level drainLevel

Click Save. The recipe appears in the Master Recipes list in its initial Draft state.

Master Recipe edit form: metadata header, parameters, and procedure chart editor

The recipe form, the procedure editor, and the controller's validation loop: a broken templateRef flagged red at authoring time, fixed from the picker, and the Target section resolving the step's capability to a unit. The parameter panes are covered on Parameter Binding.

# tank-mix-mod-test.yaml
apiVersion: recipe.dcs.io/v1alpha1
kind: MasterRecipe
metadata:
  name: tank-mix-mod-test
  namespace: site-riverbend
spec:
  description: "Test the modulating tank-mix variant"
  productName: "Test Mix"
  productID: "TEST-MOD-001"
  version: "1.0.0"
  header:
    batchSize: "800"
    batchSizeUnit: "L"
    minBatchSize: "100"
    maxBatchSize: "1400"
  targetProcessCell: sim-cell
  targetUnits:
    mixing: mix-tank-1b           # uses the clone unit from Step 7
  parameters:
    - name: fillLevel
      type: REAL
      defaultValue: "800"
      minValue: "50"
      maxValue: "1400"
      engineeringUnit: "L"
    - name: agitatorSpeed
      type: REAL
      defaultValue: "50"
    - name: mixDuration
      type: TIME
      defaultValue: "1m30s"
    - name: drainLevel
      type: REAL
      defaultValue: "5"
  procedure:
    chart:
      initialStep: mix
      steps:
        - name: mix
          templateRef: tank-mix-modulating
          templateKind: PhaseTemplate
          targetCapability: mixing
          parameterBindings:
            fill_level: fillLevel
            agitator_speed: agitatorSpeed
            mix_duration: mixDuration
            drain_level: drainLevel
      transitions: []

Apply:

dcs apply -f tank-mix-mod-test.yaml

Approve:

Open tank-mix-mod-test from the Recipes list, click Approve, enter a reason in the dialog, and confirm. The lifecycle state transitions from Draft to Approved.

MasterRecipe detail: ACTIONS panel with Approve, Reject, and Edit buttons for a Draft recipe

The ACTIONS ceremonies end to end: Approve's reason dialog holds its confirm disabled until the reason policy is met, and the typed-name Delete at the end of the clip is the same ceremony Step 10's teardown uses.

dcs recipe approve tank-mix-mod-test

Step 9: run a batch and watch it execute

Start a simulation batch against the new recipe. dcs simulate creates a simulation Batch, waits for it to finish, and cleans up (pass --keep if you want the Batch to stick around):

/hmiBatch Execution+ NewBatch → pick tank-mix-mod-test from the recipe list → accept the default formula values → Start. Watch the procedural tree cascade on the batch detail page. You'll see the new slow-fill step appear in the tree, the visual confirmation that your adaptation worked. Hover an SFC node to see its live action status.

HMI → Batch Execution view: batch list and procedural tree panel where the adapted recipe's new step would appear mid-run

Create → Start → Running → the batch detail with its live procedural tree. The clip runs a generic recipe, and with tank-mix-mod-test the same tree carries your new slow-fill step.

dcs simulate tank-mix-mod-test --keep

While it runs, open another shell and watch the procedural tree stream live:

dcs watch states --unit mix-tank-1b -i 2s
apiVersion: batch.dcs.io/v1alpha1
kind: Batch
metadata:
  name: tank-mix-mod-test-run-1
  namespace: site-riverbend
spec:
  batchID: tank-mix-mod-test-run-1
  masterRecipeRef: tank-mix-mod-test
  simulation: true

Apply with dcs apply -f. Note that this will execute immediately. Prefer dcs simulate for throwaway test runs, and reserve raw Batch manifests for production. Never bulk-apply a directory of Batch YAMLs.

You should see the batch move through these states:

  1. The mix step starts.
  2. The tank-mix-modulating phase begins on mix-tank-1b.
  3. The phase walks through open-inletfillingslow-fillclose-inletconfirm-fill (operator prompt) → ... → done.

If the phase gets stuck on filling, pull a runtime snapshot and compare mt1-level-sensor.PV against mt1b-inlet-valve.CMD:

/dataTrends → add both mt1-level-sensor.PV and mt1b-inlet-valve.CMD to the chart over the batch window. The valve command should step from 100 → 50 → 0 while the level rises monotonically. A flat level with a non-zero command is the classic "written to the wrong channel" symptom.

Data → Trends view with two tags plotted against engineering range axes, the same view used for post-run batch diagnosis

Building a multi-tag trend and drilling into one control module. The clip reviews temperature history, and the diagnosis here plots mt1-level-sensor.PV against mt1b-inlet-valve.CMD the same way and reads the two curves against each other.

dcs get runtime --unit mix-tank-1b
dcs get mt1b-inlet-valve.CMD

The most common failure is that tagBindings on the new ControlModule write to channels the simulator doesn't translate into a level sensor reading, so the level never rises and the guard never becomes true.

Step 10: tear down

If this was just an experiment, roll the five new resources back. The UI offers a row-action Delete on each one. The CLI covers everything except the Template kinds (PhaseTemplate / OperationTemplate / UnitProcedureTemplate), which still fall back to kubectl.

  1. Abort the batch if it's still running: /hmi → Batch Execution → open tank-mix-mod-test-run-1Abort → enter a reason and confirm.
  2. Delete the master recipe: /systemRecipes → open tank-mix-mod-testDelete in the ACTIONS panel.
  3. Delete the unit: /system → Equipment Library → Units → row-action menu on mix-tank-1bDelete.
  4. Delete the control module: /system → Equipment Library → Control Modules → row-action menu on mt1b-inlet-valveDelete.
  5. Delete the phase template: /system → Equipment Library → Phases → row-action menu on tank-mix-modulatingDelete.

Each Delete pops the same confirm-delete modal. It spells out what will be removed, and you type the resource name to confirm.

MasterRecipe detail: ACTIONS panel with the Delete button that opens the confirm-delete modal, representative of the row-action Delete on every resource kind

# Abort the batch if still running (reason required for irreversible commands):
dcs command Batch <batch-name> Abort --reason "cleanup"

# Delete the resources that have dcs delete subcommands:
dcs delete masterrecipe tank-mix-mod-test
dcs delete controlmodule mt1b-inlet-valve
dcs delete unit mix-tank-1b

PhaseTemplate, OperationTemplate, and UnitProcedureTemplate don't yet have dcs delete subcommands. Remove the new phase template via kubectl until the CLI catches up:

kubectl -n site-riverbend delete phasetemplate tank-mix-modulating

tank-mix, mix-tank-1, and the original reference-plant recipes are untouched. Adding a parallel unit and leaving the existing one alone is the whole point of Option B in step 7 (clone the unit).

What you just did, in ISA-88 terms

You created a new PhaseTemplate (a new class), instantiated a new ControlModule on a cloned Unit (new instances), wrote a new MasterRecipe that targets the clone unit's mixing capability, and ran a new Batch through it. The old templates, unit, and recipes still work, and the new ones work in parallel. This is the core adaptation pattern: clone, rename, bind, run.

For more details on the recipe lifecycle (master recipe → control recipe → batch), see Recipes. For the parameter-binding mechanism specifically, see Parameter Binding.

Common pitfalls

Symptom Cause Fix
Recipe's BindingsResolved condition is False (or batch fails) with "requires moduleType analog-control but unit … binds it to ControlModule … of moduleType discrete-valve" The target unit's cmRoles.inlet_valve points at a discrete-valve CM but the template requires analog-control Either rebind Unit.spec.cmRoles.inlet_valve to an analog-control instance (Option A in step 7), or target a different unit whose cmRoles already point at an analog-control CM (Option B)
Phase transitions to filling and never advances Tag bindings on the new ControlModule write to channels the simulator doesn't translate to a level sensor reading Check tank-sim IOModule channel mapping; pick a channel the agitated-tank SimulationPreset (or an inline spec.simulation.behaviors override) actually drives
Over-fill during a run even though the recipe looked "in range" dcs recipe approve does not enforce minValue/maxValue on formal parameters today — out-of-range values are accepted Add an AlarmDefinition on level_sensor.PV with exceptionAction: Stop as the runtime safeguard
New phase doesn't appear in Equipment Library after apply Browser cache Hard-refresh (Ctrl+Shift+R) the gateway tab
Batch starts but inlet_valve.CMD doesn't move Wrote to the wrong tag — modulating valves use CMD (REAL) not state (BOOL) Double-check the analog-control template's tag list — see analog-control

For more failure-mode patterns at the phase level, see Phases.