← Back to Home
March 2026 AI PLC Concrete Plant

Teaching an AI to Program Industrial PLCs: How We Decoded Schneider's XEF Format

How a hidden folder in Control Expert's installation directory turned a large language model into a PLC programming tool.

If you've ever programmed a Schneider M340 PLC, you know EcoStruxure Control Expert. You also know its text editor is stuck somewhere around Notepad circa 2003. No multi-cursor. No find-and-replace with regex. No bulk editing. Copy-pasting I/O mappings to memory addresses — the kind of work that makes up 60% of a new project — is a manual, error-prone grind.

            <p>For years I dealt with it the way most automation engineers do: I'd export the relevant sections, edit them in Sublime Text using multi-cursor, paste them back. It worked, but it was still tedious. A full scaffolding pass &mdash; variables, I/O mapping, basic section structure &mdash; could easily eat half a day on a medium-sized project.</p>

            <p>Then, while working on integrating a large language model into our SCADA application for concrete batching plants, I had a thought: XEF files are XML. Plain text. The one thing LLMs are genuinely good at.</p>

            <h2>First attempt: the obvious approaches</h2>

            <p>My first instinct was automation through Control Expert's own interfaces. Surely a tool this mature would have some COM or OLE automation API &mdash; something I could script against.</p>

            <p>It doesn't. Control Expert has no programmatic interface. No COM objects, no command-line compiler, no scripting hooks. The only way in and out is the GUI and its XML export/import.</p>

            <p>But while browsing through Control Expert's installation directory looking for any kind of API, DLL, or automation hook, I found something better.</p>

            <h2>The hidden Rosetta Stone</h2>
C:\Program Files (x86)\Schneider Electric\Control Expert 15.3\SrcXmlSchema\
            <p>XSD (XML Schema Definition) is a formal language that describes the exact structure of an XML document &mdash; what elements are allowed, what attributes they can have, what values are valid, how elements nest inside each other. It's the blueprint. If the XEF file is a building, the XSD is the architectural drawing.</p>

            <p>And these XSDs defined the complete XEF format. <code>FefExchangeFile.xsd</code> &mdash; the root schema &mdash; includes 15 sub-schemas covering everything from <code>commonElements.xsd</code> (variables, headers, basic types) to <code>LDSource.xsd</code> (Ladder Diagram graphic representation), <code>SFCSource.xsd</code> (Sequential Function Charts), <code>IOConf.xsd</code> (hardware configuration), and <code>program.xsd</code> (program sections).</p>

            <p>Schneider's official documentation mentions XML export/import as a feature but says nothing about the format's structure. Forums had scattered fragments &mdash; someone's reverse-engineered snippet of the variable block, another person's partial description of the SFC format. But the complete, authoritative schema had been sitting in the installation directory the entire time, shipped with every Control Expert installation.</p>

            <figure class="blog-image">
                <img src="/blog/img/screenshot_xsd_folder.png" alt="SrcXmlSchema folder in Control Expert installation directory" loading="lazy">
                <figcaption>The SrcXmlSchema folder inside Control Expert's installation directory &mdash; 87 files defining the complete XEF format, shipped with every installation, practically undocumented.</figcaption>
            </figure>

            <p>With the schema in hand, I went back to the original idea &mdash; but now with a completely different approach. Instead of asking an AI to guess the XML structure, I could teach it the rules.</p>

            <p>I tried the simplest thing first: export a project to XEF, feed it to Claude, ask it to modify the Structured Text sections. For simple ST edits &mdash; like remapping <code>%I0.3.0</code> through <code>%I0.3.31</code> to <code>%MW100</code> through <code>%MW131</code> &mdash; it worked on the first try. Import, compile, zero errors.</p>

            <p>That was the moment I realized this could be something bigger than a text-editing shortcut.</p>

            <h2>The problem with "just XML"</h2>

            <p>XEF files are XML, but they're not simple XML. A typical project file is 500 KB of deeply nested elements spanning hardware configuration, variable declarations, program logic in four different languages (Structured Text, Ladder Diagram, Sequential Function Charts, Instruction List), communication parameters, and IDE settings.</p>

            <p>When I asked Claude to generate a complete Ladder Diagram section from scratch, it produced valid-looking XML that Control Expert rejected with cryptic errors. Having the XSD schemas was necessary but not sufficient &mdash; the AI also needed to learn the practical rules that aren't captured in any schema.</p>

            <h2>Building the skill</h2>

            <p>With the XSD schemas as ground truth, I built what Claude Code calls a "skill" &mdash; a structured document that teaches the AI the rules of a specific domain. Ours covers:</p>

            <p><strong>The complete XEF structure</strong> &mdash; every element, attribute, and nesting relationship. What goes inside <code>&lt;FEFExchangeFile&gt;</code>, how <code>&lt;dataBlock&gt;</code> holds variables, how <code>&lt;program&gt;</code> sections contain <code>&lt;STSource&gt;</code>, <code>&lt;LDSource&gt;</code>, or <code>&lt;FBDSource&gt;</code>, how <code>&lt;SFCProgram&gt;</code> chains steps, transitions, and actions.</p>

            <p><strong>All enumerations from the XSD</strong> &mdash; the valid values for every attribute. Step types (<code>initialStep</code>, <code>step</code>, <code>macroStep</code>), coil types (<code>coil</code>, <code>setCoil</code>, <code>resetCoil</code>, <code>callCoil</code>), contact types, SFC action qualifiers (<code>N</code>, <code>P</code>, <code>S</code>, <code>R</code>, <code>L</code>, <code>D</code>, <code>DS</code>), task types, protection levels. An AI guessing at these values produces XML that looks right but fails compilation.</p>

            <p><strong>Python extraction and generation patterns</strong> &mdash; ElementTree code for reading variables, sections, hardware config, execution order. And critically, the writing rules: preserve the original XML header, never use self-closing tags (<code>&lt;tag /&gt;</code> &mdash; Control Expert rejects them), always use <code>&lt;tag&gt;&lt;/tag&gt;</code>.</p>

            <p><strong>The traps</strong> &mdash; this is where most of the development time went. Every one of these was discovered the hard way, through a compile-error-fix cycle:</p>

            <ul>
                <li><code>step</code> is a reserved keyword in Control Expert. Use it as a variable name in a DFB and you get <code>E1235 invalid identifier</code>. We learned to use <code>sm_step</code> or <code>etapa</code> instead.</li>
                <li><code>WRITE_VAR</code>'s buffer parameter is called <code>EMIS</code>, not <code>EMESSION</code> as some Schneider documentation suggests. <code>E1031 'EMESSION' is not a parameter</code>.</li>
                <li>EBOOL variables in Ladder must use <code>&lt;coil&gt;</code>, not <code>&lt;operateBlock&gt;</code>. The <code>operateBlock</code> element is for calculations (REAL, INT, TIME). Using it with EBOOL compiles but produces wrong runtime behavior.</li>
                <li><code>posY</code> in FFBBlock elements is absolute grid position. Insert a new rung and every block below it needs its Y coordinate incremented, or elements overlap.</li>
                <li>One statement per <code>operateBlock</code>. Two assignments in the same block gives <code>E1002</code>. Need two? Two separate <code>typeLine</code> elements with <code>VLink</code> continuation.</li>
                <li>Boolean literals must be uppercase: <code>TRUE</code>, <code>FALSE</code>. Lowercase <code>true</code> is a syntax error in IEC 61131-3 ST.</li>
                <li>Dynamic arrays must be explicitly enabled in project settings (<code>unity.dynamicArray=1</code>) or <code>READ_VAR</code>/<code>WRITE_VAR</code> in DFBs fail with <code>E1208</code>.</li>
            </ul>

            <p>The skill document is 570 lines. Every line earned through a failed compilation.</p>

            <h2>A real example: Laumas W100 weighing indicator</h2>

            <p>To make this concrete, here's a DFB (Derived Function Block) generated entirely by AI &mdash; a Modbus TCP driver for a Laumas W100 industrial weighing indicator used in concrete batching plants.</p>

            <p>The task: communicate with the W100 over Modbus, read weight registers (gross, net, tare), decode status bits (stable, near-zero, load cell error), handle calibration commands, and manage communication errors with timeout and retry logic.</p>

            <p>In Control Expert, writing this by hand means: create the DFB, define 4 inputs and 14 outputs and 16 private variables, write the Modbus state machine with <code>READ_VAR</code>/<code>WRITE_VAR</code> calls, implement the register parsing (32-bit weight values split across two 16-bit registers), decode the division factor lookup table from the instrument's protocol manual, add timeout handling, add error recovery. A solid half-day of work if you know the protocol well.</p>

            <p>With the skill, I described the W100's Modbus register map and the AI generated 128 lines of production-ready Structured Text:</p>
(* State machine: READ registers 40006-40050 *)
10:
    READ_VAR(ADR := ADDM(addm_string),
        OBJ := 'MW', NUM := 5, NB := 45,
        GEST := gest_r, RECP => buf_r);
    comm_state := 1; sm_step := 11;

11:
    READ_VAR(ADR := ADDM(addm_string),
        OBJ := 'MW', NUM := 5, NB := 45,
        GEST := gest_r, RECP => buf_r);
    IF (gest_r[1] AND 1) = 0 THEN
        IF gest_r[2] = 0 THEN
            reg_status := buf_r[1];
            reg_gross_h := buf_r[2]; reg_gross_l := buf_r[3];
            reg_net_h := buf_r[4]; reg_net_l := buf_r[5];
            reg_div_units := buf_r[8];
            data_valid := TRUE;
            ok_counter := ok_counter + 1;
            IF ok_counter >= 3 THEN comm_ok := TRUE; END_IF;
            sm_step := 30;
        ELSE sm_step := 90;  (* Modbus error *)
        END_IF;
    ELSIF timer.Q THEN sm_step := 90;  (* Timeout *)
    END_IF;

(* Decode division factor from instrument config register *)
30:
    div_index := reg_div_units AND 16#FF;
    CASE div_index OF
        0: divisor:=100.0; 1: divisor:=50.0;  2: divisor:=20.0;
        3: divisor:=10.0;  4: divisor:=5.0;   5: divisor:=2.0;
        6: divisor:=1.0;   7: divisor:=0.5;   8: divisor:=0.2;
        9: divisor:=0.1;  10: divisor:=0.05; 11: divisor:=0.02;
       12: divisor:=0.01; 13: divisor:=0.005;14: divisor:=0.002;
       15: divisor:=0.001;
    ELSE divisor := 1.0;
    END_CASE;

    (* Reconstruct 32-bit weight from two 16-bit registers *)
    raw_32 := SHL(INT_TO_DINT(reg_gross_h), 16)
              OR WORD_TO_DINT(INT_TO_WORD(reg_gross_l));
    gross_weight := DINT_TO_REAL(raw_32) * divisor;
            <figure class="blog-image">
                <img src="/blog/img/screenshot_dfb_laumas_code.png" alt="AI-generated laumas_w100 DFB in Control Expert" loading="lazy">
                <figcaption>The AI-generated laumas_w100 DFB opened in Control Expert &mdash; 128 lines of Structured Text, compiled successfully as part of a complete concrete plant automation project.</figcaption>
            </figure>

            <p>This DFB was generated for a specific plant that uses Laumas W100 indicators &mdash; one of many plant-specific adaptations. The base project is a standard concrete batching program we've developed over years. The AI handles the customization: the specific instruments, I/O configurations, and communication protocols that differ from plant to plant. The result: 539 variables, 14 program sections, 6 DFBs, and an SFC sequence with 84 steps and 95 transitions &mdash; imported and compiled with zero errors.</p>

            <figure class="blog-image">
                <img src="/blog/img/screenshot_import_progress.png" alt="Importing XEF into Control Expert" loading="lazy">
                <figcaption>Importing the AI-generated XEF into Control Expert &mdash; "Begin IMPORT, File name: Z:\smart-concrete\docs\automat.XEF".</figcaption>
            </figure>

            <figure class="blog-image">
                <img src="/blog/img/screenshot_import_xef.png" alt="Import succeeded with 0 errors" loading="lazy">
                <figcaption>Import integration complete &mdash; "Process succeeded: 0 Error(s), 0 Warning(s)".</figcaption>
            </figure>

            <figure class="blog-image">
                <img src="/blog/img/screenshot_build_success.png" alt="Full project compiled in Control Expert" loading="lazy">
                <figcaption>The full project in Control Expert: PLC bus with M340 hardware configuration, project browser showing all DFBs and variables, build log confirming "0 Error(s)".</figcaption>
            </figure>

            <h2>What it looks like in practice</h2>

            <p>Today, when I need to create a new PLC program for a concrete batching plant, the workflow is:</p>

            <ul>
                <li>I start from our standard concrete batching project &mdash; the base program, SFC sequences, and core logic that's common across all plants.</li>
                <li>I describe the plant-specific configuration to Claude &mdash; what instruments are connected (weighing indicators, moisture sensors), what I/O modules are in the rack, what Modbus addresses to use. Claude generates the custom DFBs, adapts the I/O mapping, adds plant-specific variables and sections.</li>
                <li>I import the XEF into Control Expert. Compile. Fix any issues (rare now &mdash; the skill has matured).</li>
                <li>From our SCADA application, a script rewrites the Modbus memory addresses on the PLC, synchronizing the HMI with the generated program.</li>
                <li>I focus on what actually matters: the process logic. The dosing sequences, the timing, the safety interlocks &mdash; the parts that require engineering judgment, not copy-paste endurance.</li>
            </ul>

            <p>What used to take hours &mdash; adapting the standard project to a new plant's specific hardware and instruments &mdash; now takes minutes. The bottleneck has shifted from writing code to the manual steps that can't be automated: importing into Control Expert's GUI, building, and transferring to the PLC. The adaptation is the part an LLM does well &mdash; repetitive, pattern-based, rule-following. The process tuning is the part that still needs an engineer who's stood next to a running plant.</p>

            <h2>What comes next</h2>

            <p>We're working on something more ambitious: an LLM module integrated directly into the SCADA application. Not for generating PLC code &mdash; for optimizing plant parameters during operation. Concrete batching has dozens of variables that interact in non-obvious ways: aggregate moisture, cement temperature, mixing time, water correction factors. An AI that can observe the process data and suggest parameter adjustments could meaningfully improve production quality.</p>

            <p>The XEF skill was an accident of curiosity &mdash; I was looking for a way to avoid copy-paste and found a way to make an AI genuinely useful in industrial automation. The schemas were always there, in a folder nobody thought to open.</p>

            <hr>

            <p class="blog-endnote">We build automation systems for asphalt and concrete plants &mdash; control software, PLC programming, electrical panels, and retrofit of legacy systems. If you're dealing with aging plant controls that the original manufacturer no longer supports, <a href="/en/#contact">get in touch</a>.</p>