Skip to content

guides

Intended Documentation

PLC Migration Patterns

Common safety-PLC interlocks (Siemens F-CPU, GuardLogix, TwinCAT Safety, Pilz) translated into Intended policy clauses — boolean, compound, consensus, and continuous-time predicates.

PLC Migration Patterns#

Audience: controls engineers familiar with safety-PLC programming (Beckhoff TwinCAT 3 Safety, Siemens F-CPU / SCL, Rockwell GuardLogix / AOIs, Pilz PNOZmulti) who are layering Intended on top of an existing safety stack.

Intended does not replace your safety PLC. It adds intent-aware authority gating above it. Your e-stop chain, light curtains, and safety-rated I/O stay exactly where they are. What changes is how AI-generated or operator-issued commands get classified, authorized, and audited before they reach motion.

Layering, not replacing#

                Operator HMI / autonomous planner / AI agent
                            │
                            ▼
                   ┌──────────────────┐
                   │  Intended layer  │   ← classify intent, evaluate policy, issue token
                   │ (this is new)    │
                   └────────┬─────────┘
                            │ token + safe-default
                            ▼
   ┌────────────────────────────────────────────────────┐
   │  Existing safety PLC + bus (unchanged)             │
   │  e-stop chain, light curtains, presence scanners   │
   └────────────────────────────────────────────────────┘
                            │
                            ▼
                       motion controller → motors

The safety PLC remains the authority on physical safety hazards. Intended is the authority on whether the command should be issued in the first place — and records every decision.

How a clause is evaluated#

A tenant policy is a list of clauses, evaluated in order against the physicalState snapshot submitted with each issuance. The first clause that both applies and whose predicate matches decides the outcome:

Clause fieldMeaning
idStable clause identifier; echoed into the denial as clauseId.
effectdeny, escalate, or allow (an explicit allow short-circuits later denies).
applies_to.oil_categoriesOptional — restrict to OI codes or families (OI-1500 matches OI-15xx).
applies_to.verbsOptional — restrict to specific structured-goal verbs.
predicateThe condition tested against the snapshot (types below).
safe_defaultFallback action carried in the denial.
safety_citationsOI-29xx citations recorded on the decision.

Predicate types the evaluator supports: boolean, number, continuous_time, consensus (N-of-M), compound (all / any), attestation, audit_gap, and classifier_confidence.

The default policy

Until a tenant uploads its own policy (POST /v1/physical/policy), the runtime applies a built-in default that denies on three conditions: safety/emergency_stop == true (cites OI-2901/OI-2902), safety/light_curtain_breached == true (cites OI-2902), and safety/human_in_cell == true (cites OI-2902/OI-2904). The patterns below extend that baseline.

Pattern translations#

Pattern 1 — Latched e-stop interlock#

Safety-PLC (Siemens F-SCL):

scl
IF estop_button_pressed THEN
    motion_enable := FALSE;
    motion_latch  := TRUE;
END_IF;

Intended clause:

json
{
  "id": "deny-on-emergency-stop",
  "effect": "deny",
  "applies_to": { "oil_categories": ["OI-1500", "OI-1800"] },
  "predicate": { "type": "boolean", "predicate": "safety/emergency_stop", "equals": true },
  "safe_default": "stop",
  "safety_citations": ["OI-2901", "OI-2902"]
}

What the layer adds: the safety PLC stops the motors; Intended additionally denies issuance of new motion tokens while e-stop is latched, with the attempted structured goal, actor identity, and OI category recorded. That record answers "did the agent keep trying to move during the latched e-stop?" — a question the PLC alone cannot.

Pattern 2 — Light-curtain mute / re-arm#

Deny on the breach predicate, and escalate restart commands after a recent breach:

json
{
  "id": "deny-on-curtain-breach",
  "effect": "deny",
  "predicate": { "type": "boolean", "predicate": "safety/light_curtain_breached", "equals": true },
  "safe_default": "stop"
},
{
  "id": "escalate-restart-after-breach",
  "effect": "escalate",
  "applies_to": { "verbs": ["resume-motion", "clear-fault"] },
  "predicate": {
    "type": "compound",
    "operator": "all",
    "predicates": [
      { "type": "boolean", "predicate": "safety/light_curtain_breached", "equals": false },
      { "type": "boolean", "predicate": "safety/curtain_recently_breached", "equals": true }
    ]
  },
  "safe_default": "request-operator"
}

The second clause forces any "resume" after a recent breach through human approval, independent of the PLC mute state — catching a partially-corrupted agent that issues resume-motion before the cell is visually confirmed clear.

Pattern 3 — Speed/separation monitoring (cobot, ISO/TS 15066)#

json
{
  "id": "deny-fast-motion-when-human-near",
  "effect": "deny",
  "applies_to": { "oil_categories": ["OI-1500"] },
  "predicate": {
    "type": "compound",
    "operator": "all",
    "predicates": [
      { "type": "boolean", "predicate": "workspace/human_within_2m", "equals": true },
      { "type": "number",  "predicate": "command/tcp_speed_mps", "greater_than": 0.25 }
    ]
  },
  "safe_default": "hold-position"
}

This denies a high-speed goal while a human is near, even if the PLC's speed override would clamp the actual TCP speed — giving you an audit record of the agent attempting unsafe-zone motion.

Pattern 4 — Two-hand control / dual confirmation#

N-of-M consensus (ECE-P4). The predicate trips when at least n of the sub-predicates match:

json
{
  "id": "require-dual-confirmation-for-hazardous-motion",
  "effect": "deny",
  "applies_to": { "oil_categories": ["OI-1900"] },
  "predicate": {
    "type": "consensus",
    "n": 2, "of": 2,
    "predicates": [
      { "type": "boolean", "predicate": "operator/left_hand_button",  "equals": false },
      { "type": "boolean", "predicate": "operator/right_hand_button", "equals": false }
    ]
  },
  "safe_default": "stop"
}

The PLC enforces the millisecond timing of the press; Intended records that two distinct operator inputs were the authorization source, with timestamps.

Pattern 5 — Continuous-time predicates ("if X for ≥250 ms")#

ECE-P2 — true only when the predicate has held the target value continuously for at least min_duration_ms:

json
{
  "id": "require-stable-presence-detection",
  "effect": "deny",
  "predicate": {
    "type": "continuous_time",
    "predicate": "safety/human_in_cell",
    "equals": true,
    "min_duration_ms": 250
  },
  "safe_default": "hold-position"
}

Use this when the decision should depend on input stability, not the instantaneous value — protecting against flicker in higher-level fusion (e.g. a vision stack briefly disagreeing with the safety scanner). The evaluator needs the predicate's recent history to compute the run-length; supply it via the snapshot's timestamps.

Pattern 6 — Time-bounded audit gap (AUD-P5)#

A robot that keeps moving when it cannot reach the audit chain has lost its forensic guarantee. An audit_gap clause denies once the last confirmed audit write is older than max_gap_ms. When this clause is the decider, the issuer returns 423 (not 422) — same envelope, different status — so your controller can distinguish "policy says no" from "we've lost the audit trail."

json
{
  "id": "deny-on-audit-gap",
  "effect": "deny",
  "predicate": { "type": "audit_gap", "max_gap_ms": 5000 },
  "safe_default": "transition-safe-state"
}

What you should NOT translate to Intended#

  • Hard real-time interlocks. Anything needing ms-level guaranteed response stays in the safety PLC. The (roadmap) edge verifier targets ≤50ms; sub-ms response belongs in dedicated silicon.
  • Direct I/O control. Intended issues authority; it does not drive outputs. The PLC continues to drive contactors, valves, brakes.
  • Safety-rated signals with no bus representation. If you cannot surface it as a PhysicalStateValue over a named protocol, Intended cannot reason about it.

Lifecycle parity#

Treat the policy file with the same rigor as your PLC program:

PLC disciplineIntended equivalent
Source control on the PLC projectGit on the policy JSON
Change-board approval for ladder editsPR review on policy edits
Validation before commissioningReplay against captured traces in shadow mode
Backup / rollbackGit revert + re-upload (POST /v1/physical/policy, versioned)
Operator change logThe audit chain records every decision

Migration sequencing#

Inventory

List every operator command, AI-issued command, and autonomous planner output. Map each to an OI category.

Predicates

List every safety-bus signal used by an interlock. Map each to a PhysicalStateProvider predicate name.

Bridge

Build the predicate provider against your bus and submit it in the snapshot. This is where the integration work lives — typically 1–2 weeks for a single cell.

Policy

Translate your interlock catalog into clauses. Start with effect: deny for the obvious safety patterns; add escalate where human approval is required. Upload with activate: false first.

Shadow

Run with the controller logging denials but still actuating from the PLC. Watch false-deny rates.

Cut over

Activate enforcement (activate: true). The PLC interlocks stay fully active — the Intended layer is defense-in-depth.

See also#

PLC Migration Patterns | Intended