Product · Multi-vendor round-trip

Three vendors.
One source. Diff-clean both ways.

Open a working L5X, SimaticML, or TcPOU. Edit it in Python. Export it back. The diff is clean and the structure your team depends on stays intact.

Round-trip is not an export feature. It is a constraint on the entire architecture. Vendor IR mirrors the vendor's schema exactly so nothing rounds off. The Universal IR is a view, not a re-encoding.

One source · three outputs

Compile once. Open in Studio 5000, TIA Portal, or TwinCAT 3.

The output on the right was produced by the current compiler from the file on the left, with no edits. Each tab opens directly in its vendor's IDE.

valve_ctrl.py Source · Python
"""ValveCtrl — the Python the Koyl landing hero shows.

This file is the canonical source for the hero side-by-side. The Koyl website
reads it at build time and renders it as the left-hand panel. The right-hand
panel renders ``valve_ctrl.L5X``, which is regenerated from this file by
``generate.py`` and pinned by CI.

If this file changes, the L5X must be regenerated. CI will fail otherwise.
"""

from datetime import timedelta

from plx.framework import TON, Input, Output, fb, project


@fb
class ValveCtrl:
    cmd_open: Input[bool]
    feedback: Input[bool]
    valve_out: Output[bool]
    is_open: Output[bool]
    fault: Output[bool]

    fault_timer: TON

    def logic(self):
        self.valve_out = self.cmd_open
        if self.cmd_open:
            self.is_open = self.feedback
            self.fault_timer(IN=not self.feedback, PT=timedelta(seconds=3))
            self.fault = self.fault_timer.Q
        else:
            self.is_open = False
            self.fault = False


hero_project = project("HeroDemo", pous=[ValveCtrl])
<?xml version="1.0" ?>
<RSLogix5000Content SchemaRevision="1.0" SoftwareRevision="34.00" TargetName="HeroDemo" TargetType="Controller" ContainsContext="true">
  <Controller Use="Target" Name="HeroDemo" MajorRev="1" MinorRev="0" SFCExecutionControl="CurrentActive" SFCRestartPosition="MostRecent" SFCLastScan="DontScan" MatchProjectToController="false" CanUseRPIFromProducer="false" InhibitAutomaticFirmwareUpdate="0" DownloadProjectDocumentationAndExtendedProperties="true" DownloadProjectCustomProperties="true" ReportMinorOverflow="false">
    <SafetyInfo/>
    <DataTypes/>
    <Modules/>
    <AddOnInstructionDefinitions>
      <AddOnInstructionDefinition Name="ValveCtrl" ExecutePrescan="false" ExecutePostscan="false" ExecuteEnableInFalse="false" SoftwareRevision="34.00" SignatureRunModeProtect="false" IsEncrypted="false">
        <Parameters>
          <Parameter Name="cmd_open" TagType="Base" DataType="BOOL" Usage="Input" Required="false" Visible="true" ExternalAccess="Read/Write" Constant="false"/>
          <Parameter Name="feedback" TagType="Base" DataType="BOOL" Usage="Input" Required="false" Visible="true" ExternalAccess="Read/Write" Constant="false"/>
          <Parameter Name="valve_out" TagType="Base" DataType="BOOL" Usage="Output" Required="false" Visible="true" ExternalAccess="Read/Write" Constant="false"/>
          <Parameter Name="is_open" TagType="Base" DataType="BOOL" Usage="Output" Required="false" Visible="true" ExternalAccess="Read/Write" Constant="false"/>
          <Parameter Name="fault" TagType="Base" DataType="BOOL" Usage="Output" Required="false" Visible="true" ExternalAccess="Read/Write" Constant="false"/>
        </Parameters>
        <LocalTags>
          <LocalTag Name="fault_timer" DataType="FBD_TIMER" ExternalAccess="Read/Write"/>
        </LocalTags>
        <Routines>
          <Routine Name="Logic" Type="ST">
            <STContent>
              <Line Number="0"><![CDATA[valve_out := cmd_open;]]></Line>
              <Line Number="1"><![CDATA[IF cmd_open THEN]]></Line>
              <Line Number="2"><![CDATA[    is_open := feedback;]]></Line>
              <Line Number="3"><![CDATA[    fault_timer.PRE := 3000;]]></Line>
              <Line Number="4"><![CDATA[    fault_timer.TimerEnable := NOT feedback;]]></Line>
              <Line Number="5"><![CDATA[    TONR(fault_timer);]]></Line>
              <Line Number="6"><![CDATA[    fault := fault_timer.DN;]]></Line>
              <Line Number="7"><![CDATA[ELSE]]></Line>
              <Line Number="8"><![CDATA[    is_open := FALSE;]]></Line>
              <Line Number="9"><![CDATA[    fault := FALSE;]]></Line>
              <Line Number="10"><![CDATA[END_IF;]]></Line>
            </STContent>
          </Routine>
        </Routines>
      </AddOnInstructionDefinition>
    </AddOnInstructionDefinitions>
    <Tags/>
    <Programs/>
    <Tasks/>
    <Trends/>
    <DataLogs/>
  </Controller>
</RSLogix5000Content>

Examples are generated by the live compiler. CI fails if they drift.

Vendor coverage

The three vendors Western industry actually runs.

Allen Bradley

ControlLogix · CompactLogix

IDE

Studio 5000

Files

.L5X · .ACD (via L5X export)

AOIs and routines round-trip through the Universal IR. FB inheritance from the Python framework flattens cleanly into Studio 5000's AOI model.

Siemens

S7-1200 · S7-1500

IDE

TIA Portal

Files

.SimaticML

FCs, FBs, and DBs round-trip through SimaticML. Inheritance from the framework flattens into a single FB on raise; technicians open the result in TIA Portal as native Siemens code.

Beckhoff

TwinCAT 3 (any IPC)

IDE

TwinCAT 3 / Visual Studio

Files

.TcPOU · .tsproj

Native EXTENDS / SUPER^() support means FB inheritance from the framework maps directly. Closest one-to-one of the three vendor mappings.

Capability matrix

Three vendors. Five operations. All shipping.

Capability Allen Bradley Siemens Beckhoff
Parser (vendor file → vendor IR) Ships Ships Ships
Exporter (vendor IR → vendor file) Ships Ships Ships
Lower (vendor IR → Universal IR) Ships Ships Ships
Raise (Universal IR → vendor IR) Ships Ships Ships
Round-trip lossless Ships Ships Ships

What "lossless" actually means

No data loss. No structural drift. No surprise diffs.

The vendor IR is a typed Pydantic mirror of the vendor's native schema. Every attribute, every comment, every order-sensitive element survives the parse. When we re-emit, the file lines up byte-for-byte against the parts you didn't change.

The Universal IR is a view on top of the vendor IR: a vendor-agnostic projection used by the framework, the simulator, and the AI agent. The vendor IR remains the source of truth for vendor-specific detail. Round-tripping does not flow through the universal layer; it stays in the vendor IR.

That's what makes Koyl safe to use on the project your business runs on. You can edit one routine and check in the diff. Nothing else changes.

Pilot it on your codebase

Open a production project. Make a production change. See the diff.

Design partners run Koyl against their existing L5X, SimaticML, or TcPOU files and tell us what surprises us both.