<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: tia-forge</title>
    <description>The latest articles on DEV Community by tia-forge (@tiaforge).</description>
    <link>https://dev.to/tiaforge</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4179613%2Fddd904c9-5210-4f7a-be21-51476a4ccf54.png</url>
      <title>DEV Community: tia-forge</title>
      <link>https://dev.to/tiaforge</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tiaforge"/>
    <language>en</language>
    <item>
      <title>Claude Code Meets TIA Portal: One Prompt, a Compiled and Simulated PLC Program</title>
      <dc:creator>tia-forge</dc:creator>
      <pubDate>Tue, 27 Oct 2026 08:19:21 +0000</pubDate>
      <link>https://dev.to/tiaforge/claude-code-meets-tia-portal-one-prompt-a-compiled-and-simulated-plc-program-1h6c</link>
      <guid>https://dev.to/tiaforge/claude-code-meets-tia-portal-one-prompt-a-compiled-and-simulated-plc-program-1h6c</guid>
      <description>&lt;p&gt;Most "AI writes PLC code" posts stop at the SCL listing. The listing is the easy part. What matters is whether the thing lands in your project, compiles, downloads, and behaves — and whether you can tell when it doesn't.&lt;/p&gt;

&lt;p&gt;We ran that whole path on a real TIA Portal V19 machine: from an empty device, one requirement, one prompt, to a program that compiled with zero errors, downloaded to S7-PLCSIM Advanced, and passed three behavioural scenarios. Everything below is measured, including the parts that failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this was measured.&lt;/strong&gt; 2026-10-27, Windows, TIA Portal V19 (Openness 19.0), S7-PLCSIM Advanced 6.0, gateway 1.3.2.0 driving the open project. The agent talked to TIA Portal over MCP, the same way any MCP client would.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the gateway gives an agent
&lt;/h2&gt;

&lt;p&gt;Five groups of capability, all against the project that is currently open in TIA Portal:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Read&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;project tree, block contents, tag tables, UDTs, cross-references, HMI screens, hardware config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Write&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;generate or rewrite SCL, create blocks and tables, write back, compile one block or the whole PLC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Configure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;add a CPU, plug modules, create subnets, wire IO systems, set PROFINET device names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Verify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;download to S7-PLCSIM Advanced, read and write variables, run scenarios&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Archive&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;export SimaticML for offline diffing and version control&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This build exposes 34 tools across those five groups, plus one more for LAD work. Install it in whichever client you use — the tool surface is identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hooking it up
&lt;/h2&gt;

&lt;p&gt;With Claude Code, it is two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http tialink http://192.168.1.110:8080/mcp
claude mcp list          &lt;span class="c"&gt;# confirm it connected, and how many tools it sees&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other clients differ only in where the config lives: Cursor (&lt;code&gt;.cursor/mcp.json&lt;/code&gt;), Claude Desktop (&lt;code&gt;claude_desktop_config.json&lt;/code&gt;), or a UI connector in WorkBuddy/CodeBuddy (remember to click &lt;strong&gt;Enable&lt;/strong&gt; — an added-but-disabled connector shows no tools and looks broken).&lt;/p&gt;

&lt;p&gt;One practical note: raise the client timeout. Adding a CPU took 34 s on an older device and 2.2 s on a current one; a download takes ~10 s. A 60 s default will fire on work that is merely slow, and you will misread it as a failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  One requirement, one prompt
&lt;/h2&gt;

&lt;p&gt;The requirement, as an engineer would write it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One start button starts motor 1; motor 1 running for 3 s starts motor 2 automatically.&lt;/li&gt;
&lt;li&gt;The stop button (normally closed) or a fault on either motor stops both immediately.&lt;/li&gt;
&lt;li&gt;Each motor has a run output; timing uses IEC timers.&lt;/li&gt;
&lt;li&gt;The logic lives in a reusable FB; OB1 calls it.&lt;/li&gt;
&lt;li&gt;I/O on M area: &lt;code&gt;M0.0&lt;/code&gt; start, &lt;code&gt;M0.1&lt;/code&gt; stop (normally closed), &lt;code&gt;M0.2&lt;/code&gt; fault, &lt;code&gt;M0.3&lt;/code&gt; motor 1, &lt;code&gt;M0.4&lt;/code&gt; motor 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prompt we actually sent (verbatim):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;【需求】两台电机的顺序启动控制
- 一台启动按钮启动 1 号电机；1 号电机运行 3 秒后自动启动 2 号电机
- 停止按钮（常闭）或任一电机故障时，1 号、2 号电机立即全部停止
- 两台电机各有运行输出，定时用 IEC 定时器
- 逻辑写成一个可复用的 FB，OB1 里调用它
- 输入输出用 M 区：M0.0 = 启动按钮，M0.1 = 停止（常闭，1 表示允许），M0.2 = 故障，
  M0.3 = 1 号电机，M0.4 = 2 号电机

【要求】
1. 自己决定步骤和工具，把它们下发到 PLC_3 并编译通过（0 错误）
2. 编译通过后下载到 S7-PLCSIM Advanced，创建全新的实例名，运行后用场景验证
3. 不要保存工程
4. 最后贴出完整的 SCL 源码和实测数值
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No SCL template, no step list — the sequence was the agent's choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it produced
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FUNCTION_BLOCK "FB_MotorSeq"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
   VAR_INPUT
      Start : Bool;             // start button (M0.0)
      StopPermit : Bool;        // stop, normally closed: 1 = allowed (M0.1)
      Fault : Bool;             // fault (M0.2)
      RunDelay : Time := T#3S;  // motor 2 delay
   END_VAR
   VAR_OUTPUT
      Motor1 : Bool;            // motor 1 running (M0.3)
      Motor2 : Bool;            // motor 2 running (M0.4)
   END_VAR
   VAR
      statRun : Bool;           // run latch
      statPermit : Bool;        // run permitted
      rtStart : R_TRIG;         // rising edge of the start button
      tonRun2 : TON_TIME;       // IEC on-delay timer
   END_VAR
BEGIN
    // Run is permitted when the normally-closed stop is closed and there is no fault
    #statPermit := #StopPermit AND NOT #Fault;

    // Latch on the start edge; stop or fault clears it immediately
    #rtStart(CLK := #Start);
    #statRun := (#rtStart.Q OR #statRun) AND #statPermit;

    // Motor 1 runs at once; motor 2 starts RunDelay after motor 1
    #Motor1 := #statRun;
    #tonRun2(IN := #Motor1,
             PT := #RunDelay);
    #Motor2 := #statRun AND #tonRun2.Q;
END_FUNCTION_BLOCK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And OB1, wiring it to the M area:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORGANIZATION_BLOCK "Main"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
BEGIN
    "DB_MotorSeq"(Start := %M0.0,
                  StopPermit := %M0.1,
                  Fault := %M0.2,
                  RunDelay := T#3s,
                  Motor1 =&amp;gt; %M0.3,
                  Motor2 =&amp;gt; %M0.4);
END_ORGANIZATION_BLOCK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compile: &lt;code&gt;state=Success, errorCount=0, warningCount=0&lt;/code&gt;, both &lt;code&gt;Main (OB1)&lt;/code&gt; and &lt;code&gt;FB_MotorSeq (FB1)&lt;/code&gt; cleaned.&lt;/p&gt;

&lt;p&gt;Two details worth reading in that code: the start is edge-triggered (&lt;code&gt;R_TRIG&lt;/code&gt;) — otherwise holding the button keeps it latched — and stop/fault runs through &lt;code&gt;#statPermit&lt;/code&gt;, a path that never touches the timer. That second one is exactly what the third scenario below checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How long each step took
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;health probe (attached, project, device count, prerequisites)&lt;/td&gt;
&lt;td&gt;&amp;lt; 1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;look up the CPU model in the hardware catalogue&lt;/td&gt;
&lt;td&gt;~1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;create the CPU (CPU 1516-3 PN/DP, FW V2.9)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.2 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;import the FB from SCL source&lt;/td&gt;
&lt;td&gt;11.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;import the instance DB and OB1&lt;/td&gt;
&lt;td&gt;0.6 s each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;compile&lt;/td&gt;
&lt;td&gt;1.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;create a virtual PLC and power it on&lt;/td&gt;
&lt;td&gt;~6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;download to S7-PLCSIM Advanced&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.6 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;run, write variables, read variables&lt;/td&gt;
&lt;td&gt;&amp;lt; 100 ms per call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One ordering constraint you cannot skip: &lt;strong&gt;import does not compile&lt;/strong&gt;. Right after import the block is not marked consistent, and even exporting it is refused (&lt;code&gt;Block is inconsistent (not compiled)&lt;/code&gt;). Import → compile → read back, in that order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three scenarios
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn8xm518wodfilfz7gk1u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn8xm518wodfilfz7gk1u.png" alt="Timing: start pulse, motor 1 runs immediately, motor 2 starts 3.12 s later, then stop or fault drops both within the same scan" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A green compile only says the code is acceptable to the PLC. These three runs say what it does.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Observed&lt;/th&gt;
&lt;th&gt;Read as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Start&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;M0.1=1&lt;/code&gt;, pulse &lt;code&gt;M0.0&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;motor 1 goes to 1 at once; &lt;strong&gt;motor 2 at +3.12 s&lt;/strong&gt;; both hold after the button is released&lt;/td&gt;
&lt;td&gt;delay is fed by "motor 1 running", not by the button&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;both running, &lt;code&gt;M0.1 → 0&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;both drop &lt;strong&gt;within one sample&lt;/strong&gt;; restoring &lt;code&gt;M0.1=1&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; restart them&lt;/td&gt;
&lt;td&gt;the stop path is not gated by the timer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fault&lt;/td&gt;
&lt;td&gt;both running, &lt;code&gt;M0.2 → 1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;both drop &lt;strong&gt;within 13 ms (one scan)&lt;/strong&gt;; clearing the fault does not restart&lt;/td&gt;
&lt;td&gt;fault is an independent cut-out, and it does not self-heal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 3.12 s figure is 120 ms off the 3 s setting; that is one polling quantum (150 ms) plus scan jitter, not a logic error. The stop and fault cases are the ones worth insisting on: they are invisible at compile time, hard to reproduce on a machine, and they are what decides whether an operator has to press reset afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond "Success"
&lt;/h2&gt;

&lt;p&gt;A download that returns success still deserves a look at what actually landed. We exported the three blocks as SimaticML and checked three things: OB1 really calls the instance DB with those M addresses, the FB uses an IEC &lt;code&gt;TON&lt;/code&gt; and no stray instance, and the block metadata says &lt;code&gt;consistent&lt;/code&gt;. Sizes for the record: &lt;code&gt;Main&lt;/code&gt; 7,766 B, &lt;code&gt;FB_MotorCtrl&lt;/code&gt; 15,978 B (the earlier single-motor variant), &lt;code&gt;DB_MotorCtrl&lt;/code&gt; 6,903 B.&lt;/p&gt;

&lt;p&gt;The gateway's own notes make one behaviour explicit: &lt;strong&gt;nothing is persisted until you save&lt;/strong&gt;. Imports, hardware writes, deletes and compiles all live in memory. That is what makes "try it, look at the result, throw it away" safe — and it is also the trap: if you never save and assume you are done, closing TIA Portal loses the lot. Our habit: verify first, save once, at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist before you let it touch a real project
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read-only first.&lt;/strong&gt; Have it read the tree, blocks and cross-references; confirm it understood your project before it writes anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview writes for a week.&lt;/strong&gt; Compare its previewed diff against what a person would change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read back after every write.&lt;/strong&gt; Export or compare hashes; if it does not match, treat it as not written.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One project, one session, one writer at a time.&lt;/strong&gt; The Openness side is serial; parallel agents produce half-finished work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compile as a gate.&lt;/strong&gt; Passing does not mean correct, but failing must block the commit. This is the only gate you can fully automate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archive before changing.&lt;/strong&gt; VCI or Git, exported as diffable text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep an audit trail.&lt;/strong&gt; Who, when, in which mode, which object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not hand download or go-online to an unattended agent.&lt;/strong&gt; Man in the loop, one step at a time, with a rollback plan.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the rule that outranks all of them: &lt;strong&gt;AI-generated SCL gets a human review&lt;/strong&gt;, especially safety logic (emergency stop, interlocks, F-programs). The tool guarantees it was written, not that it is right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Versions and boundaries
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;What we ran&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TIA Portal&lt;/td&gt;
&lt;td&gt;V19 / V20 supported; this run was V19 with Openness 19.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simulator&lt;/td&gt;
&lt;td&gt;S7-PLCSIM Advanced 6.0, Softbus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU firmware&lt;/td&gt;
&lt;td&gt;matters more than expected — a V1.1 device was refused by the simulator, V2.9 worked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;the gateway runs on the same Windows machine as TIA Portal; the agent can be anywhere that can reach its HTTP port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool count&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The deployment line surprises people: the &lt;strong&gt;gateway&lt;/strong&gt; has to be on the TIA machine, the &lt;strong&gt;agent&lt;/strong&gt; does not. Every call in this article was made from a different computer over HTTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it yourself
&lt;/h2&gt;

&lt;p&gt;You need two things: a Windows machine with TIA Portal V19 or V20, and the gateway installed on that same machine. It publishes its endpoint on &lt;code&gt;http://localhost:8080/&lt;/code&gt;, and the client side is the one line of config from earlier.&lt;/p&gt;

&lt;p&gt;Setup and version support: &lt;a href="https://tia.hubwiz.com/tia-link?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=claude-code-tia-portal" rel="noopener noreferrer"&gt;tia.hubwiz.com/tia-link&lt;/a&gt;. If you want the agent to understand the project before it starts writing, &lt;a href="https://tia.hubwiz.com/tia-context?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=claude-code-tia-portal" rel="noopener noreferrer"&gt;TIA-Context&lt;/a&gt; feeds it blocks, tag tables and call relationships.&lt;/p&gt;

&lt;p&gt;Once the loop works, the mistake to avoid is asking for too much at once. One block, one change with a test case in front of it, beats "refactor this project" every time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Further reading (in Chinese): &lt;a href="https://www.hubwiz.com/blog/tia-portal-openness-mcp-write-guards/?utm_source=devto&amp;amp;utm_medium=syndication" rel="noopener noreferrer"&gt;AI changes a TIA Portal program: write guards and boundaries&lt;/a&gt; · &lt;a href="https://www.hubwiz.com/blog/opcua-mcp-hands-on/?utm_source=devto&amp;amp;utm_medium=syndication" rel="noopener noreferrer"&gt;OPC UA MCP hands-on&lt;/a&gt; · &lt;a href="https://www.hubwiz.com/blog/ai-industrial-stack/?utm_source=devto&amp;amp;utm_medium=syndication" rel="noopener noreferrer"&gt;AI × industrial / PLC guide&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Timings and error strings come from the 2026-10-27 run. AI-generated PLC code needs human review before it goes anywhere near production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>programming</category>
      <category>automation</category>
    </item>
    <item>
      <title>Three Kinds of LAD-to-SCL Tooling, and Why Only One Belongs in a Real Project</title>
      <dc:creator>tia-forge</dc:creator>
      <pubDate>Tue, 27 Oct 2026 03:37:16 +0000</pubDate>
      <link>https://dev.to/tiaforge/three-kinds-of-lad-to-scl-tooling-and-why-only-one-belongs-in-a-real-project-6l0</link>
      <guid>https://dev.to/tiaforge/three-kinds-of-lad-to-scl-tooling-and-why-only-one-belongs-in-a-real-project-6l0</guid>
      <description>&lt;p&gt;&lt;strong&gt;"Which software converts ladder to SCL?" is the wrong question&lt;/strong&gt;, and not because the tools are bad. It's because three completely different classes of tool get filed under one name, and their failure modes have almost nothing in common. Comparing them on "accuracy" is comparing nothing.&lt;/p&gt;

&lt;p&gt;Here they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The three classes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;th&gt;What it actually reads&lt;/th&gt;
&lt;th&gt;Ceiling&lt;/th&gt;
&lt;th&gt;Where it fails&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Built into TIA Portal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LAD ↔ FBD, LAD → STL&lt;/td&gt;
&lt;td&gt;The blocks in your project&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Cannot produce SCL&lt;/strong&gt; — only FBD/STL&lt;/td&gt;
&lt;td&gt;Not SCL. STL is assembler-ish: it runs, but nobody can review it, and diffs show nothing semantic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;In-project add-in&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reads LAD blocks out of the project via TIA Openness / SimaticML, emits SCL source&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;The project itself&lt;/strong&gt; — interfaces, tag tables, instance DBs, network order, every part and every wire&lt;/td&gt;
&lt;td&gt;Whole-project coverage; anything its rules cannot handle must be surfaced as "review required"&lt;/td&gt;
&lt;td&gt;Semantic structures its rule set does not cover (edge memory bits, jumps, MCR, multiple coils) — the answer must be "flag it", never "guess"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Screenshot / paste-a-text converters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Paste an image or a text blob, get code back&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Pixels or characters&lt;/strong&gt; — no tags, no instances, no network order&lt;/td&gt;
&lt;td&gt;One small network; demos and learning&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;The information was lost on the input side.&lt;/strong&gt; This is where "error rate is huge" comes from&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the widely repeated claim that &lt;em&gt;third-party converters are inaccurate&lt;/em&gt; is true of class 3 and false of class 2. Class 2 and class 1 read the same data — the only difference is who generates the SCL.&lt;/p&gt;

&lt;p&gt;One more thing to retire: &lt;strong&gt;there is no "convert everything in one click"&lt;/strong&gt;. What decides the outcome is not the software; it's the ratio, in &lt;em&gt;your&lt;/em&gt; project, between structures that map mechanically and structures that only look like they do.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Fourteen structures, judged one by one
&lt;/h2&gt;

&lt;p&gt;The table below is derived from the semantics of the two languages, not from a sample — which is why every row also says how you verify it yourself instead of taking my word for it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Structure&lt;/th&gt;
&lt;th&gt;What conversion can do&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;What you must do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Series/parallel contacts&lt;/td&gt;
&lt;td&gt;Boolean expression&lt;/td&gt;
&lt;td&gt;✅ Safe&lt;/td&gt;
&lt;td&gt;Check parentheses and operator precedence; don't rely on default associativity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Plain coil&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x := expr;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ Safe&lt;/td&gt;
&lt;td&gt;Confirm it isn't a multiple coil (row 10)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;(S)&lt;/code&gt; / Reset &lt;code&gt;(R)&lt;/code&gt; coil&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IF cond THEN x := TRUE/FALSE; END_IF;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ Converts — &lt;strong&gt;inspect the output&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;When the condition is false the value must &lt;strong&gt;hold&lt;/strong&gt;; an unconditional assignment has already broken the semantics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RS&lt;/code&gt; / &lt;code&gt;SR&lt;/code&gt; boxes&lt;/td&gt;
&lt;td&gt;Calls the corresponding FB&lt;/td&gt;
&lt;td&gt;⚠️ Converts, &lt;strong&gt;trivially easy to pick the wrong one&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;They look identical in LAD; the difference only shows when set and reset are true in the same scan. Picking wrong does not error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Positive / negative edge contacts&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;R_TRIG&lt;/code&gt; / &lt;code&gt;F_TRIG&lt;/code&gt; instance, or an explicit last-scan compare&lt;/td&gt;
&lt;td&gt;⚠️ &lt;strong&gt;The most commonly broken row&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;The edge bit must live in &lt;strong&gt;static&lt;/strong&gt; memory. Put it in TEMP and it is rebuilt every scan — the edge disappears, silently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TON&lt;/code&gt; / &lt;code&gt;TOF&lt;/code&gt; / &lt;code&gt;TP&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Calls the IEC timer FB&lt;/td&gt;
&lt;td&gt;⚠️ Converts; instance matters&lt;/td&gt;
&lt;td&gt;Instance must sit in STATIC or a multi-instance DB; check PT type/units, and whether "wrapped in an IF" matches the original EN behaviour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CTU&lt;/code&gt; / &lt;code&gt;CTD&lt;/code&gt; / &lt;code&gt;CTUD&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Calls the counter FB&lt;/td&gt;
&lt;td&gt;⚠️ Same as above&lt;/td&gt;
&lt;td&gt;Instance location, preset, and the priority of reset vs. load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Multi-instance FB call with parameters&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#myFB(IN1 := ..., IN2 := ...);&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;⚠️ Converts, &lt;strong&gt;parameters get dropped&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;A missing pin does &lt;strong&gt;not&lt;/strong&gt; error — it just keeps its previous value, so the bug surfaces only in one specific operating case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;JMP&lt;/code&gt; / &lt;code&gt;LABEL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;Cannot convert&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;SCL has no &lt;code&gt;GOTO&lt;/code&gt;. Rewrite the control flow as a &lt;code&gt;CASE&lt;/code&gt; state machine or structured branching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Multiple coils (same variable written in several networks)&lt;/td&gt;
&lt;td&gt;Sequential assignments&lt;/td&gt;
&lt;td&gt;⚠️ Converts — &lt;strong&gt;result depends on order&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Make the write order explicit and check whether anything reads between the writes; which write gets read changes everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MCR&lt;/code&gt; (master control relay)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;Cannot convert&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Expand into an explicit condition chain: write out what the outputs do while the zone is disabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;EN&lt;/code&gt; / &lt;code&gt;ENO&lt;/code&gt; chain&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;EN&lt;/code&gt; → &lt;code&gt;IF&lt;/code&gt; wrapper&lt;/td&gt;
&lt;td&gt;⚠️ Half converts&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ENO&lt;/code&gt; routinely gets dropped → upstream fails, downstream keeps running. Check the return value or error bit explicitly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Implicit type conversion&lt;/td&gt;
&lt;td&gt;Explicit conversion functions&lt;/td&gt;
&lt;td&gt;⚠️ Converts — &lt;strong&gt;the boundaries move&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Re-check divide-by-zero, integer overflow, REAL precision, TIME and counter type limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Initialisation / first scan (OB100)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;❌ Must be manual&lt;/td&gt;
&lt;td&gt;Start-up block, static initial values, first-scan behaviour. These only fail after a power cycle&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern is easy to state: &lt;strong&gt;connection structure converts; state and timing do not.&lt;/strong&gt; Rows 5, 6, 8 and 12 are all "the picture carried this for you, and the text won't".&lt;/p&gt;

&lt;p&gt;And none of the fourteen will produce a compile error. Which is the real reason "it compiles" proves nothing about a LAD→SCL migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The brand question, briefly
&lt;/h2&gt;

&lt;p&gt;Search suggestions often put "Siemens" and "Mitsubishi" in the same sentence. There is no universal converter, and there cannot be: the pictures look alike, but three things differ underneath — device addressing (&lt;code&gt;I/Q/M/DB&lt;/code&gt; vs &lt;code&gt;X/Y/M/D&lt;/code&gt;), instruction set and function-block semantics, and how edges and timers are implemented. A TIA Portal add-in reads TIA Portal projects. Across vendors it's a re-implementation with a fresh I/O mapping, not a conversion.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. How to verify, without trusting any tool
&lt;/h2&gt;

&lt;p&gt;Three steps, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Compare interfaces and tags.&lt;/strong&gt; IN / OUT / IN_OUT / STATIC / TEMP, one by one; symbol names, types, retentivity. This alone kills most "looks like it runs" illusions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the same inputs twice.&lt;/strong&gt; Feed the original LAD block and the new SCL block an identical input sequence, then compare output timing and final values. Minimum coverage: first scan after power-up, start alone, stop alone, both at once, interlock opened mid-run, timer at the threshold, counter at the preset, manual/auto switchover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Localise the divergence to a network.&lt;/strong&gt; The first step where they differ maps back to one row of the table above — then it's either "converted but unchecked" or "should have been rewritten by hand".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 2 needs two environments that can run. With TIA Portal and PLCSIM you have them. Without TIA Portal you can still run the SCL side on a cloud simulator that executes scan cycles and returns final values plus PASS/FAIL — I used &lt;a href="https://tia.hubwiz.com/sim/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=lad-to-scl-tooling" rel="noopener noreferrer"&gt;PLC Cloud Sim&lt;/a&gt; for regression runs, and its MCP endpoint lets an agent replay the same case set. To be precise about the boundary: that verifies &lt;strong&gt;logical equivalence&lt;/strong&gt; only. It does not replace PLCSIM timing detail, hardware diagnostics, or commissioning on the machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. So which one
&lt;/h2&gt;

&lt;p&gt;For a real migration, class 2 for the bulk, your own judgement for rows 3–14, and class 3 nowhere near a project. Concretely, the add-in I use is &lt;a href="https://tia.hubwiz.com/lad2scl/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=lad-to-scl-tooling" rel="noopener noreferrer"&gt;LAD2SCL&lt;/a&gt; — a TIA Portal add-in by Hubwiz that reads LAD blocks from the open project and emits SCL. In the interest of not overselling: &lt;strong&gt;only the V19 installer has run on a real machine so far&lt;/strong&gt;; V17/V18/V20/V21 are advertised on the page but have not been verified version by version, and the download buttons for them currently say "not released yet".&lt;/p&gt;

&lt;p&gt;The longer Chinese version, with the migration workflow and the pre-release checklist, is &lt;a href="https://www.hubwiz.com/blog/lad-to-scl-migration-guide-tia-portal/" rel="noopener noreferrer"&gt;here&lt;/a&gt; (in Chinese).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The 14-row table is a semantic analysis, not a benchmark. If you find a row that's wrong for your project, that's more useful than a star — tell me which one and what it did.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>plc</category>
      <category>automation</category>
      <category>programming</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Empty Folder to MP4 in 162 Seconds: What a Coding Agent Can't Do With Video</title>
      <dc:creator>tia-forge</dc:creator>
      <pubDate>Mon, 26 Oct 2026 04:29:50 +0000</pubDate>
      <link>https://dev.to/tiaforge/empty-folder-to-mp4-in-162-seconds-what-a-coding-agent-cant-do-with-video-2bo7</link>
      <guid>https://dev.to/tiaforge/empty-folder-to-mp4-in-162-seconds-what-a-coding-agent-cant-do-with-video-2bo7</guid>
      <description>&lt;p&gt;&lt;strong&gt;The one-paragraph version.&lt;/strong&gt; There are two unrelated technologies behind the phrase "AI video". One paints pixels from a prompt (Veo, Gemini Omni, Sora). The other renders a &lt;strong&gt;program&lt;/strong&gt; into frames — a video is a React component tree or an HTML file, and the output is deterministic. Only the second one is something a coding agent can own end to end: it writes files, reads type errors, renders, looks at frames. We ran the whole loop on a clean machine and timed it. Total to first MP4: &lt;strong&gt;about three minutes&lt;/strong&gt;. Then we hit the wall that every article about this topic skips — the agent has no way to know whether what it rendered is any good.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure.&lt;/strong&gt; All commands and timings below were run by us on 2026-10-26 (Windows 11, 8-core CPU, Node 24.21.0, npm 11.19.0, Remotion 4.0.538). Everything else — license terms, skill counts, design rationale — is quoted from the vendor's own docs and is linked in Sources. We did not benchmark cloud rendering, and we did not run a paid generative model.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Two things are called "AI video"
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Prompt → pixels&lt;/th&gt;
&lt;th&gt;Code → frames&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Examples&lt;/td&gt;
&lt;td&gt;Veo 3.1, Gemini Omni Flash, Sora&lt;/td&gt;
&lt;td&gt;Remotion, HyperFrames&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unit of authoring&lt;/td&gt;
&lt;td&gt;A sentence&lt;/td&gt;
&lt;td&gt;A file in git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output for the same input&lt;/td&gt;
&lt;td&gt;Different every time (sampling)&lt;/td&gt;
&lt;td&gt;Byte-identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editing a detail&lt;/td&gt;
&lt;td&gt;Re-prompt, hope&lt;/td&gt;
&lt;td&gt;Change one number, re-render&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What the model must know&lt;/td&gt;
&lt;td&gt;Cinematography, physics, faces&lt;/td&gt;
&lt;td&gt;The API you gave it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where an agent fits&lt;/td&gt;
&lt;td&gt;Calls an API&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Owns the whole repo&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The distinction matters because the two have wildly different failure modes. A diffusion model fails by hallucinating a sixth finger; a render pipeline fails by throwing &lt;code&gt;TypeError: undefined is not a function&lt;/code&gt;. Coding agents are good at the second kind of failure and useless at the first.&lt;/p&gt;

&lt;p&gt;That is the whole reason this niche got hot in 2026: Remotion shipped &lt;strong&gt;Agent Skills&lt;/strong&gt; in January, HeyGen open-sourced &lt;strong&gt;HyperFrames&lt;/strong&gt; in April, and both are pure text — which is the only substrate a coding agent actually understands.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What "video as code" means mechanically
&lt;/h2&gt;

&lt;p&gt;The mental model is one line: &lt;strong&gt;a frame is a pure function of time.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCurrentFrame&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// 0, 1, 2, … 149&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spring&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;damping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no timeline, no keyframe UI, no video editor. You write a component that, given a frame number, returns what that frame looks like. The renderer then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;bundles the project (Remotion) or loads &lt;code&gt;index.html&lt;/code&gt; as-is (HyperFrames),&lt;/li&gt;
&lt;li&gt;opens headless Chrome and &lt;strong&gt;seeks&lt;/strong&gt; each timestamp,&lt;/li&gt;
&lt;li&gt;screenshots the frame,&lt;/li&gt;
&lt;li&gt;pipes the frames into FFmpeg.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both frameworks do this. HyperFrames' README states the property that makes it usable in CI: &lt;em&gt;"The renderer seeks each frame in headless Chrome and encodes the result with FFmpeg, so the same input produces the same video."&lt;/em&gt; Remotion's pitch is the same idea with a React component tree as the authoring surface.&lt;/p&gt;

&lt;p&gt;Why agents like it: the artifact is TypeScript or HTML, the feedback loop is a compiler, and the diff is reviewable. &lt;code&gt;git diff&lt;/code&gt; on a video is a strange sentence that is now literally true.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The demo, with real numbers
&lt;/h2&gt;

&lt;p&gt;Four commands, no editor, no API key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-video@latest &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="nt"&gt;--blank&lt;/span&gt; demo   &lt;span class="c"&gt;# scaffold&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;demo
npx skills add remotion-dev/skills &lt;span class="nt"&gt;-a&lt;/span&gt; claude-code &lt;span class="nt"&gt;-y&lt;/span&gt;   &lt;span class="c"&gt;# 12 skills → .claude/skills&lt;/span&gt;
npx remotion render MyComp out/video.mp4      &lt;span class="c"&gt;# render&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Measured, on the machine above:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Wall time&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create-video --yes --blank&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 m 56 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;almost entirely &lt;code&gt;npm install&lt;/code&gt;; dominates the clock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;skills add …&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.0 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;12 skill folders written&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;first &lt;code&gt;remotion render&lt;/code&gt; (5 s @ 1080p)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;39.9 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;includes downloading headless Chrome&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;same render, warm&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11.7 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;150 frames, 1080p30, &lt;code&gt;--concurrency&lt;/code&gt; default = cores/2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60 s @ 1080p&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;68.7 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1800 frames; ≈ 26 frames/s of output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60 s @ 1080p, &lt;code&gt;--concurrency=2&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;84.4 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;forcing 2 workers is 23% slower on 8 cores&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things fall out of that table. First, &lt;strong&gt;render time is linear in duration&lt;/strong&gt;: 150 frames took 11.7 s, 1800 frames took 68.7 s — the same ~26 frames per second of output either way (we only measured one composition, so treat "per frame" as roughly constant, not proven constant). Second, the "three minutes to your first video" claim is real, but it is &lt;em&gt;scaffold&lt;/em&gt; time, not &lt;em&gt;authoring&lt;/em&gt; time: 116 s of install + 6 s of skills + 40 s of cold render ≈ 162 s, and &lt;strong&gt;the npm install is two-thirds of it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A note on the install command, because half the articles online get it wrong. Remotion's own docs now say &lt;code&gt;npx remotion skills add&lt;/code&gt;. The generic installer also works: &lt;code&gt;npx skills add remotion-dev/skills -a claude-code -y&lt;/code&gt;. &lt;strong&gt;They write to different directories&lt;/strong&gt; — the first put 12 skills in &lt;code&gt;.agents/skills/&lt;/code&gt; and created a &lt;code&gt;skills-lock.json&lt;/code&gt;; the second put 12 skills in &lt;code&gt;.claude/skills/&lt;/code&gt;. If your agent "can't see the skills", check which directory got them before you debug anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. What the skills actually cost you in context
&lt;/h2&gt;

&lt;p&gt;People assume installing agent skills means stuffing a manual into the prompt. It doesn't, and the numbers are worth knowing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Skills installed&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontmatter (&lt;code&gt;name&lt;/code&gt; + &lt;code&gt;description&lt;/code&gt;) of all 12&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1,031 chars ≈ 290 tokens&lt;/strong&gt; — this is the always-visible part&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full &lt;code&gt;SKILL.md&lt;/code&gt; bodies, all 12&lt;/td&gt;
&lt;td&gt;43 KB ≈ 12k tokens, &lt;strong&gt;read on demand&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Support files (docs, example &lt;code&gt;.tsx&lt;/code&gt;, SVGs, map data)&lt;/td&gt;
&lt;td&gt;285 files / 2.3 MB — opened only if the agent asks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The biggest single skill is &lt;code&gt;remotion-best-practices&lt;/code&gt;, whose &lt;code&gt;SKILL.md&lt;/code&gt; is a 4.2 KB router: it tells the agent which of the other eleven references to load for the task at hand. That's the whole design — pay ~300 tokens up front, buy 12k tokens of API knowledge. It also means the failure mode is silent: if the router doesn't fire, the agent writes Remotion from memory and you get plausible-looking code that renders a black screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The cost model nobody writes down
&lt;/h2&gt;

&lt;p&gt;Two numbers matter: license and compute. Compute you can measure (§3). License is where the surprise is.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Remotion&lt;/th&gt;
&lt;th&gt;HyperFrames&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Source-available &lt;strong&gt;Remotion License&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Apache 2.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;Individuals and companies of &lt;strong&gt;up to 3 people&lt;/strong&gt;; unlimited commercial use&lt;/td&gt;
&lt;td&gt;No per-render fees, no commercial-use threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paid tier&lt;/td&gt;
&lt;td&gt;Company License: &lt;strong&gt;Creators $25/seat/mo&lt;/strong&gt;, or &lt;strong&gt;Automators $0.01/render, $100/mo minimum&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render engine&lt;/td&gt;
&lt;td&gt;Headless Chrome + FFmpeg&lt;/td&gt;
&lt;td&gt;Headless Chrome + FFmpeg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authoring&lt;/td&gt;
&lt;td&gt;React components&lt;/td&gt;
&lt;td&gt;HTML + CSS + seekable animation (GSAP, Lottie, Three.js, WAAPI…)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The threshold is the thing to read twice. A three-person studio is free. Hire a fourth person and you are, by the license's own terms, in Company License territory — and if you're building a product that renders videos for &lt;em&gt;other people&lt;/em&gt;, that's the Automators plan at $0.01/render with a $100/month floor. For a tool that renders 50,000 personalized clips a month, $500 is fine. For a side project that renders nine, it is not.&lt;/p&gt;

&lt;p&gt;HyperFrames is the escape hatch: Apache 2.0, same rendering mechanism, different authoring bet (HTML instead of React). Its README is blunt about the trade-off — &lt;em&gt;"Remotion's bet is React components; HyperFrames' bet is plain HTML that humans and agents can both write easily."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For the token side, one datapoint rather than a claim: NiharDaily's October 2026 hands-on reports &lt;strong&gt;$0.13&lt;/strong&gt; in model cost for a first prompt-to-MP4 run in Claude Code. Treat that as an order of magnitude, not a benchmark — it depends entirely on how many times the agent renders, reads a frame, and iterates.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Where it actually breaks
&lt;/h2&gt;

&lt;p&gt;This is the section that decides whether you ship or abandon, and it's the part the tutorials skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The verification gap.&lt;/strong&gt; &lt;em&gt;"It compiles" is not "it looks good."&lt;/em&gt; An agent can run &lt;code&gt;remotion render&lt;/code&gt; and get a green exit code from a composition where the text is white on white, the logo is cropped, or the animation finished at frame 12. Digital Applied calls this the hardest self-verification problem in the agentic toolchain, and they're right: there is no unit test for "does this look right", and the visual-regression tooling that exists measures pixel differences, not intent.&lt;/p&gt;

&lt;p&gt;Practical workarounds, in order of usefulness:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Give the agent a component library, not a blank canvas.&lt;/strong&gt; Improvised motion is exactly where AI video looks cheap. If it composes &lt;code&gt;&amp;lt;LowerThird/&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;StatCard/&amp;gt;&lt;/code&gt; instead of inventing springs, the floor on quality rises enormously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make it render stills, not videos, while iterating.&lt;/strong&gt; &lt;code&gt;npx remotion still MyComp out/f.png --frame=45&lt;/code&gt; took &lt;strong&gt;6.7 s&lt;/strong&gt;; a full 5-second render takes 11.7 s and a 60-second one takes 68.7 s. Bundling is a fixed ~5 s cost, so the longer your composition, the more the still saves — and it catches most layout errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have it read the frame back.&lt;/strong&gt; Agents with vision can open &lt;code&gt;out/frame.png&lt;/code&gt;. Without this, you are the verification loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Everything else that bites:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your first render is not your render time.&lt;/strong&gt; Headless Chrome download alone is most of the 40 s. Budget for it in CI warm-up, not in your demo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonts and media load asynchronously.&lt;/strong&gt; A frame screenshot that races an image decode is a flaky test you'll spend a day on. Local assets, not CDN URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long videos stay linear.&lt;/strong&gt; 10 minutes at 1080p is ~11 minutes of render on this machine. That's fine locally, unacceptable in a request handler — this is what Remotion Lambda and HyperFrames' Lambda path exist for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The license threshold is a headcount trigger, not a usage trigger.&lt;/strong&gt; Track it before legal does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-generated is visible.&lt;/strong&gt; If you can tell a video was made from a prompt, the tool didn't fail; the brief did.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. When to use which
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Motion graphics, explainers, charts, lower-thirds, data-driven or parameterized clips&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Video as code&lt;/strong&gt; (Remotion / HyperFrames)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Photoreal footage, people, b-roll, "a rainy street in Osaka"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;A generative model&lt;/strong&gt; (Veo 3.1 / Gemini Omni Flash), then assemble in code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A talking head&lt;/td&gt;
&lt;td&gt;An avatar product; neither of the above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000 personalized variants of one template&lt;/td&gt;
&lt;td&gt;Video as code + Lambda; this is the killer use case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One 30-second promo, once&lt;/td&gt;
&lt;td&gt;Neither. Open an editor.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest framing: video as code is not a cheaper way to make the video you were going to make in Premiere. It's a way to make videos that &lt;strong&gt;didn't exist as a category before&lt;/strong&gt; — one per customer, one per data update, one per pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Checklist before you commit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Decide the authoring surface first (React vs HTML). Migrating later is a rewrite, not a refactor.&lt;/li&gt;
&lt;li&gt;[ ] Read the license terms against your current headcount, not next year's.&lt;/li&gt;
&lt;li&gt;[ ] Ship a component library on day one; never let the agent invent motion.&lt;/li&gt;
&lt;li&gt;[ ] Use &lt;code&gt;remotion still&lt;/code&gt; (or &lt;code&gt;render --frames=0-59&lt;/code&gt;) for iteration; full render only on merge.&lt;/li&gt;
&lt;li&gt;[ ] Keep media local; pin fonts.&lt;/li&gt;
&lt;li&gt;[ ] Put a human on frame review — that loop is the product.&lt;/li&gt;
&lt;li&gt;[ ] Measure render seconds per output second on &lt;em&gt;your&lt;/em&gt; box; ours was 26 fps of output at 1080p.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Remotion docs — &lt;em&gt;Coding Agents&lt;/em&gt;: &lt;a href="https://www.remotion.dev/docs/ai/coding-agents" rel="noopener noreferrer"&gt;https://www.remotion.dev/docs/ai/coding-agents&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Remotion docs — &lt;em&gt;Agent Skills&lt;/em&gt; (12 skills, installers): &lt;a href="https://www.remotion.dev/docs/ai/skills" rel="noopener noreferrer"&gt;https://www.remotion.dev/docs/ai/skills&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Remotion — license and pricing: &lt;a href="https://www.remotion.pro/license" rel="noopener noreferrer"&gt;https://www.remotion.pro/license&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;HyperFrames — repository and README (determinism, Apache 2.0, comparison table): &lt;a href="https://github.com/heygen-com/hyperframes" rel="noopener noreferrer"&gt;https://github.com/heygen-com/hyperframes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;HyperFrames — docs: &lt;a href="https://hyperframes.heygen.com" rel="noopener noreferrer"&gt;https://hyperframes.heygen.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;HeyGen Research — &lt;em&gt;HTML to Video Was Not Easy: Here's How We Solved It&lt;/em&gt;, James Russo, 2026-06-22: &lt;a href="https://www.heygen.com/research/html-to-video" rel="noopener noreferrer"&gt;https://www.heygen.com/research/html-to-video&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Digital Applied — &lt;em&gt;Video as Code: Remotion and the Agent Feedback Gap&lt;/em&gt;, 2026-08-11: &lt;a href="https://www.digitalapplied.com/blog/video-as-code-remotion-agentic-generation-2026" rel="noopener noreferrer"&gt;https://www.digitalapplied.com/blog/video-as-code-remotion-agentic-generation-2026&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NiharDaily — &lt;em&gt;Claude Code and Remotion: install the skill, render a video&lt;/em&gt;, 2026-10-17: &lt;a href="https://www.nihardaily.com/posts/claude-code-and-remotion-install-the-skill-render-a-video" rel="noopener noreferrer"&gt;https://www.nihardaily.com/posts/claude-code-and-remotion-install-the-skill-render-a-video&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;snapcn — &lt;em&gt;Claude Code + Remotion&lt;/em&gt;, 2026-09-24: &lt;a href="https://snapcn.dev/claude-code-remotion-videos" rel="noopener noreferrer"&gt;https://snapcn.dev/claude-code-remotion-videos&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;VEED — &lt;em&gt;open-edit&lt;/em&gt;, agent-driven editing pipeline: &lt;a href="https://github.com/veedstudio/open-edit" rel="noopener noreferrer"&gt;https://github.com/veedstudio/open-edit&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further reading (in Chinese)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/generative-media-stack/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=video-as-code-coding-agents" rel="noopener noreferrer"&gt;多模态生成完全指南 —— image / video / audio hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/building-ai-generated-video-with-remotion/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=video-as-code-coding-agents" rel="noopener noreferrer"&gt;用 Remotion 构建 AI 生成视频 —— the "typed catalog" architecture that complements this one&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Commands and timings are our own (2026-10-26). Product claims are attributed above. Chinese original: &lt;a href="https://www.hubwiz.com/blog/video-as-code-coding-agents/" rel="noopener noreferrer"&gt;hubwiz&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your coding agents don't need more messages — they need a handoff contract</title>
      <dc:creator>tia-forge</dc:creator>
      <pubDate>Sun, 25 Oct 2026 04:56:58 +0000</pubDate>
      <link>https://dev.to/tiaforge/your-coding-agents-dont-need-more-messages-they-need-a-handoff-contract-3lna</link>
      <guid>https://dev.to/tiaforge/your-coding-agents-dont-need-more-messages-they-need-a-handoff-contract-3lna</guid>
      <description>&lt;p&gt;I run several coding agents at once. The part that keeps costing me time is not the code they write — it is the note I hand from one to the next.&lt;/p&gt;

&lt;p&gt;When I write that note by hand, I write what hurts: &lt;em&gt;"CSV export loses the filters."&lt;/em&gt; That sentence is accurate and completely useless to an executor. It does not say which revision to start from, which command reproduces it, what output counts as correct, or who is allowed to say it is done.&lt;/p&gt;

&lt;p&gt;That is the whole thesis of this post, and it comes from four October 2026 write-ups by people who hit the same wall. Everything factual below is theirs; I reproduced none of it, so treat the numbers as claims with sources. Sources and disclosures are at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  A ticket describes the symptom. A contract describes the job.
&lt;/h2&gt;

&lt;p&gt;The author of the first write-up puts it in one line: a ticket can accurately describe a bug while leaving the executor unable to start.&lt;/p&gt;

&lt;p&gt;His framing is more useful than a tool comparison, because it applies before any tool enters the picture: &lt;em&gt;compare responsibilities, not labels.&lt;/em&gt; A task tracker can be part of a well-managed execution system. An agent runner can be badly managed. The difference is whether the handoff specifies four things.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;The question it must answer&lt;/th&gt;
&lt;th&gt;How it usually gets faked&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Which exact starting revision? Which behavior, constraints, allowed files?&lt;/td&gt;
&lt;td&gt;"Just change it on main"; a long conversation instead of a short, current spec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runner&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Who is the authorized operator? Is the environment actually ready?&lt;/td&gt;
&lt;td&gt;Naming an agent when nobody installed the deps or can grant repo access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Evidence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Patch revision, commands actually run, results, and &lt;strong&gt;untested cases&lt;/strong&gt;?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"done"&lt;/code&gt; — a state label, not a verification record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Acceptance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Who decides the result satisfies the requirement?&lt;/td&gt;
&lt;td&gt;The executor quietly swapping the requirement for an easier reading&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the asymmetry: three of these four are things an agent &lt;em&gt;cannot&lt;/em&gt; invent for itself. Acceptance in particular has to be a person or a role that is not the implementer, which is why "the agent said it was done" is not a verification story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The template
&lt;/h2&gt;

&lt;p&gt;The original template is for a CSV repair. Here it is generalized, with the author's own warning intact: values in angle brackets must come from the real project, and &lt;strong&gt;command names must not be invented&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s"&gt;Preserve the selected filters in CSV export&lt;/span&gt;
&lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;&amp;lt;authorized repository&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;base_revision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;verified commit&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;reproduction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;Select status=open, then export CSV&lt;/span&gt;
  &lt;span class="na"&gt;expected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s"&gt;Export contains exactly the rows matching the filter&lt;/span&gt;
  &lt;span class="na"&gt;fixtures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s"&gt;&amp;lt;sanitized fixture path&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;Export query and focused regression coverage&lt;/span&gt;
  &lt;span class="na"&gt;excluded&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Authentication, schema migrations, unrelated UI changes&lt;/span&gt;
&lt;span class="na"&gt;execution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s"&gt;&amp;lt;developer or authorized agent operator&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;setup_command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;verified command from this repository&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;test_command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;&amp;lt;verified command from this repository&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;Existing authorized development access only&lt;/span&gt;
&lt;span class="na"&gt;delivery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;patch_revision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;required&lt;/span&gt;
  &lt;span class="na"&gt;commands_and_results&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;required&lt;/span&gt;
  &lt;span class="na"&gt;sample_output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;         &lt;span class="s"&gt;sanitized CSV from the fixture&lt;/span&gt;
  &lt;span class="na"&gt;untested_cases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;required, even when empty&lt;/span&gt;
&lt;span class="na"&gt;acceptance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;product_owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Confirms filtering semantics&lt;/span&gt;
  &lt;span class="na"&gt;engineer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s"&gt;Reviews implementation and regression evidence&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three rules ride along with it, and the second one is the one people break in a hurry:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Do not paste credentials into the contract.&lt;/strong&gt; The executor uses their own authorized tools and access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An unavailable environment is an execution blocker, not an invitation to share an account.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The template is a &lt;em&gt;proposed&lt;/em&gt; operating practice — not evidence that any platform fills in every field automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;untested_cases: required, even when empty&lt;/code&gt; deserves its own sentence. Empty and "none" are different claims: the first is missing information, the second is a checkable statement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one handoff actually looks like
&lt;/h2&gt;

&lt;p&gt;The walkthrough in the source is worth keeping whole, because it shows where a human belongs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The product owner builds a fixture: three records, two open and one closed. The expected filtered export is &lt;strong&gt;exactly two rows&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;An engineer confirms the behavior and points at the export path.&lt;/li&gt;
&lt;li&gt;The executor &lt;strong&gt;first reproduces the failure at the recorded revision&lt;/strong&gt;. If it does not reproduce, the next useful delivery is not a patch — it is the attempted command, environment details, and observed output. Editing before resolving that mismatch risks fixing a different problem.&lt;/li&gt;
&lt;li&gt;Only then: a scoped change, plus coverage for the filtered export. The &lt;strong&gt;unfiltered&lt;/strong&gt; export gets checked too, because repairing one path can silently narrow the other.&lt;/li&gt;
&lt;li&gt;The delivery attaches the patch revision and the real output of the verified test command.&lt;/li&gt;
&lt;li&gt;Two reviewers, separately: the engineer reads the diff and the regression evidence; the product owner looks at the sanitized sample and confirms that "filtered" means what users think it means.&lt;/li&gt;
&lt;li&gt;If either review fails, the same task records the reason and the next attempt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Honest note from the original: those are &lt;em&gt;proposed&lt;/em&gt; checks. No passing results were claimed. That restraint is part of the standard, not a weakness of the article.&lt;/p&gt;

&lt;h2&gt;
  
  
  The portable version: five fields, five sections of evidence
&lt;/h2&gt;

&lt;p&gt;The follow-up post targets a messier reality: one teammate uses a terminal agent, another uses an editor assistant. What transfers between them is &lt;strong&gt;the task contract&lt;/strong&gt; — inputs, allowed changes, required evidence, a reviewer. Conversation history is optional context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;reject invalid worker-count configuration before startup&lt;/span&gt;
&lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;config parser and focused parser tests&lt;/span&gt;
  &lt;span class="na"&gt;excluded&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deployment, credentials, queue architecture&lt;/span&gt;
&lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;revision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;record the real base commit&lt;/span&gt;
  &lt;span class="na"&gt;config_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;replace with the actual key&lt;/span&gt;
  &lt;span class="na"&gt;reproduction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supply a synthetic invalid value&lt;/span&gt;
&lt;span class="na"&gt;acceptance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;omitted value preserves the current default&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;valid positive integer preserves behavior&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;zero, negative, and non-integer values are rejected&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;error names the key without printing secret configuration&lt;/span&gt;
&lt;span class="na"&gt;stop_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;the parser contract conflicts with documented behavior&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;the change requires a production configuration update&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unrelated failures prevent a trustworthy comparison&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stop_when&lt;/code&gt; is the field I stole immediately. It gives the executor permission to stop &lt;em&gt;and&lt;/em&gt; a reason to record, which is exactly what you want from an autonomous worker that is about to guess.&lt;/p&gt;

&lt;p&gt;The evidence protocol is five lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before:      command, exit code, observed output, base revision
Change:      files and the reason each file is necessary
After:       same reproduction, focused tests, actual exit codes
Uncertainty: checks not run, and why
Review:      commit reference and named acceptance owner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two sentences from that post are worth pinning above a desk:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The model says validation is fixed" is a hypothesis. A command and its output are evidence that a check ran.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;…and the reviewer still has to judge whether that check covers the intended failure.&lt;/p&gt;

&lt;p&gt;Two gates that get skipped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eligibility comes before capability.&lt;/strong&gt; Access is a gate: the runner must already be authorized. A teammate with a strong model and no repository permission is not eligible just because someone could paste them a token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest non-results.&lt;/strong&gt; In the article's example the executor discovers that a worker count of zero &lt;em&gt;deliberately&lt;/em&gt; means automatic sizing. The correct move is to stop, return the fact, and revise the requirement — not to change working behavior to satisfy an invented test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pre-acceptance checks: verify the recorded revision, reproduce the named cases, look for accidental configuration disclosure in the error message, and confirm no deployment or unrelated cleanup slipped into the diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the handoff an object (and holding the writes)
&lt;/h2&gt;

&lt;p&gt;The third write-up takes the opposite route: no template, a document model.&lt;/p&gt;

&lt;p&gt;Its observation is blunt — passing messages between agents is straightforward; &lt;strong&gt;keeping track of requirements, decisions, results, validation, and unfinished work is not&lt;/strong&gt;. A research agent handing off to a coding agent cannot send a summary: the receiver needs the original requirements, the constraints, the decisions already made, the results, the open questions, and the next steps.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;Work Order&lt;/code&gt; in, &lt;code&gt;Work Report&lt;/code&gt; back, with one model and one set of validation rules behind three entry points — a TypeScript package, an MCP server (so Claude Code and friends can create/accept/report/review through tools), and a Python implementation that runs the &lt;strong&gt;same shared test cases&lt;/strong&gt; and reports the same findings for the same documents.&lt;/p&gt;

&lt;p&gt;The interesting part is the MCP gateway in the middle. The worker does not touch real tools directly; every call goes through the gateway, which &lt;strong&gt;enforces that order's allowed tools and budget on each call and records the call as evidence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Version 0.7 added a side-effect gate — staged tools:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiz30i10us7qgsjqija4h.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiz30i10us7qgsjqija4h.jpg" alt="Staged tools: the worker's reads run straight through the gateway, but its gmail/send call is held, logged, and answered with a placeholder; the worker lists it in the report; on approve the gateway runs it once, in order" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Reads pass through, writes are held, and only an approval makes them run — once. (Image: the source post at dev.to, rehosted.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Mechanics worth copying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reads run normally.&lt;/strong&gt; Only staged calls wait, so the worker can still research.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes are held.&lt;/strong&gt; The gateway counts and logs the call with its arguments but does not forward it; the worker gets a placeholder like &lt;code&gt;pending:gmail/send#3&lt;/code&gt; and lists it under &lt;code&gt;effects&lt;/code&gt; in its report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The manager decides.&lt;/strong&gt; Approve → the gateway runs the held calls once, in order. Send it back → they are dropped, and &lt;em&gt;nothing has to be undone because nothing ran&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four details the author says only showed up in real workflows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Partial approval.&lt;/strong&gt; Approve the work but run only chosen effects: &lt;code&gt;effects: ["gmail/send#3"]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependent effects.&lt;/strong&gt; A later held call can reference an earlier one — &lt;code&gt;{"$ref": "github/create_issue#3", "pointer": "/structuredContent/number"}&lt;/code&gt; — and the gateway fills it in at execution time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At most once, by reconciliation.&lt;/strong&gt; Before running anything, the gateway checks every effect in the report against what it actually logged. Mismatch → nothing runs. Re-running &lt;code&gt;--commit&lt;/code&gt; never double-sends, even after a crash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sub-agents cannot route around it.&lt;/strong&gt; A worker cannot hand a staged tool to a sub-agent: the sub-agent's writes would execute when the &lt;em&gt;worker&lt;/em&gt; approves, before the manager has seen anything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is also &lt;code&gt;progress()&lt;/code&gt; for in-flight notes and &lt;code&gt;trackRecord()&lt;/code&gt; — a worker's history of first-time approvals, rework, failures, expiries, and budget spent. Judge a worker by that, not by its self-report.&lt;/p&gt;

&lt;p&gt;Disclosure: the author is building these packages, so read it as "someone doing the thing", not neutral review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ledger: derive health, never store it
&lt;/h2&gt;

&lt;p&gt;Writing a contract solves &lt;em&gt;what&lt;/em&gt; to hand over. The Agent Board write-up solves the other half: whether you can still see the whole board afterwards.&lt;/p&gt;

&lt;p&gt;The problem statement is three questions you cannot answer when agents run in parallel: &lt;strong&gt;is it still working, is it stuck, and how much of the subscription is it burning?&lt;/strong&gt; The original requirement was blunter — one always-accurate page. The author then names why that is hard: this is a distributed systems problem. Agents start and die outside any single process, report asynchronously, and &lt;strong&gt;lie by omission when they crash&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;His answer: the board is a projection; the ledger is the only source of truth. Four invariants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Register before spawn.&lt;/strong&gt; The task row exists before the process does, so nothing started through the supported path can run unrecorded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown stays unknown.&lt;/strong&gt; No estimated percentages, no inferred exit codes, no guessed usage. A Claude session without a reliable PID shows &lt;code&gt;unknown&lt;/code&gt; and its Stop button is disabled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Derive, do not store, health.&lt;/strong&gt; Status is a fact written by a producer; health is computed at read time from status plus idle time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never block the agent.&lt;/strong&gt; Instrumentation fails open: a busy ledger drops an event rather than adding latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Three producers, one append-only ledger
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Launch wrapper.&lt;/strong&gt; Writes the row, then spawns the agent in JSON-event mode; heartbeats every 15 s while the child lives; records the exit code; a spawn failure is recorded as failed with exit 127 instead of vanishing; signal forwarding is installed before the prompt is sent, so stop reaches the child. A test asserts the ordering: a fake agent blocks before its first event while the test polls the ledger and asserts the row already exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifecycle hooks.&lt;/strong&gt; Session start/stop, prompt submit, helper start/stop, tool events before and after each call. Lifecycle hooks run synchronously, tool hooks asynchronously. Each hook gets a &lt;strong&gt;25 ms lock wait and a 150 ms work budget&lt;/strong&gt;, targeting an exit under 200 ms; on timeout it logs and drops the event; it always exits zero and &lt;strong&gt;never writes to stdout&lt;/strong&gt; — anything printed would be injected into the agent's context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciler, every 5 s.&lt;/strong&gt; A running task whose process is gone with no exit becomes &lt;code&gt;lost&lt;/code&gt;. A running task idle for more than 600 s becomes &lt;code&gt;stalled&lt;/code&gt;, flipping back on the next event. It also scans the process table for Codex processes that never went through the wrapper, walks up to three parent levels to avoid double counting, and records them as &lt;code&gt;untracked&lt;/code&gt;, keyed by PID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fksrdv6fh5jeuyncwew4t.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fksrdv6fh5jeuyncwew4t.jpg" alt="Seven recorded statuses and four derived health bands" width="790" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Seven statuses; health is a read-time function, so the board cannot drift from the ledger. (Redrawn from the source's section 5.)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Progress exists only when there is a denominator
&lt;/h3&gt;

&lt;p&gt;My favourite rule in the whole write-up: percent complete is shown only when a denominator was declared. Runs launched with a step count ask the agent to emit &lt;code&gt;PROGRESS n/total&lt;/code&gt;, parsed from messages; Claude tasks report checklist items; everything else says "progress not reported". Even then it is the agent's own claim — &lt;strong&gt;the timeline is the evidence&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Admission control: one writer, two slots
&lt;/h3&gt;

&lt;p&gt;Concurrency per repository is bounded by kernel advisory locks, with the lock root derived from a hash of the checkout path. A writing run takes an exclusive writer lock, then one of two slot locks, tried without blocking. No sentinel files, no sleeps, no polling loops — a crashed holder releases when the kernel closes the file.&lt;/p&gt;

&lt;p&gt;The cap of two came from evidence, not taste: on day one, with everything at high effort, the Codex weekly allowance dropped to &lt;strong&gt;11% left&lt;/strong&gt;, and a third concurrent stream stalled while two ran. Runs also carry a token budget — 40 million by default — counted from the agent's own token events; over budget, the run is stopped and exits 125.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the ledger actually recorded (2026-09-08 → 10-23)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Task rows / events&lt;/td&gt;
&lt;td&gt;28,149 / 540,175&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;By source&lt;/td&gt;
&lt;td&gt;8,331 Codex, 7,267 Claude sessions, 12,373 Claude helpers, 178 other&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrapper-launched Codex runs&lt;/td&gt;
&lt;td&gt;1,380 done, 116 failed, 267 lost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Codex processes found outside the wrapper&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6,568 — all ended as lost&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean wall time, done runs&lt;/td&gt;
&lt;td&gt;7.95 min over 803 runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review verdicts&lt;/td&gt;
&lt;td&gt;160 APPROVE, 337 FIX, 87 INVALID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;188 methods in 22 Python files + 12 browser scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The largest number is the useful one: most Codex rows were never launched by the wrapper at all. They are processes the reconciler caught running on their own, recorded as untracked, then lost. That is the detector working — and a measure of how much agent activity is otherwise invisible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Five failures worth stealing
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The kill chain reaction.&lt;/strong&gt; Killing a reviewer produced a signal exit → parsed as INVALID → triggered the fix loop → spawned a new worker &lt;em&gt;and&lt;/em&gt; a new review. Each kill produced two or three more tasks. Fix: a reviewer stopped by a signal ends the chain with a &lt;code&gt;STOPPED&lt;/code&gt; verdict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget burn, itemized.&lt;/strong&gt; Day one: 36 tasks, ~24 agent hours in 36 hours, all high effort. The breakdown: re-reading context ~25%, oversized command output 18%, repeated aggregate verification 12%, high effort on routine work 12%. Roles, bounded packets, a release window and the two-run cap all came out of that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phantom helpers.&lt;/strong&gt; Helpers that never closed showed as stalled for hundreds of hours and flooded the view. Aggregation by project plus a 15-minute idle cutoff replaced one sprite per helper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock contention.&lt;/strong&gt; Builds in the board's own repo failed with &lt;code&gt;database is locked&lt;/code&gt; while a rendering service held the ledger. Operating rule: pause that service during builds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ready before verified.&lt;/strong&gt; UI work was reported ready before anyone opened it in a real browser with real data. New rule: no UI is ready until someone has looked at a screenshot.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The review gate: packet, grammar, one fix round
&lt;/h2&gt;

&lt;p&gt;Implementation runs as the role &lt;code&gt;worker&lt;/code&gt;, followed by an isolated review. The reviewer gets &lt;strong&gt;a packet, not a conversation&lt;/strong&gt;: the task-local diff, at most 200 lines of test output, the worker report, and the path to the original brief — inside a 256 KB budget. When the budget is tight, unchanged context and non-test file bodies are dropped, with recovery paths the reviewer can open.&lt;/p&gt;

&lt;p&gt;The verdict grammar is strict: exactly &lt;code&gt;APPROVE&lt;/code&gt;, or a numbered list of fixes. Anything else is classified &lt;code&gt;INVALID&lt;/code&gt;. A &lt;code&gt;FIX&lt;/code&gt; returns to the worker &lt;strong&gt;once&lt;/strong&gt;; a second review follows; still unresolved → exit 2, surfaced in a human queue. Nothing escalates automatically. Reviewers can be two different models, in which case both must approve and disagreements are labelled.&lt;/p&gt;

&lt;p&gt;584 verdicts on record: 160 APPROVE, 337 FIX, 87 INVALID. &lt;strong&gt;Fix requests outnumber approvals two to one&lt;/strong&gt; — and the author says that is the point of the loop, not a bad sign.&lt;/p&gt;

&lt;p&gt;The human queue is computed, not curated, from five sources (tasks failed in the last two hours, reviews still asking for changes, manually raised needs, needs raised by a live Claude session, work items waiting on the owner), with three suppression rules (acknowledged after the latest signal; a later run in the same directory or with the same title succeeded; dedupe per task, keep the newest signal). Display cap: five per lane. In the author's words — needs you, maximum five, real and current only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the "discussion" part goes wrong
&lt;/h2&gt;

&lt;p&gt;Handoffs are half the problem. The other half is agents disagreeing about a plan.&lt;/p&gt;

&lt;p&gt;A vendor write-up (CEO's column, so read it as an argument) describes a consensus cycle: agents share draft plans, surface conflicts, revise, repeat. It settles many disagreements — and when it cannot, the system escalates to a human. Then the important observation: &lt;strong&gt;the safe thing has a cost curve.&lt;/strong&gt; One team escalating occasionally is governance; fifty teams escalating every unresolved disagreement is a flooded queue, and flooded people approve by default. That is automation bias (Parasuraman &amp;amp; Manzey 2010; the EU AI Act names it in Article 14). A rubber stamp and a working control produce the same output while conditions are normal.&lt;/p&gt;

&lt;p&gt;So the constraint is not agent capacity — that is cheap and getting cheaper. &lt;strong&gt;It is the handful of people who understand the operation well enough to settle an argument.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The triage that follows is the part I actually use. Take the last twenty decisions your team pushed to a person and sort them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pile one:&lt;/strong&gt; the parties disagreed about &lt;em&gt;what will happen&lt;/em&gt; — different data, different history, different confidence in the same lever. This pile does not need a person's judgement; it needs both parties to see the same evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pile two:&lt;/strong&gt; they agreed on what will happen and disagreed about &lt;em&gt;what to want&lt;/em&gt; — throughput against energy cost, this customer's order against that one's. This pile is where a person's day should go.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your queue is mostly pile one, you are running an evidence problem through a governance process, and adding approvers will not fix it.&lt;/p&gt;

&lt;p&gt;The proposed split is clean: &lt;strong&gt;agents strategize, mathematics closes the deal.&lt;/strong&gt; Each agent decides only two things — eight weights over delivery slots, and a walk-away floor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;U(o) = w1·u1(o1) + w2·u2(o2) + … + w8·u8(o8)   accept only if U(o) ≥ r
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agreement itself is produced by a bargaining engine running alternating offers. No language model makes an offer, concedes a point, or declares a deal; the same positions in produce the same agreement out, auditable to the individual offer, settling in about a second — and when there is genuinely nothing to trade, the agents walk away, which is also correct.&lt;/p&gt;

&lt;p&gt;That gives you a third stage between "force consensus" and "page a human": a mediator proposes candidate settlements, filtered so nothing breaching a constraint is ever offered; each agent judges only whether a candidate beats the one on the table, and any agent can veto — without revealing its weights. What reaches the person is not two drafts and a deadlock notice, but a short list of what could not be settled and who vetoed what.&lt;/p&gt;

&lt;p&gt;The companion piece on roles adds four short rules that keep a "team" from becoming a demo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A role is one job and one checkable output.&lt;/strong&gt; "Research competitors and return a claims table with a source column" is a role. "Help with marketing" is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boundaries belong in tools, not prompts.&lt;/strong&gt; A reviewer reads the diff and posts comments; it cannot merge. Instructions can be ignored or misread; a missing tool cannot be used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the handoff explicit:&lt;/strong&gt; name the artefact, the reason it moved, and what the receiver must check — and log the handoff, so you can see where a decision changed hands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap the coordinator:&lt;/strong&gt; max reassignments, a time limit, and a rule for what happens after a task fails twice. It should also point at evidence per task, not "all complete".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the sequencing advice I keep repeating to myself: start with &lt;strong&gt;two roles and one handoff&lt;/strong&gt;, and get it boring before adding a third.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the three layers together
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F722lofoz0on8jmfotr81.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F722lofoz0on8jmfotr81.jpg" alt="Three layers: handoff contract (revision, commands, expected output, acceptor, open items), ledger (append-only, health derived), gates (packet, APPROVE or numbered fixes, one fix round, then a human)" width="790" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Handoff contract for the next agent; ledger for the machine; gate before side effects. (Drawn by the author from the four sources above.)&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Written by&lt;/th&gt;
&lt;th&gt;Read by&lt;/th&gt;
&lt;th&gt;If missing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Handoff&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HANDOFF.md&lt;/code&gt; (five fields + five evidence sections)&lt;/td&gt;
&lt;td&gt;Upstream owner or upstream agent&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;The next agent's start script&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The next agent guesses, or asks you — which is where we started&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ledger&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tasks.tsv&lt;/code&gt; or SQLite, append-only&lt;/td&gt;
&lt;td&gt;Wrapper, hooks, reconciler&lt;/td&gt;
&lt;td&gt;Board, gates, coordinator&lt;/td&gt;
&lt;td&gt;No answer to "who is running, stuck how long, spent what"; crashes lie by omission&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gate&lt;/td&gt;
&lt;td&gt;Review packet + verdict + approval&lt;/td&gt;
&lt;td&gt;Independent reviewer or authorized human&lt;/td&gt;
&lt;td&gt;Merge, deploy, anything with consequences&lt;/td&gt;
&lt;td&gt;You reject the report after the email has already gone out&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things actually get automated here — none of them is "make the model smarter":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Generate the contract at the end of the upstream task&lt;/strong&gt;: template plus a validator that checks the four fields are present, &lt;code&gt;acceptor ≠ implementer&lt;/code&gt;, and &lt;code&gt;base_commit&lt;/code&gt; actually exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consume the contract at the start of the downstream task&lt;/strong&gt;: the launcher injects &lt;code&gt;HANDOFF.md&lt;/code&gt; into the first turn, instead of you pasting it into a chat box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate before the side effect&lt;/strong&gt;: not "reject the report afterwards", but "hold the write until approved". That is exactly why staged tools matters — a rejection leaves nothing to undo.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A three-week rollout with pass/fail criteria
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Week 1 — contract plus validator.&lt;/strong&gt; &lt;code&gt;HANDOFF.md&lt;/code&gt; in the repo, a ~30-line script. Pass: ten consecutive tasks where no downstream agent comes back asking for missing information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 2 — wrapper plus reconciler.&lt;/strong&gt; Register the row before spawn; heartbeat every 15 s; mark &lt;code&gt;lost&lt;/code&gt; when a PID disappears without an exit; mark &lt;code&gt;stalled&lt;/code&gt; past 10 minutes idle. Pass: you can answer "who is running, stuck how long, spent what" at any moment, and &lt;code&gt;unknown&lt;/code&gt; was never silently filled with 0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 3 — reviewer plus grammar plus one fix round.&lt;/strong&gt; Packet, not conversation; verdicts limited to APPROVE or a numbered list; a single fix round; a human queue capped at five. Pass: the fix/approve ratio is stable, and the queue stays under five — if it does not, fix the process before adding approvers.&lt;/p&gt;

&lt;p&gt;The discussion layer goes after week 2: give plans an independent read-only reviewer, cap the loop at two rounds, and escalate — otherwise you build the flooded approver from the previous section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cautions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The ledger only covers runs that go through the wrapper. Those 6,568 outside processes are the proof: you must assume a share of activity is invisible.&lt;/li&gt;
&lt;li&gt;Heartbeats prove liveness, not progress.&lt;/li&gt;
&lt;li&gt;Lifecycle hooks cannot get a reliable PID for Claude sessions, so the board cannot stop them; progress counts are self-reported; budgets come from the agent's own token events.&lt;/li&gt;
&lt;li&gt;A strict verdict grammar has a price: 87 of 584 verdicts were INVALID (~15%). Watch what INVALID looks like, or you will fix formatting instead of behaviour.&lt;/li&gt;
&lt;li&gt;Locking collides with builds (&lt;code&gt;database is locked&lt;/code&gt;); the rule is to pause the lock holder during builds.&lt;/li&gt;
&lt;li&gt;"Ready before verified" is the most common self-deception; for UI, nobody has verified it until someone looked at a screenshot with real data.&lt;/li&gt;
&lt;li&gt;Never put credentials in a handoff contract.&lt;/li&gt;
&lt;li&gt;The four fields prevent "cannot start". They do not prevent "did it wrong" — that needs the evidence package and the acceptor. Do not merge the two jobs.&lt;/li&gt;
&lt;li&gt;Thresholds quoted here (256 KB packet, 200 log lines, 15 s heartbeat, 10 min idle, 40 M tokens, two slots) are one operator's values, not universal standards — recalibrate for your repo and budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;A task record is not an execution contract: a handoff template for coding agents&lt;/em&gt;, dev.to, 2026-10-12 — &lt;a href="https://dev.to/hiroshi_takamura_c851fe71/a-task-record-is-not-an-execution-contract-a-handoff-template-for-coding-agents-4bp8"&gt;https://dev.to/hiroshi_takamura_c851fe71/a-task-record-is-not-an-execution-contract-a-handoff-template-for-coding-agents-4bp8&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;A portable handoff contract for teammates using different AI tools&lt;/em&gt;, dev.to, 2026-10-15 — &lt;a href="https://dev.to/hiroshi_takamura_c851fe71/a-portable-handoff-contract-for-teammates-using-different-ai-tools-4ige"&gt;https://dev.to/hiroshi_takamura_c851fe71/a-portable-handoff-contract-for-teammates-using-different-ai-tools-4ige&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Stop AI Agents from Acting Before You Approve: Work Orders for TypeScript, Python and MCP&lt;/em&gt;, dev.to, 2026-10-16 — &lt;a href="https://dev.to/murthyg/how-to-build-reliable-work-handoffs-between-ai-agents-in-typescript-1neh"&gt;https://dev.to/murthyg/how-to-build-reliable-work-handoffs-between-ai-agents-in-typescript-1neh&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sam Ochoa, &lt;em&gt;Agent Board: coordinating concurrent AI coding agents with a shared task ledger&lt;/em&gt; (WP-05), 2026-10-23 — &lt;a href="https://samuelochoa.com/papers/agent-board/" rel="noopener noreferrer"&gt;https://samuelochoa.com/papers/agent-board/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pieter van Schalkwyk, &lt;em&gt;Should Multi-Agent Systems Negotiate?&lt;/em&gt;, XMPro, 2026-10-19 — &lt;a href="https://xmpro.com/should-multi-agent-systems-negotiate/" rel="noopener noreferrer"&gt;https://xmpro.com/should-multi-agent-systems-negotiate/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Running a Team of AI Agents: Roles, Boundaries, and the Handoffs That Break&lt;/em&gt;, dev.to, 2026-10-24 — &lt;a href="https://dev.to/flisk/running-a-team-of-ai-agents-roles-boundaries-and-the-handoffs-that-break-4fe9"&gt;https://dev.to/flisk/running-a-team-of-ai-agents-roles-boundaries-and-the-handoffs-that-break-4fe9&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disclosures.&lt;/strong&gt; The first two posts disclose AI assistance in drafting and state that the worked example was not run against a production repository, and reference a vendor's product page for the workflow model. The third is written by the author of the packages it describes. Agent Board is one operator's single-machine record (its own limitations section applies). The negotiation piece is a vendor CEO's column. Nothing here was independently reproduced by me.&lt;/p&gt;

&lt;p&gt;A longer Chinese version with the field-level contracts, the seven-state machine and the full failure list lives at &lt;a href="https://www.hubwiz.com/blog/agent-handoff-contract-and-ledger/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=agent-handoff-contract" rel="noopener noreferrer"&gt;hubwiz.com/blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>One machine, ten coding agents: giving each one its own identity</title>
      <dc:creator>tia-forge</dc:creator>
      <pubDate>Sat, 24 Oct 2026 06:14:27 +0000</pubDate>
      <link>https://dev.to/tiaforge/one-machine-ten-coding-agents-giving-each-one-its-own-identity-e20</link>
      <guid>https://dev.to/tiaforge/one-machine-ten-coding-agents-giving-each-one-its-own-identity-e20</guid>
      <description>&lt;p&gt;"An agent did it" stops being an answer the moment you run more than one.&lt;/p&gt;

&lt;p&gt;On a single developer machine you can now have Claude Code, Codex, OpenCode, Copilot, Antigravity, or &lt;code&gt;pi&lt;/code&gt; all working in the same repo, sometimes in the same hour. The default setup — one &lt;code&gt;git config user.name&lt;/code&gt;, one GitHub login, one &lt;code&gt;.env&lt;/code&gt; — works fine right up until something goes wrong and you have to answer a question that has no answer: &lt;em&gt;which&lt;/em&gt; agent wrote this, and what was it allowed to touch?&lt;/p&gt;

&lt;p&gt;This post compiles five write-ups from September–October 2026 where teams actually did this, including the mistakes. Everything below is attributed at the end; nothing here was reproduced by me, so treat the numbers as claims with sources rather than verified facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of one shared identity
&lt;/h2&gt;

&lt;p&gt;Vitruvian Software runs ten agents in one monorepo — Forge on Bazel, Atlas on infrastructure, Ridge on Kubernetes, Wren on application code, Scout on tests and CI, Aegis on security review, Quill on docs. Until recently, GitHub saw all of them as &lt;strong&gt;one person&lt;/strong&gt;: every push, PR, and merge went out under the repository owner's account.&lt;/p&gt;

&lt;p&gt;That looked harmless and cost them three things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you think you have&lt;/th&gt;
&lt;th&gt;What you actually get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Attribution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Ridge opened #1440" is a claim in a chat log. Neither git nor GitHub can back it up.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Peer review&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GitHub blocks self-approval, and every PR looks self-authored — so a security agent cannot give a valid approval on an infra agent's diff.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Audit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The trail says one human did everything, including work that human never saw.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second one is the one that hurts. If you want agents to check each other, the platform has to believe they are different identities. With one identity, the best you can do is have an agent leave a comment and hope someone reads it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1 — Git: a token decides &lt;em&gt;who can push&lt;/em&gt;, an environment variable decides &lt;em&gt;who signed&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;This is the failure that silently corrupts history, and it is easy to miss.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1) who pushes (API + git push)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;installation token&amp;gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 2) who is named in the commit — completely independent of the token&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GIT_AUTHOR_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"vitruvian-wren-agent[bot]"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GIT_AUTHOR_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;bot_user_id&amp;gt;+vitruvian-wren-agent[bot]@users.noreply.github.com"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GIT_COMMITTER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GIT_AUTHOR_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GIT_COMMITTER_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GIT_AUTHOR_EMAIL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vitruvian set only the token first. Result: &lt;strong&gt;the PR showed the bot, and the commit log still showed the machine owner&lt;/strong&gt; — because git takes authorship from local config and environment, not from whoever pushes.&lt;/p&gt;

&lt;p&gt;Three details worth copying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the &lt;strong&gt;bot's noreply address&lt;/strong&gt;. GitHub ties it to the App, so commits link back to &lt;code&gt;xxx[bot]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-hour installation tokens are a feature.&lt;/strong&gt; The only long-lived secret is the private key, which lives in a vault; the token is minted per session and never pasted anywhere.&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;both&lt;/strong&gt; &lt;code&gt;GIT_AUTHOR_*&lt;/code&gt; and &lt;code&gt;GIT_COMMITTER_*&lt;/code&gt;. Change only the author and a rebase or cherry-pick leaks the real operator into the committer field.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Layer 2 — Platform: GitHub Apps, not one bot account per agent
&lt;/h2&gt;

&lt;p&gt;The obvious fix is a machine user per agent. GitHub's terms allow &lt;strong&gt;one free machine account per person&lt;/strong&gt;, so ten agents would mean ten accounts the terms don't cover plus ten sets of credentials to rotate. GitHub Apps have no such limit: an org can own as many as it likes, each gets a real &lt;code&gt;name[bot]&lt;/code&gt; identity that genuinely authors commits and submits reviews, and each authenticates with short-lived installation tokens instead of passwords or PATs. They named them &lt;code&gt;vitruvian-&amp;lt;agent&amp;gt;-agent&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The question the docs don't answer
&lt;/h3&gt;

&lt;p&gt;Before building, they needed to know whether an App's approval counts toward a &lt;code&gt;require 1 approving review&lt;/code&gt; rule. The docs on protected branches say approvals come from "people with write permissions" or a designated code owner — Apps aren't mentioned either way.&lt;/p&gt;

&lt;p&gt;So they measured it on a canary repo with &lt;code&gt;required_approving_review_count: 1&lt;/code&gt; and &lt;code&gt;enforce_admins: true&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before   reviewDecision=REVIEW_REQUIRED  mergeable_state=blocked
POST /pulls/1/reviews   event=APPROVE   (App installation token)
after    reviewDecision=APPROVED         mergeable_state=unstable
         review  &amp;lt;app-slug&amp;gt;[bot]  type=Bot  state=APPROVED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It counts. (&lt;code&gt;unstable&lt;/code&gt; was an unrelated pending check.) One limit surfaced: &lt;strong&gt;an App cannot be a code owner&lt;/strong&gt; — &lt;code&gt;CODEOWNERS&lt;/code&gt; accepts users, teams, and email addresses only. So they turned off "require code owner review" and used the approval count as the lever.&lt;/p&gt;

&lt;h3&gt;
  
  
  The step you cannot automate
&lt;/h3&gt;

&lt;p&gt;App creation has no API. You either fill in a web form or use the &lt;strong&gt;App manifest flow&lt;/strong&gt;, which is better because it returns the private key in an API response instead of a browser download:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitruvian-&amp;lt;agent&amp;gt;-agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default_permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"contents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pull_requests"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"checks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"workflows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"write"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default_events"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub redirects back with a &lt;strong&gt;one-time code&lt;/strong&gt; you exchange within the hour for the App id and private key. That is the only copy of the key: straight into the vault, response deleted.&lt;/p&gt;

&lt;p&gt;Two traps: the install screen &lt;strong&gt;preselects "All repositories"&lt;/strong&gt; (accept it and every agent can write to every repo in the org — choose "Only select repositories"), and &lt;code&gt;workflows: write&lt;/code&gt; is only for agents that should touch CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make "who can write where" reviewable
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;repo_config:agentApps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;beacon&lt;/span&gt;
    &lt;span class="na"&gt;installationId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;152031070"&lt;/span&gt;
  &lt;span class="c1"&gt;# ...one entry per agent&lt;/span&gt;
&lt;span class="na"&gt;repo_config:requiredApprovals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pinning each App's installation to the repo turns authorization into &lt;strong&gt;a diff you review&lt;/strong&gt;, not a checkbox on a settings page nobody opens.&lt;/p&gt;

&lt;p&gt;And ordering matters. &lt;code&gt;requiredApprovals: "1"&lt;/code&gt; must be &lt;strong&gt;last&lt;/strong&gt;. Enable required reviews before the agents have identities and there is nobody who can approve: every PR waits at &lt;code&gt;REVIEW_REQUIRED&lt;/code&gt; forever, and release-please / Dependabot auto-merges get stuck too, because a ruleset bypass covers direct merges but never auto-merge into a merge queue.&lt;/p&gt;

&lt;p&gt;Safe order: create and install one App per agent → put keys in the vault and point each harness at its key → add every agent to &lt;code&gt;agentApps&lt;/code&gt; → &lt;em&gt;then&lt;/em&gt; raise &lt;code&gt;requiredApprovals&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Day to day, an agent takes on its identity with one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;bazel run //tools/agent-app &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;env &lt;/span&gt;wren&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks the agent up in a &lt;strong&gt;committed registry&lt;/strong&gt; (App ids, installation ids, bot user ids are all public identifiers), signs a JWT, exchanges it for an installation token, and exports both the token &lt;em&gt;and&lt;/em&gt; the git identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3 — Inside the commit: who writes the three lines
&lt;/h2&gt;

&lt;p&gt;A git author field holds a name and an email. It cannot hold "which job, which tool, which model". Obi (a Rust agent execution engine) appends three trailers instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Oby-Job: 3f2a9c1e
Oby-Agent: Codex
Oby-Model: gpt-6.1-sol
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a teammate reading &lt;code&gt;git log&lt;/code&gt; can tell which code an agent wrote and with what, without asking the owner — and the owner can query a model's track record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--grep&lt;/span&gt; &lt;span class="s2"&gt;"Oby-Model: gpt-6.1-sol"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why the engine must write them, not the agent
&lt;/h3&gt;

&lt;p&gt;This is the most transferable idea in the whole design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;trailers are written during &lt;code&gt;keep_commits&lt;/code&gt;, when a finished job's commits are packed back into the engine's clone;&lt;/li&gt;
&lt;li&gt;each new commit is rewritten directly — read the raw object with &lt;code&gt;git cat-file commit&lt;/code&gt;, repoint parents, append the trailers, write the new object with &lt;code&gt;git hash-object -t commit -w --stdin&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;deliberately not&lt;/strong&gt; &lt;code&gt;git commit-tree&lt;/code&gt; or &lt;code&gt;git interpret-trailers&lt;/code&gt;, which read &lt;code&gt;commit.gpgSign&lt;/code&gt; / &lt;code&gt;trailer.*&lt;/code&gt; and can change the bytes;&lt;/li&gt;
&lt;li&gt;tree, author, committer, and both dates stay exactly as they were;&lt;/li&gt;
&lt;li&gt;if the agent wrote &lt;code&gt;Oby-*&lt;/code&gt; lines itself, they are &lt;strong&gt;removed first&lt;/strong&gt; — only the engine gets to say this;&lt;/li&gt;
&lt;li&gt;writing the same trailers twice produces the same commit id (idempotent).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two more choices: &lt;code&gt;Oby-Agent&lt;/code&gt; records the &lt;strong&gt;tool&lt;/strong&gt; (&lt;code&gt;Claude Code&lt;/code&gt;, &lt;code&gt;Codex&lt;/code&gt;, &lt;code&gt;Cursor&lt;/code&gt;), not an account; and nothing ever records an account name, email, or hostname, &lt;strong&gt;because commits can be public&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Compare with "make the agent add a &lt;code&gt;Co-authored-by&lt;/code&gt; line": the agent can forget, can get it wrong, and can be talked out of it by an injected instruction. &lt;strong&gt;Attribution written by a layer that does not reason is attribution you can trust.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4 — Granularity: one action, one commit
&lt;/h2&gt;

&lt;p&gt;Orquesta takes the other route: don't rewrite trailers, make every action a commit. Their argument is that today's tools either dump changes into the working tree or produce one mega-commit at the end ("AI refactor") — and that isn't traceability, it's a black box with a receipt.&lt;/p&gt;

&lt;p&gt;The mechanism:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the agent works on a &lt;strong&gt;real branch&lt;/strong&gt; cut from base;&lt;/li&gt;
&lt;li&gt;every file edit, code-changing shell command, and test-fix cycle is committed, with a Conventional-Commits-style message;&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;author field is the agent&lt;/strong&gt;, via a separate git config:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config user.name  &lt;span class="s2"&gt;"Orquesta Agent"&lt;/span&gt;
git config user.email &lt;span class="s2"&gt;"agent@orquesta.local"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;read-only commands produce &lt;strong&gt;no&lt;/strong&gt; commit (nothing changed), but they are logged in the activity feed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payoff is surgical rollback: when an agent both adds an endpoint and fixes an unrelated bug in one session, and the endpoint regresses, you revert one commit and keep the fix. Noise is handled by asking the agent to combine logically identical changes, plus a squash step before merge — granularity during execution, clean history after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 5 — Process and credentials: give a key to exactly one agent
&lt;/h2&gt;

&lt;p&gt;Identity is not just a name; it is &lt;strong&gt;what the process can read&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Bill Mongan runs nine agents side by side on a small always-on Ubuntu box (~14 GiB RAM): Claude Code, Codex, Antigravity, Grok Build, Hermes, GitHub Copilot, two separate copies of OpenCode, and &lt;code&gt;pi&lt;/code&gt;. Herdr (an agent-aware terminal multiplexer) gives each one a labelled tab and &lt;strong&gt;its own directory&lt;/strong&gt; under &lt;code&gt;~/agents/&amp;lt;name&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That directory separation matters more than expected: most CLIs resolve "resume the latest conversation" relative to the current directory, and several ask you to trust a workspace before touching it. One directory per agent keeps histories, trust decisions, and scratch files apart.&lt;/p&gt;

&lt;p&gt;Most agents share a single &lt;code&gt;mcpproxy&lt;/code&gt; instance for tools. One tool talks to a service that needs a personal API key, and only one agent should hold it. The trick:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the provider declares the secret &lt;strong&gt;both&lt;/strong&gt; as a server-side env var and as a caller-supplied header;&lt;/li&gt;
&lt;li&gt;the proxy's &lt;code&gt;.env&lt;/code&gt; gets the base URL only, and the key is &lt;strong&gt;left unset&lt;/strong&gt; — so the proxy has no credential to hand out;&lt;/li&gt;
&lt;li&gt;the key lives in one scoped OpenCode's private env file and is forwarded as a header:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="nl"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mcpproxy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"remote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1:8888/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"oauth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"X-MCPProxy-Service-Key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{env:SERVICE_API_KEY}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;the launcher is a small wrapper: &lt;code&gt;set -a; source service.env; set +a; exec opencode&lt;/code&gt;. &lt;code&gt;set -a&lt;/code&gt; exports every assignment, &lt;code&gt;exec&lt;/code&gt; avoids leaving a shell process holding a copy — &lt;strong&gt;so the variable exists in that process tree and nowhere else&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other agents can still see the tool; their calls simply fail to authenticate. When they need the capability, they &lt;strong&gt;delegate&lt;/strong&gt; the task to the scoped agent instead of holding the key.&lt;/p&gt;

&lt;h3&gt;
  
  
  And the author is explicit that this is not a hard boundary
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;any agent with a shell, running as the same Unix user, can read that &lt;code&gt;service.env&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the header travels over plain HTTP, acceptable only because the proxy listens on loopback;&lt;/li&gt;
&lt;li&gt;real isolation between agents would mean separate Unix users or containers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So: &lt;strong&gt;least privilege by process environment, not a security boundary — and not treated as one.&lt;/strong&gt; Its value is that other agents can't use the key by accident, and can't leak it through a tool that dumps environment variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  What identity does not stop
&lt;/h2&gt;

&lt;p&gt;Be clear about this, or you'll think you're done. Vitruvian published two incidents from their own setup:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Incident&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Merge queue bypass&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;main&lt;/code&gt; let repo admins skip the merge queue as a human emergency path. Agent sessions under that account &lt;strong&gt;inherited the bypass&lt;/strong&gt;: 5 of 40 merges skipped the queue in two days; one of them had been rejected by the queue &lt;strong&gt;eight minutes earlier&lt;/strong&gt;, and main went red.&lt;/td&gt;
&lt;td&gt;A pre-tool hook refuses &lt;code&gt;gh pr merge --admin&lt;/code&gt;, direct merges via REST/GraphQL, and pushes to &lt;code&gt;main&lt;/code&gt; — and tells the agent to fix what the queue rejected. A human who really needs the bypass runs it in their own terminal, where the hook doesn't apply.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The worktree that uninstalled Argo CD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The homelab Pulumi config is gitignored, so it doesn't exist in a worktree → every toggle read &lt;code&gt;false&lt;/code&gt; → one &lt;code&gt;pulumi up&lt;/code&gt; removed Argo CD, its namespace, and every Application it managed.&lt;/td&gt;
&lt;td&gt;The program now refuses to run unless the config states the setting explicitly; and "never apply from a worktree" lives in &lt;code&gt;AGENTS.md&lt;/code&gt; &lt;strong&gt;next to the incident date&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Their conclusion is worth quoting in full:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Per-agent identity is a convention we keep, written into the &lt;code&gt;AGENTS.md&lt;/code&gt; that every coding tool in the repo reads: use your own name; minting as another agent puts their name on your work. It makes honest work attributable and reviewable. It doesn't make dishonest work impossible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What actually stops corner-cutting is the other layer: &lt;strong&gt;guardrails written after real incidents&lt;/strong&gt;, where every fix ships with the check that would catch it again.&lt;/p&gt;

&lt;p&gt;Safeguard's October write-up names the five risks of "running as you": excess authority, blind audit trails, prompt injection, credential reach (tokens in env vars and dotfiles are readable by anything you run), and long sessions that accumulate context and access. Their rollout advice is practical: start with the highest-risk accounts and tasks (production, releases, cloud admin), keep everyday coding in a sandbox but remove long-lived secrets — and remember that &lt;strong&gt;if a scoped token takes a day and a ticket, developers will keep using their own identity&lt;/strong&gt;. Tooling matters as much as policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  A weekend checklist (dependency order, not priority order)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decide who appears in history.&lt;/strong&gt; Give each agent its own &lt;code&gt;GIT_AUTHOR_*&lt;/code&gt; / &lt;code&gt;GIT_COMMITTER_*&lt;/code&gt; and a resolvable email. Ten minutes, immediate payoff: &lt;code&gt;git log&lt;/code&gt; becomes readable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the platform agree.&lt;/strong&gt; One GitHub App per agent (manifest flow), key in a vault, installation tokens only, "Only select repositories".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put attribution inside the commit.&lt;/strong&gt; Either an engine that rewrites trailers (Obi) or one-commit-per-action with a distinct author (Orquesta). Do not rely on the agent to remember.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Narrow what the process can read.&lt;/strong&gt; Per-agent config dirs and env files (mode 0600), header-injected or brokered credentials, nothing sensitive in a shared &lt;code&gt;.env&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raise required reviews last.&lt;/strong&gt; After step 2, so a &lt;em&gt;different&lt;/em&gt; identity can approve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write guardrails, not hopes.&lt;/strong&gt; Incident date in &lt;code&gt;AGENTS.md&lt;/code&gt;, every fix paired with a test that fails if it regresses, and "identity is attribution, not enforcement" as a stated design assumption.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  If it's just you and three agents
&lt;/h2&gt;

&lt;p&gt;The sources above are all team-scale. A lighter ladder:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where you are&lt;/th&gt;
&lt;th&gt;Smallest useful move&lt;/th&gt;
&lt;th&gt;Why it's enough&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Three agents on one &lt;code&gt;user.name&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;per-agent &lt;code&gt;GIT_AUTHOR_*&lt;/code&gt; / &lt;code&gt;GIT_COMMITTER_*&lt;/code&gt; at launch&lt;/td&gt;
&lt;td&gt;One hour to a readable &lt;code&gt;git log&lt;/code&gt;, zero platform changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want them to review each other&lt;/td&gt;
&lt;td&gt;trailers + branch prefixes (&lt;code&gt;agent/&amp;lt;name&amp;gt;/&amp;lt;topic&amp;gt;&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Without platform identity, "peer review" is still human review; first make authorship legible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want branch protection to actually bite&lt;/td&gt;
&lt;td&gt;then GitHub Apps (or at least separate accounts)&lt;/td&gt;
&lt;td&gt;"The platform accepts these are two identities" is the one thing env vars can't buy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You hold sensitive credentials&lt;/td&gt;
&lt;td&gt;one config dir + one env file (0600) per agent&lt;/td&gt;
&lt;td&gt;Keeps a key inside one process tree; the rest can't misuse it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A wrapper is enough to start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/bin/agent-run — usage: agent-run wren claude&lt;/span&gt;
agent-run&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift
  export &lt;/span&gt;&lt;span class="nv"&gt;GIT_AUTHOR_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GIT_AUTHOR_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;@agents.local"&lt;/span&gt;
  &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GIT_COMMITTER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GIT_COMMITTER_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;@agents.local"&lt;/span&gt;
  &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PI_CODING_AGENT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.pi/agents/&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# swap for your tool's config-dir variable&lt;/span&gt;
  &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And one check that fails loudly when you got it wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%an &amp;lt;%ae&amp;gt; | %cn &amp;lt;%ce&amp;gt;'&lt;/span&gt;   &lt;span class="c"&gt;# expect the agent name, not yours&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Boundaries
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This is a compilation, not a reproduction. The App-approval measurement, the trailer rewriting, the git-native flow, and the nine-agent box all come from the authors' own environments.&lt;/li&gt;
&lt;li&gt;Platform behaviour changes; "does an App approval count" and "can an App be a code owner" are both in the "not in the docs, measure it" category. Re-verify on a canary repo before relying on any number here.&lt;/li&gt;
&lt;li&gt;Naming (&lt;code&gt;vitruvian-*&lt;/code&gt;, &lt;code&gt;Oby-*&lt;/code&gt;) is incidental. What transfers is the structure: &lt;em&gt;who writes attribution, where it is stored, and which layer holds the credential.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Adjacent but different: sandboxing, egress limits, approval gates. Those answer "what can it do". This post answers "who does it count as".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Vitruvian Software, &lt;em&gt;Every AI agent gets its own GitHub identity&lt;/em&gt;, 2026-10-09 — &lt;a href="https://vitruviansoftware.dev/blog/2026/10/09/every-ai-agent-gets-its-own-github-identity/" rel="noopener noreferrer"&gt;https://vitruviansoftware.dev/blog/2026/10/09/every-ai-agent-gets-its-own-github-identity/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Obi (&lt;code&gt;obi-ai-bot/oby&lt;/code&gt;) issue #1068, &lt;em&gt;obid: commits say which job, agent and model wrote them&lt;/em&gt;, 2026-10-22 — &lt;a href="https://github.com/obi-ai-bot/oby/issues/1068" rel="noopener noreferrer"&gt;https://github.com/obi-ai-bot/oby/issues/1068&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Orquesta, &lt;em&gt;Git-Native AI Development: Every Action is a Commit&lt;/em&gt;, 2026-10-23 — &lt;a href="https://getorquesta.com/blog/git-native-ai-development-every-action-is-a-commit-2026-10-23" rel="noopener noreferrer"&gt;https://getorquesta.com/blog/git-native-ai-development-every-action-is-a-commit-2026-10-23&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;William M. Mongan, &lt;em&gt;Herding Agents: Multi-Agent Coordination with Herdr and Per-Agent MCP Credentials&lt;/em&gt;, 2026-10-04 — &lt;a href="https://www.billmongan.com/posts/2026/10/herdr-multiagent-mcpproxy/" rel="noopener noreferrer"&gt;https://www.billmongan.com/posts/2026/10/herdr-multiagent-mcpproxy/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Safeguard Security Team, &lt;em&gt;What are the identity risks when an AI coding agent runs as you&lt;/em&gt;, 2026-10-18 — &lt;a href="https://safeguard.sh/resources/blog/when-your-ai-coding-agent-runs-as-you-identity-risks-and-fixes" rel="noopener noreferrer"&gt;https://safeguard.sh/resources/blog/when-your-ai-coding-agent-runs-as-you-identity-risks-and-fixes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Background: Sumant Thakur, &lt;em&gt;I Ran Multiple Coding Agents. The Code Was the Easy Part.&lt;/em&gt;, 2026-09-30 — &lt;a href="https://sumantthakur.substack.com/p/multi-agent-coding-workflow" rel="noopener noreferrer"&gt;https://sumantthakur.substack.com/p/multi-agent-coding-workflow&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chinese original (full version, with the platform-specific details): &lt;a href="https://www.hubwiz.com/blog/multiple-coding-agents-identity-git-attribution/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=multiple-coding-agents-identity" rel="noopener noreferrer"&gt;多 Agent 同机：Git 作者、凭据与提交归属怎么分&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More on this site (in Chinese): &lt;a href="https://www.hubwiz.com/blog/vibe-coding-stack/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=multiple-coding-agents-identity" rel="noopener noreferrer"&gt;AI 编程 / Vibe Coding 完全指南&lt;/a&gt; · &lt;a href="https://www.hubwiz.com/blog/agent-stack/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=multiple-coding-agents-identity" rel="noopener noreferrer"&gt;Agent / MCP / 自动化完全指南&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>git</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>113 Siemens Ladder Instructions, One Page Each — and the Sections That Actually Save You Time</title>
      <dc:creator>tia-forge</dc:creator>
      <pubDate>Thu, 22 Oct 2026 08:42:11 +0000</pubDate>
      <link>https://dev.to/tiaforge/113-siemens-ladder-instructions-one-page-each-and-the-sections-that-actually-save-you-time-2pog</link>
      <guid>https://dev.to/tiaforge/113-siemens-ladder-instructions-one-page-each-and-the-sections-that-actually-save-you-time-2pog</guid>
      <description>&lt;p&gt;&lt;strong&gt;When you look up a ladder instruction mid-commissioning, the expensive part is never remembering what the symbol looks like.&lt;/strong&gt; It's the next two questions: &lt;em&gt;how is this different from the instruction next to it&lt;/em&gt;, and &lt;em&gt;what happens if I pick the wrong one&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tia.hubwiz.com/learn-lad/instructions/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=lad-instruction-handbook-siemens" rel="noopener noreferrer"&gt;Learn-LAD's LAD instruction handbook&lt;/a&gt; answers exactly those. It's &lt;strong&gt;113 S7-1200/1500 instructions in 13 groups, one page each&lt;/strong&gt;, with a fixed seven-section structure per page: one-sentence summary, symbol and pins, minimal example, how it works (truth/timing table), typical uses, common pitfalls, related examples. Each page also carries an "try it in TIA" panel: pick an example block, and &lt;a href="https://tia.hubwiz.com/tia-link?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=lad-instruction-handbook-siemens" rel="noopener noreferrer"&gt;TiaLink&lt;/a&gt; imports it into the TIA Portal project you currently have open — pre-check first, no overwriting of same-named objects by default, no auto-save after import.&lt;/p&gt;

&lt;p&gt;It is not a replacement for the TIA help. The help tells you &lt;em&gt;what a pin is&lt;/em&gt;; this tells you &lt;em&gt;which instruction to pick, and how it fails&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Where people actually get stuck
&lt;/h2&gt;

&lt;p&gt;Everyone learns contacts, coils and timers once. The real problems show up later, and they cluster into four shapes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Neighbours look identical.&lt;/strong&gt; &lt;code&gt;TON&lt;/code&gt; / &lt;code&gt;TOF&lt;/code&gt; / &lt;code&gt;TP&lt;/code&gt; are all "timers". &lt;code&gt;SCoil&lt;/code&gt; and &lt;code&gt;Sr&lt;/code&gt; both latch. The failure always happens between two instructions that resemble each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You know the definition, not the consequence.&lt;/strong&gt; "&lt;code&gt;ET&lt;/code&gt; resets when &lt;code&gt;IN&lt;/code&gt; goes to 0" reads as trivia — until you use it as an accumulated run-time counter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parameter help stops at parameters.&lt;/strong&gt; TIA help will tell you &lt;code&gt;PT&lt;/code&gt; is the preset time. It won't warn you that a one-scan pulse never reaches &lt;code&gt;PT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You understand the page, then rebuild the network by hand&lt;/strong&gt; — and introduce new mistakes in the rebuild.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the useful test for any instruction reference is five criteria:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;th&gt;What the handbook does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pin table with directions&lt;/td&gt;
&lt;td&gt;A flow input (RLO) is not a value input&lt;/td&gt;
&lt;td&gt;Every page has one, labelled flow input / input / flow output / output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A minimal example&lt;/td&gt;
&lt;td&gt;Definitions don't stick; a three-rung network does&lt;/td&gt;
&lt;td&gt;Every page has a rendered ladder plus variable notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Truth or timing table&lt;/td&gt;
&lt;td&gt;"When is it 1" must be derivable scan by scan&lt;/td&gt;
&lt;td&gt;Timers, counters and edges get state tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"How it differs from its neighbour"&lt;/td&gt;
&lt;td&gt;Misuse happens between lookalikes&lt;/td&gt;
&lt;td&gt;A fixed "common pitfalls" section; its first line is usually "X is not Y"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;A single instruction is meaningless outside a program&lt;/td&gt;
&lt;td&gt;Related examples graded basic/advanced/full, plus related instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  2. Scale: 113 instructions, 13 groups
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Representative instructions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bit logic&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Contact, Coil, SCoil, RCoil, Not, SBitfield, RBitfield&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flip-flops&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Sr (set-dominant), Rs (reset-dominant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;PContact, NContact, PBox, NBox, PCoil, NCoil, R_TRIG, F_TRIG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timers&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;TON, TOF, TP, TONR, CoilTON, CoilTOF, CoilTP, CoilTONR, PtCoil, ResetIECTimerCoil&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Counters&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;CTU, CTD, CTUD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comparison&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Eq, Lt, Gt, Le, Ge, Ne, InRange, OutRange, OK, NOK, IS_ARRAY, IS_NULL, NOT_NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Word logic&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;SEL, And, Or, Xor, Inv, Mux, Demux, DECO, ENCO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;LEN, CONCAT, LEFT, RIGHT, MID, FIND, INSERT, DELETE, REPLACE, S_Move&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shift/rotate&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Shl, Shr, Rl, Rr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Math&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;Add, Sub, Mul, Div, Mod, LIMIT, MIN, MAX, Abs, Inc, Dec, trig/log/rounding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Move&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Move, Swap, MoveBlockI/U, FillBlockI/U, Lower_Bound, Upper_Bound, GATHER, SCATTER, Serialize, Deserialize&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conversion&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Convert, Scale_X, Normalize, Round, Trunc, Ceil, Floor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Program control&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Jump, JumpNot, Switch, JmpList&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three editorial choices say a lot about the intent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CoilTON&lt;/code&gt; / &lt;code&gt;CoilTOF&lt;/code&gt; / &lt;code&gt;CoilTP&lt;/code&gt; / &lt;code&gt;CoilTONR&lt;/code&gt; get their own pages.&lt;/strong&gt; Coil-form timers and box-form timers differ in syntax &lt;em&gt;and&lt;/em&gt; in where they may be placed; mixing them is a standard commissioning incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OK&lt;/code&gt; / &lt;code&gt;NOK&lt;/code&gt; / &lt;code&gt;IS_ARRAY&lt;/code&gt; / &lt;code&gt;IS_NULL&lt;/code&gt; / &lt;code&gt;NOT_NULL&lt;/code&gt; are filed under comparison.&lt;/strong&gt; These aren't comparisons of magnitude; they're validity checks, and you cannot write reusable FBs without them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only four program-control instructions.&lt;/strong&gt; No attempt to mirror the whole instruction set — just the jump forms ladder actually uses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every page header also carries &lt;strong&gt;applicable CPUs (S7-1200/1500), the version basis (TIA V19), and aliases&lt;/strong&gt; — &lt;code&gt;TON&lt;/code&gt; is alias-mapped to "on-delay timer / SD", &lt;code&gt;TP&lt;/code&gt; to "pulse timer / SP". The alias line matters when you're reading old drawings: the sheet says &lt;code&gt;SD&lt;/code&gt;, TIA says &lt;code&gt;TON&lt;/code&gt;, same thing.&lt;/p&gt;

&lt;p&gt;One implementation detail worth knowing: pin names and directions come from the &lt;strong&gt;&lt;code&gt;lad-ir&lt;/code&gt; instruction catalogue&lt;/strong&gt;, and the figures are rendered by &lt;strong&gt;&lt;code&gt;lad-render&lt;/code&gt;&lt;/strong&gt; — the same renderer used by the example library, so a handbook figure and an example figure look identical side by side.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. What one page contains (TON as the example)
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://tia.hubwiz.com/learn-lad/instructions/ton/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=lad-instruction-handbook-siemens" rel="noopener noreferrer"&gt;TON — on-delay timer&lt;/a&gt;. Top to bottom, seven sections. The order is itself a reading strategy: &lt;strong&gt;usable first, mechanism second, boundary last.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) One sentence.&lt;/strong&gt; "&lt;code&gt;TON&lt;/code&gt; is an on-delay timer: &lt;code&gt;Q&lt;/code&gt; goes to 1 only after &lt;code&gt;IN&lt;/code&gt; has been continuously 1 for &lt;code&gt;PT&lt;/code&gt;; as soon as &lt;code&gt;IN&lt;/code&gt; drops, &lt;code&gt;Q&lt;/code&gt; and &lt;code&gt;ET&lt;/code&gt; reset." Trigger condition, decision condition, reset condition — drop any one of the three and the instruction gets misused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) Symbol and pins.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pin&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;flow input&lt;/td&gt;
&lt;td&gt;Enable; starts timing while 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;input&lt;/td&gt;
&lt;td&gt;Preset time (Time)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Q&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;flow output&lt;/td&gt;
&lt;td&gt;1 after elapsed; resets when &lt;code&gt;IN&lt;/code&gt; drops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;output&lt;/td&gt;
&lt;td&gt;Elapsed time; returns to 0 when &lt;code&gt;IN&lt;/code&gt; is 0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus three metadata lines that decide whether it can live in an FC at all: &lt;strong&gt;group: timers&lt;/strong&gt;, &lt;strong&gt;needs an instance&lt;/strong&gt;, &lt;strong&gt;template value: time type&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3) Minimal example.&lt;/strong&gt; &lt;code&gt;Start&lt;/code&gt; held for 5 s, then &lt;code&gt;Motor&lt;/code&gt; is set; &lt;code&gt;Start&lt;/code&gt; dropping resets &lt;code&gt;Motor&lt;/code&gt; immediately. The example uses a static multi-instance &lt;code&gt;Timer&lt;/code&gt; inside an FB; in an OB/FC you can use a global IEC timer instance DB instead. The figure notes that &lt;code&gt;#&lt;/code&gt; means a block-local variable, and that real projects may use global tags or DB variables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4) How it works (state table).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;IN&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Elapsed&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Q&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;ET&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&amp;lt; &lt;code&gt;PT&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;increments from 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;≥ &lt;code&gt;PT&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;stops at &lt;code&gt;PT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 → 0&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;back to 0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four notes follow, and each one is a pitfall in disguise: &lt;code&gt;IN&lt;/code&gt; must be &lt;em&gt;continuous&lt;/em&gt;; &lt;code&gt;PT&lt;/code&gt; versus &lt;code&gt;ET&lt;/code&gt;; &lt;code&gt;ET&lt;/code&gt; stops growing once the time is reached; never call the same instance from multiple places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5) Typical uses.&lt;/strong&gt; Delay-on start (3–5 s confirmation before energising a motor); debounce / in-position confirmation (drop &lt;code&gt;PT&lt;/code&gt; to 200–500 ms and use &lt;code&gt;Q&lt;/code&gt; as "signal is stable"); timeout alarm (start timing on the command, alarm if "done" never arrives). That third one adds the premise most people miss: &lt;strong&gt;you usually need a &lt;code&gt;NOT Done&lt;/code&gt; condition in series&lt;/strong&gt;, and if &lt;code&gt;Start&lt;/code&gt; is a momentary button you must first latch it with &lt;code&gt;SCoil&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6) Common pitfalls.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TON is not TP&lt;/strong&gt;: &lt;code&gt;TON&lt;/code&gt; holds &lt;code&gt;Q&lt;/code&gt; until &lt;code&gt;IN&lt;/code&gt; drops; &lt;code&gt;TP&lt;/code&gt; is a fixed pulse that &lt;code&gt;IN&lt;/code&gt; cannot cut short.&lt;br&gt;
&lt;strong&gt;TON is not TOF&lt;/strong&gt;: &lt;code&gt;TOF&lt;/code&gt; is off-delay; &lt;code&gt;Q&lt;/code&gt; stays 1 for &lt;code&gt;PT&lt;/code&gt; &lt;em&gt;after&lt;/em&gt; &lt;code&gt;IN&lt;/code&gt; drops.&lt;br&gt;
&lt;strong&gt;A short pulse never reaches &lt;code&gt;TON&lt;/code&gt;&lt;/strong&gt;: a one-scan pulse never gets to &lt;code&gt;PT&lt;/code&gt;.&lt;br&gt;
&lt;strong&gt;&lt;code&gt;ET&lt;/code&gt; resets&lt;/strong&gt;: with &lt;code&gt;IN = 0&lt;/code&gt;, &lt;code&gt;ET&lt;/code&gt; goes back to 0 — don't use it as an accumulator.&lt;br&gt;
&lt;strong&gt;Instance conflicts&lt;/strong&gt;: calling the same instance from multiple networks overwrites the timing.&lt;br&gt;
&lt;strong&gt;Missing &lt;code&gt;PT&lt;/code&gt;&lt;/strong&gt;: TIA reports an unused parameter; SCL calls like &lt;code&gt;TON(IN:=...)&lt;/code&gt; lose it easily.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Five of the six describe &lt;em&gt;failure shapes&lt;/em&gt;, not definitions. That's where the information density of this handbook actually lives — it reads like it was written from observed mistakes, not from a parameter table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7) Related examples and related instructions.&lt;/strong&gt; Two graded examples (delayed motor start with pump control; three-motor staggered start/stop), each with a one-line summary and a difficulty tag, plus two lateral links (&lt;code&gt;SCoil&lt;/code&gt;, &lt;code&gt;TOF&lt;/code&gt;) — "the next thing you will confuse this with".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus: the "try it in TIA" panel.&lt;/strong&gt; This is the difference from a normal documentation site. A block selector offers variants (&lt;code&gt;TON&lt;/code&gt; has minimal example / signal debounce / timeout alarm). Importing then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;goes through &lt;strong&gt;TiaLink into the project you currently have open&lt;/strong&gt;, with &lt;strong&gt;pre-check, no overwrite of same-named objects by default, and no auto-save&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;does &lt;strong&gt;not&lt;/strong&gt; run the block: you still call it from &lt;code&gt;OB1&lt;/code&gt; and wire real variables yourself;&lt;/li&gt;
&lt;li&gt;uses &lt;code&gt;http://localhost:8080&lt;/code&gt; by default, or &lt;code&gt;?tialink=&lt;/code&gt; appended to the page URL;&lt;/li&gt;
&lt;li&gt;accepts the &lt;code&gt;--api-token&lt;/code&gt; value if TiaLink was started with one — stored only in the browser's localStorage, never uploaded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"No overwrite" plus "no auto-save" keeps the risk on the reversible side: the import puts blocks into the project; whether they stay is still your decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Stress test: the three families people confuse
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 The timer trio — TON / TOF / TP
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;TON&lt;/code&gt; on-delay&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;TOF&lt;/code&gt; off-delay&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;TP&lt;/code&gt; pulse&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;When timing starts&lt;/td&gt;
&lt;td&gt;while &lt;code&gt;IN&lt;/code&gt; is 1&lt;/td&gt;
&lt;td&gt;when &lt;code&gt;IN&lt;/code&gt; goes 1 → 0&lt;/td&gt;
&lt;td&gt;on the rising edge of &lt;code&gt;IN&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When &lt;code&gt;Q&lt;/code&gt; turns on&lt;/td&gt;
&lt;td&gt;after &lt;code&gt;PT&lt;/code&gt; with &lt;code&gt;IN&lt;/code&gt; still 1&lt;/td&gt;
&lt;td&gt;immediately, while &lt;code&gt;IN&lt;/code&gt; is 1&lt;/td&gt;
&lt;td&gt;for a fixed &lt;code&gt;PT&lt;/code&gt; after the edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;IN&lt;/code&gt; drops mid-way&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Q&lt;/code&gt; and &lt;code&gt;ET&lt;/code&gt; reset instantly&lt;/td&gt;
&lt;td&gt;this is when timing begins&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;does not cut the pulse&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A one-scan pulse&lt;/td&gt;
&lt;td&gt;never reaches &lt;code&gt;PT&lt;/code&gt;; &lt;code&gt;Q&lt;/code&gt; never turns on&lt;/td&gt;
&lt;td&gt;is stretched to the full &lt;code&gt;PT&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;is the normal operating mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical use&lt;/td&gt;
&lt;td&gt;start delay, debounce, timeout alarm&lt;/td&gt;
&lt;td&gt;fan run-on, conveyor clearing&lt;/td&gt;
&lt;td&gt;fixed-width valve pulse, start pulse&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two pitfalls are mirror images of the same physical fact:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TON&lt;/code&gt;'s pitfall is &lt;strong&gt;"a short pulse never reaches &lt;code&gt;PT&lt;/code&gt;"&lt;/strong&gt; — so don't use it to catch pulses; use &lt;code&gt;TP&lt;/code&gt; or latch an edge.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TOF&lt;/code&gt;'s pitfall is &lt;strong&gt;"a short pulse is stretched to the full &lt;code&gt;PT&lt;/code&gt;"&lt;/strong&gt; — so don't use it as a pulse filter; it amplifies one scan into the whole preset time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You only see that pairing when the two pages sit next to each other.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TP&lt;/code&gt; also carries an honest footnote: &lt;strong&gt;a second rising edge during the pulse usually does not extend it, but confirm the behaviour on your specific CPU/firmware.&lt;/strong&gt; "I can't guarantee this in a document" is more useful than a confident wrong answer — it tells you to measure.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Bit logic — Coil / SCoil / RCoil / Sr / Rs
&lt;/h3&gt;

&lt;p&gt;Five ways to write a 1, five different semantics:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Instruction&lt;/th&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;th&gt;Behaviour&lt;/th&gt;
&lt;th&gt;How it resets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Coil&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;coil&lt;/td&gt;
&lt;td&gt;follows RLO every scan&lt;/td&gt;
&lt;td&gt;automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SCoil&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;coil&lt;/td&gt;
&lt;td&gt;sets and &lt;strong&gt;holds&lt;/strong&gt; while RLO = 1&lt;/td&gt;
&lt;td&gt;only &lt;code&gt;RCoil&lt;/code&gt; or writing 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RCoil&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;coil&lt;/td&gt;
&lt;td&gt;resets to 0 while RLO = 1&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;box&lt;/td&gt;
&lt;td&gt;set-dominant latch&lt;/td&gt;
&lt;td&gt;via the &lt;code&gt;r1&lt;/code&gt; input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Rs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;box&lt;/td&gt;
&lt;td&gt;reset-dominant latch&lt;/td&gt;
&lt;td&gt;reset wins when both are 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;Sr&lt;/code&gt;'s pitfall list is entirely semantic, and every item is a real defect: &lt;strong&gt;&lt;code&gt;operand&lt;/code&gt; cannot be an Output&lt;/strong&gt; (TIA does not allow reading outputs — use InOut or Static); &lt;strong&gt;don't confuse it with &lt;code&gt;SCoil&lt;/code&gt;/&lt;code&gt;RCoil&lt;/code&gt;&lt;/strong&gt; (box versus coil); &lt;strong&gt;pick the dominance according to the process&lt;/strong&gt;; &lt;code&gt;q&lt;/code&gt; is an RLO output you can continue in series; and &lt;strong&gt;scan order&lt;/strong&gt; — when a latch bit is written from several places, the result depends on scan order.&lt;/p&gt;

&lt;p&gt;That last one is really the thesis of ladder programming: &lt;strong&gt;write the same bit from several networks and you don't own a value — you own whatever the last network to reach it wrote.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Edges — PContact / PBox / PCoil / R_TRIG
&lt;/h3&gt;

&lt;p&gt;Edge instructions are the source of two production classics: the missed pulse and the double count. Eight pages split them by &lt;em&gt;what they watch&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Instruction&lt;/th&gt;
&lt;th&gt;Watches the edge of&lt;/th&gt;
&lt;th&gt;Instance needed&lt;/th&gt;
&lt;th&gt;Output duration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PContact&lt;/code&gt; / &lt;code&gt;NContact&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;an &lt;strong&gt;operand&lt;/strong&gt; (0→1 / 1→0)&lt;/td&gt;
&lt;td&gt;no, but needs an edge memory bit&lt;/td&gt;
&lt;td&gt;one scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PBox&lt;/code&gt; / &lt;code&gt;NBox&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the &lt;strong&gt;RLO on the left&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;no, edge memory bit&lt;/td&gt;
&lt;td&gt;one scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PCoil&lt;/code&gt; / &lt;code&gt;NCoil&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;RLO edge, writes an operand&lt;/td&gt;
&lt;td&gt;no, edge memory bit&lt;/td&gt;
&lt;td&gt;one scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;R_TRIG&lt;/code&gt; / &lt;code&gt;F_TRIG&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;CLK&lt;/code&gt;&lt;/strong&gt;, &lt;code&gt;Q&lt;/code&gt; pulses&lt;/td&gt;
&lt;td&gt;yes, an instance&lt;/td&gt;
&lt;td&gt;one scan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;PContact&lt;/code&gt; explains the mechanism: the &lt;code&gt;bit&lt;/code&gt; is edge memory that stores the operand's previous state, and &lt;strong&gt;each edge contact needs its own &lt;code&gt;bit&lt;/code&gt;&lt;/strong&gt;; &lt;code&gt;out = 1&lt;/code&gt; only in the scan where the operand goes 0 → 1, and &lt;code&gt;bit&lt;/code&gt; updates at the end of the scan.&lt;/p&gt;

&lt;p&gt;Its pitfalls double as a troubleshooting checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No preceding logic&lt;/strong&gt;: putting &lt;code&gt;PContact&lt;/code&gt; first in a rung violates TIA's edge rules — place an &lt;code&gt;Enable&lt;/code&gt; / &lt;code&gt;Not&lt;/code&gt; / contact before it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared edge bits&lt;/strong&gt;: two &lt;code&gt;PContact&lt;/code&gt; sharing one &lt;code&gt;bit&lt;/code&gt; overwrite each other and drop pulses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-retentive edge bits&lt;/strong&gt;: a &lt;code&gt;bit&lt;/code&gt; in Temp is reset on every call — use Static, a global &lt;code&gt;M&lt;/code&gt;, or a DB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Used as a level&lt;/strong&gt;: it is high for exactly one scan; if you need persistence, follow it with &lt;code&gt;SCoil&lt;/code&gt; or a latch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipped networks&lt;/strong&gt;: if a &lt;code&gt;Jump&lt;/code&gt; skips the network, the edge isn't recorded — and when the scan returns, the edge may be misinterpreted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is rarely written down anywhere, yet it's the standard path by which "code that never executes" turns into "an occasional unexpected motion".&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Three ways to use it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Firefighting (the 30-second rule).&lt;/strong&gt; One sentence → pin table → pitfalls. Skip the rest. The one-sentence line tells you whether the semantic is the one you want, the pins confirm the wiring, the pitfalls confirm whether your current code &lt;em&gt;is&lt;/em&gt; the failure shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Studying a group (read the pitfalls as a comparison table).&lt;/strong&gt; A single page doesn't stick, but read all ten timer pages' pitfalls and you get a map of &lt;em&gt;when not to use which timer&lt;/em&gt;; read the eight edge pages and you get a leak checklist. That's more durable than memorising symbols.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feeding pages to an AI as a checklist.&lt;/strong&gt; This is the biggest change in how the work is done now. When an AI generates or modifies ladder/SCL, the errors concentrate in four places: &lt;strong&gt;wrong timer family, shared edge bits, double coils, instance conflicts&lt;/strong&gt; — and the pitfalls section of each page is a ready-made checklist. Practically: paste the relevant page (summary + pins + mechanism + pitfalls) as context and ask the model to check the code against it, instead of generating from memory.&lt;/p&gt;

&lt;p&gt;Two companion pieces in the same series: &lt;a href="https://www.hubwiz.com/blog/ai-plc-programming-7-lessons-verification/" rel="noopener noreferrer"&gt;7 lessons from AI PLC programming&lt;/a&gt; (the four verification layers — structural validation → TIA compile → cloud simulation → virtual PLC/field — and what each layer does &lt;em&gt;not&lt;/em&gt; prove) and &lt;a href="https://www.hubwiz.com/blog/siemens-plc-ladder-example-library/" rel="noopener noreferrer"&gt;the Siemens ladder example library&lt;/a&gt; (how 60+ examples are arranged into a practice path). Both are in Chinese.&lt;/p&gt;

&lt;p&gt;⚠️ Keep the handbook's own disclaimer in view: &lt;strong&gt;instruction pages cover semantics and usage only; they do not replace the TIA help or a process/safety review. Safety-related conclusions require human confirmation.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. From "found it" to "ran it" in four steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Look it up&lt;/strong&gt; — confirm semantics and pitfalls on the instruction page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import&lt;/strong&gt; — use the "try it in TIA" panel to pull an example block (e.g. &lt;code&gt;TON&lt;/code&gt; → &lt;em&gt;timeout alarm&lt;/em&gt;) into your open project via TiaLink: pre-check, no overwrite, no auto-save.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wire it&lt;/strong&gt; — call the block from &lt;code&gt;OB1&lt;/code&gt; and connect real variables. It does not run by itself; this step &lt;em&gt;is&lt;/em&gt; the pin comprehension.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove it&lt;/strong&gt; — run the timing in cloud simulation, then watch IO behaviour on a virtual PLC. For the layer split, see &lt;a href="https://www.hubwiz.com/blog/ai-plc-programming-7-lessons-verification/" rel="noopener noreferrer"&gt;7 lessons&lt;/a&gt;; for practice without hardware, see &lt;a href="https://www.hubwiz.com/blog/virtual-plc-lab-factory-io-openplc-modbus-tcp/" rel="noopener noreferrer"&gt;Factory I/O + OpenPLC + Modbus TCP&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compared to pasting a snippet into your project, only step 3 is extra work — and it's what buys you the knowledge of &lt;em&gt;which scan cycle each line runs in&lt;/em&gt;. The classic counter-example: during a refactor &lt;code&gt;TP&lt;/code&gt; was written as &lt;code&gt;TON&lt;/code&gt;; compile reported 0 errors and 0 warnings; pressing the button started nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. What it deliberately doesn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't replace the TIA help.&lt;/strong&gt; Semantics, examples and pitfalls live here; firmware and CPU specifics stay with the official documentation and your own measurements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't replace a process review.&lt;/strong&gt; Especially for safety (e-stops, safety doors, F-CPUs and F-blocks) — the pages state plainly that safety conclusions require human confirmation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The scope is S7-1200/1500 on TIA V19.&lt;/strong&gt; S7-300/400-era S5 timers (&lt;code&gt;S_ODT&lt;/code&gt; and friends) are a different semantic family; don't transplant them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown behaviour is labelled unknown.&lt;/strong&gt; Where a cross-version detail can't be guaranteed (e.g. &lt;code&gt;TP&lt;/code&gt; retriggering mid-pulse), the page says "confirm on your CPU" instead of inventing an answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Examples are minimal readable shapes, not production code.&lt;/strong&gt; No process interlocks, no fault handling — add them yourself and review them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import is not adoption.&lt;/strong&gt; No overwrite, no auto-save: the decision stays on your side. That's protection and responsibility at the same time.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Sources and verification notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Handbook index (113 instructions, 13 groups): &lt;a href="https://tia.hubwiz.com/learn-lad/instructions/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=lad-instruction-handbook-siemens" rel="noopener noreferrer"&gt;tia.hubwiz.com/learn-lad/instructions/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Instruction pages quoted: &lt;a href="https://tia.hubwiz.com/learn-lad/instructions/ton/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=lad-instruction-handbook-siemens" rel="noopener noreferrer"&gt;TON&lt;/a&gt;, &lt;a href="https://tia.hubwiz.com/learn-lad/instructions/tof/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=lad-instruction-handbook-siemens" rel="noopener noreferrer"&gt;TOF&lt;/a&gt;, &lt;a href="https://tia.hubwiz.com/learn-lad/instructions/tp/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=lad-instruction-handbook-siemens" rel="noopener noreferrer"&gt;TP&lt;/a&gt;, &lt;a href="https://tia.hubwiz.com/learn-lad/instructions/ctu/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=lad-instruction-handbook-siemens" rel="noopener noreferrer"&gt;CTU&lt;/a&gt;, &lt;a href="https://tia.hubwiz.com/learn-lad/instructions/scoil/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=lad-instruction-handbook-siemens" rel="noopener noreferrer"&gt;SCoil&lt;/a&gt;, &lt;a href="https://tia.hubwiz.com/learn-lad/instructions/sr/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=lad-instruction-handbook-siemens" rel="noopener noreferrer"&gt;Sr&lt;/a&gt;, &lt;a href="https://tia.hubwiz.com/learn-lad/instructions/pcontact/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=lad-instruction-handbook-siemens" rel="noopener noreferrer"&gt;PContact&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Page structure, counts, pin tables, pitfalls and import behaviour were read from the live pages on 2026-10-22. Nothing here was independently re-measured on hardware; instruction semantics remain governed by the TIA help and the process/safety review.&lt;/li&gt;
&lt;li&gt;Cover photo: Pexels #257736 (Pexels License).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further reading (in Chinese)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/ai-industrial-stack/" rel="noopener noreferrer"&gt;AI × Industrial / PLC guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/siemens-plc-ladder-example-library/" rel="noopener noreferrer"&gt;Siemens ladder example library: 60+ importable exercises&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/ai-plc-programming-7-lessons-verification/" rel="noopener noreferrer"&gt;7 lessons from AI PLC programming, each with an acceptance check&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/lad-to-scl-migration-guide-tia-portal/" rel="noopener noreferrer"&gt;Ladder to SCL migration guide for TIA Portal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/ai-plc-program-review-lesson/" rel="noopener noreferrer"&gt;Pre-commissioning program review with an AI agent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/virtual-plc-lab-factory-io-openplc-modbus-tcp/" rel="noopener noreferrer"&gt;Practicing PLC without hardware: Factory I/O + OpenPLC&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article was originally published in Chinese on &lt;a href="https://www.hubwiz.com/blog/lad-instruction-handbook-siemens/" rel="noopener noreferrer"&gt;hubwiz.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>plc</category>
      <category>automation</category>
      <category>programming</category>
      <category>tooling</category>
    </item>
    <item>
      <title>image-blaster: the real cost of one image 3D world</title>
      <dc:creator>tia-forge</dc:creator>
      <pubDate>Wed, 21 Oct 2026 15:12:45 +0000</pubDate>
      <link>https://dev.to/tiaforge/image-blaster-the-real-cost-of-one-image-3d-world-1n4d</link>
      <guid>https://dev.to/tiaforge/image-blaster-the-real-cost-of-one-image-3d-world-1n4d</guid>
      <description>&lt;p&gt;&lt;strong&gt;One image in, a walkable 3D environment out in under five minutes.&lt;/strong&gt; That is the pitch on &lt;a href="https://github.com/neilsonnn/image-blaster" rel="noopener noreferrer"&gt;&lt;code&gt;neilsonnn/image-blaster&lt;/code&gt;&lt;/a&gt; — 11.8k stars, MIT, front page of Hacker News in May 2026. Built by a World Labs team member (not an official World Labs product), it chains &lt;strong&gt;Marble&lt;/strong&gt;, &lt;strong&gt;Hunyuan 3D&lt;/strong&gt;, &lt;strong&gt;nano-banana&lt;/strong&gt; and &lt;strong&gt;ElevenLabs&lt;/strong&gt; behind a Claude Code skill.&lt;/p&gt;

&lt;p&gt;Two questions decide whether you can use it, and the eight English write-ups I could find answer neither:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What does one run actually cost?&lt;/strong&gt; (Short answer: the world is fixed at ~$1.34; every dynamic object adds ~$0.46.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can you drop the outputs into an engine?&lt;/strong&gt; (Short answer: not before you apply a scale correction the CLI never applies for you.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both answers are below, with code references and prices as of 2026-10-21.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually is
&lt;/h2&gt;

&lt;p&gt;Not an app — a &lt;strong&gt;skill pack for Claude Code&lt;/strong&gt;. You clone the repo, run &lt;code&gt;claude&lt;/code&gt; inside it, hand Claude two API keys (World Labs + FAL), drop an image into &lt;code&gt;input/&lt;/code&gt;, and say &lt;code&gt;blast it and confirm each step with me&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repo&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;neilsonnn/image-blaster&lt;/code&gt;, MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeline&lt;/td&gt;
&lt;td&gt;created 2026-04-21, last push 2026-05-15; 11,794 ★ (snapshot 2026-10-21)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.claude/skills/*&lt;/code&gt; + Node scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requires&lt;/td&gt;
&lt;td&gt;Claude Code CLI + &lt;strong&gt;World Labs key&lt;/strong&gt; + &lt;strong&gt;FAL key&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outputs&lt;/td&gt;
&lt;td&gt;dynamic objects &lt;code&gt;.glb&lt;/code&gt;/&lt;code&gt;.obj&lt;/code&gt;; static environment &lt;code&gt;.spz&lt;/code&gt;; SFX &lt;code&gt;.mp3&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three outputs are &lt;strong&gt;not the same kind of thing&lt;/strong&gt;, and that is where every downstream problem starts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;What it is not&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Static environment&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.spz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a Gaussian-splat scene you can walk through&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;not a mesh&lt;/strong&gt; — no faces, no UVs; needs splat render support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic objects&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.glb&lt;/code&gt; / &lt;code&gt;.obj&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;one real mesh per object&lt;/td&gt;
&lt;td&gt;not a scene — you place them yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SFX&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.mp3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ambient loop + per-object impact&lt;/td&gt;
&lt;td&gt;not a mixed audio system&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pipeline, as it actually executes (read from the scripts, not the README):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input/ image
  ├─① clean plate / object cut-outs   fal-ai/nano-banana-2/edit          $0.08/img
  ├─② static environment             World Labs marble-1.1              1,500 credits
  │     └→ .spz (full_res + 500k + 100k + 150k downloaded)
  │     └→ collider mesh .glb (100–200k triangles)
  ├─③ each dynamic object            fal-ai/hunyuan-3d/v3.1/pro/...     $0.375/call
  │                                  (or .../rapid $0.225, fal-ai/meshy/v6 $0.80)
  └─④ SFX                            fal-ai/elevenlabs/sound-effects/v2 $0.002/sec
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details that matter and are easy to miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every object costs one image edit &lt;em&gt;before&lt;/em&gt; the 3D call.&lt;/strong&gt; The &lt;code&gt;image-blast-3d&lt;/code&gt; skill instructs Claude to first write a cut-out prompt ("Isolate the &lt;code&gt;&amp;lt;object&amp;gt;&lt;/code&gt; … white background, centered, tight crop"). So an object is &lt;em&gt;edit + 3D&lt;/em&gt;, not &lt;em&gt;3D&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SFX go through FAL&lt;/strong&gt;, not ElevenLabs directly (&lt;code&gt;fal-ai/elevenlabs/sound-effects/v2&lt;/code&gt;). If you price this off ElevenLabs credit tables you will be off by an order of magnitude. The skill's defaults: ambient &lt;code&gt;--loop --count 2 --duration-seconds 10&lt;/code&gt;, per object &lt;code&gt;--count 4 --duration-seconds 1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bill
&lt;/h2&gt;

&lt;p&gt;Prices below are the vendors' own pages, checked 2026-10-21.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;World Labs credits&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$1.00 = 1,250 credits&lt;/strong&gt;; minimum purchase $5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docs.worldlabs.ai/api/pricing&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;World generation &lt;code&gt;marble-1.1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,500 credits = $1.20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pano generation (non-pano image)&lt;/td&gt;
&lt;td&gt;+80 credits = &lt;strong&gt;$0.064&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HQ mesh export&lt;/td&gt;
&lt;td&gt;3,500 credits = &lt;strong&gt;$2.80&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;same (&lt;strong&gt;this pipeline never calls it&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image edit &lt;code&gt;nano-banana-2/edit&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0.08 / image&lt;/strong&gt; (2K/4K ×1.5)&lt;/td&gt;
&lt;td&gt;FAL model page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object 3D &lt;code&gt;hunyuan-3d/v3.1/pro&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.375 / generation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FAL model page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object 3D &lt;code&gt;.../rapid&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.225 / generation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FAL model page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object 3D &lt;code&gt;meshy/v6&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.80 / generation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FAL model page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SFX &lt;code&gt;elevenlabs/sound-effects/v2&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.002 / second&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FAL model page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Which reduces to a very simple structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fixed per run   = world 1.264 (1,500 + 80 credits) + one plate edit 0.08   ≈ $1.34
per object      = cut-out edit 0.08 + 3D 0.375 + impact SFX 4s × 0.002     ≈ $0.46
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scene&lt;/th&gt;
&lt;th&gt;Math&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 object&lt;/td&gt;
&lt;td&gt;1.34 + 0.46&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ $1.81&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 objects (one room)&lt;/td&gt;
&lt;td&gt;1.34 + 5 × 0.46&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ $3.66&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20 objects (street / level)&lt;/td&gt;
&lt;td&gt;1.34 + 20 × 0.46&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ $10.60&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20 objects on &lt;code&gt;rapid&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1.34 + 20 × (0.08 + 0.225 + 0.008)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ $7.62&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20 objects on &lt;code&gt;meshy v6&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1.34 + 20 × (0.08 + 0.80 + 0.008)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ $19.10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three non-obvious consequences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The world is the cheap part.&lt;/strong&gt; Marble renders an entire environment for $1.26; five small props cost twice that. Your budget is a function of &lt;em&gt;object count&lt;/em&gt;, not canvas size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The $2.80 HQ mesh export is not in the loop.&lt;/strong&gt; &lt;code&gt;generate-world.mjs&lt;/code&gt; only pulls &lt;code&gt;assets.mesh.collider_mesh_url&lt;/code&gt; — the 100–200k-triangle &lt;em&gt;collision&lt;/em&gt; mesh. If you want a static mesh you can actually look at, you have to go back to Marble and export the HQ version separately: ~600k triangles with textures (or ~1M with vertex colors), up to an hour, rate-limited to 4/hour, Pro plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failures are billable.&lt;/strong&gt; Unhappy with face count? Re-run: another $0.375. Bad cut-out? Another $0.08. This is exactly why the skill's default posture is &lt;code&gt;confirm each step with me&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Scope: these are &lt;strong&gt;estimates derived from the pipeline structure and public unit prices&lt;/strong&gt;, not our own invoice. No retries, no plan differences (Free can generate worlds but cannot export; Standard covers splat/collider exports; Pro adds HQ mesh + commercial rights).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The part nobody writes about: the &lt;code&gt;.spz&lt;/code&gt; is not in metric space
&lt;/h2&gt;

&lt;p&gt;World Labs documents this in &lt;code&gt;api/rendering-spz&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Marble SPZ files are exported in the generated asset coordinate frame. When you render them outside Marble, use &lt;code&gt;assets.splats.semantics_metadata&lt;/code&gt; to convert Gaussian centers and Gaussian sizes into metric, ground-aligned coordinates.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;metric_scale_factor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;raw generated units → &lt;strong&gt;meters&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ground_plane_offset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;puts the metric ground plane at &lt;strong&gt;y = 0&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;flip_y&lt;/code&gt; (optional)&lt;/td&gt;
&lt;td&gt;Y-axis orientation (SPZ defaults to RUB, Unreal is Z-up)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So: is it handled? I grepped the whole repo (2026-10-21, &lt;code&gt;main&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Reads &lt;code&gt;semantics_metadata&lt;/code&gt;?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;app/src/components/WorldViewer.tsx&lt;/code&gt; (bundled React viewer)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; — &lt;code&gt;metric_scale_factor&lt;/code&gt;, &lt;code&gt;ground_plane_offset&lt;/code&gt;, &lt;code&gt;flip_y&lt;/code&gt;, defaults 1 / 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.claude/scripts/**&lt;/code&gt; (the CLI pipeline that hands you the files)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; — downloads &lt;code&gt;spz_urls&lt;/code&gt; and &lt;code&gt;collider_mesh_url&lt;/code&gt;, nothing else&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Which means: &lt;strong&gt;it looks right in the bundled viewer because the viewer applies the correction, and it is wrong in Unity / Unreal / Blender / three.js because the exported files don't carry it.&lt;/strong&gt; Symptoms: a door 30+ units tall, everything floating above or sunk into the floor, and props misaligned against the splat they were generated from.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// three.js: the loader reads the file, it does not fix the frame&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;world&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SPZLoader&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;loadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;semantics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metric_scale_factor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;world&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setScalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;world&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;semantics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ground_plane_offset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the same factor to the collider mesh (&lt;em&gt;inference&lt;/em&gt; — the docs only spell this out for splats, but both come from the same generated frame).&lt;/p&gt;

&lt;h2&gt;
  
  
  What the outputs really are
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Official spec&lt;/th&gt;
&lt;th&gt;Who reads it natively&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Splat &lt;code&gt;.spz&lt;/code&gt; (full res)&lt;/td&gt;
&lt;td&gt;~2M splats&lt;/td&gt;
&lt;td&gt;three.js (&lt;code&gt;SPZLoader&lt;/code&gt; in addons); Unity / Unreal / Blender / Houdini need community splat plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Splat &lt;code&gt;.spz&lt;/code&gt; (low res)&lt;/td&gt;
&lt;td&gt;~500k / 100k / 150k&lt;/td&gt;
&lt;td&gt;same; lower res is what you actually want in-engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Splat &lt;code&gt;.ply&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;~2M / ~500k&lt;/td&gt;
&lt;td&gt;widest compatibility, much bigger — SPZ is roughly &lt;strong&gt;10× smaller&lt;/strong&gt; than the equivalent PLY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collider &lt;code&gt;.glb&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;100–200k triangles&lt;/td&gt;
&lt;td&gt;any engine; it is a physics proxy, not a render mesh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HQ &lt;code&gt;.glb&lt;/code&gt; (Pro)&lt;/td&gt;
&lt;td&gt;~600k textured / ~1M vertex-color&lt;/td&gt;
&lt;td&gt;any engine; &lt;strong&gt;not produced by this pipeline&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Ten-minute acceptance checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;th&gt;If it fails&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Units are meters&lt;/td&gt;
&lt;td&gt;measure a known reference (2 m door)&lt;/td&gt;
&lt;td&gt;apply &lt;code&gt;metric_scale_factor&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Ground at y = 0&lt;/td&gt;
&lt;td&gt;splat floor on the zero plane&lt;/td&gt;
&lt;td&gt;subtract &lt;code&gt;ground_plane_offset&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Orientation&lt;/td&gt;
&lt;td&gt;upside down → &lt;code&gt;flip_y&lt;/code&gt;; rotated → world yaw&lt;/td&gt;
&lt;td&gt;flip / rotate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Splat and mesh share scale&lt;/td&gt;
&lt;td&gt;overlay the same chair twice&lt;/td&gt;
&lt;td&gt;use one factor for both&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Face count holds up close&lt;/td&gt;
&lt;td&gt;zoom to player distance&lt;/td&gt;
&lt;td&gt;re-run with &lt;code&gt;--face-count&lt;/code&gt; (billed again)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Collider ≠ render mesh&lt;/td&gt;
&lt;td&gt;100–200k tri file is the collider&lt;/td&gt;
&lt;td&gt;keep both, don't swap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;File size&lt;/td&gt;
&lt;td&gt;prefer low-res &lt;code&gt;.spz&lt;/code&gt; in-engine&lt;/td&gt;
&lt;td&gt;see the payload comparison link below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Licensing&lt;/td&gt;
&lt;td&gt;code is MIT; &lt;strong&gt;assets are generated by third-party APIs&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Marble commercial rights sit on Pro; check each vendor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Four things that will bite you
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Static/dynamic split errors → ghosting.&lt;/strong&gt; The splat already contains the chair; generate a mesh of the chair and now you have two. Wrong split = double image, and an extra object you didn't want costs $0.46.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The splat/mesh boundary leaks.&lt;/strong&gt; Splats have no geometry, cast no shadows, don't receive light like meshes. Contact shadows and occlusion at the seam won't match. &lt;em&gt;(Inference: add AO blob decals under meshes, or cover the contact band with mesh.)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 50k default is the skill's, not the API's.&lt;/strong&gt; Hunyuan's own default is 500,000 faces. Background props are fine; hero objects look generated up close.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SFX are materials, not an audio system.&lt;/strong&gt; Two 10-second ambient loops, four 1-second impacts per object. No rolling, no sustained sounds, no material variation. Non-loop output gets trimmed and loudness-normalized by &lt;code&gt;ffmpeg&lt;/code&gt;; loop output is left raw for seamlessness.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When not to use it
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You want&lt;/th&gt;
&lt;th&gt;Why not&lt;/th&gt;
&lt;th&gt;Instead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One model, no scene&lt;/td&gt;
&lt;td&gt;the $1.34 world is wasted&lt;/td&gt;
&lt;td&gt;call the FAL model directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Precise dimensions&lt;/td&gt;
&lt;td&gt;generation isn't dimension-driven&lt;/td&gt;
&lt;td&gt;parametric CAD workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editable topology / clean UVs&lt;/td&gt;
&lt;td&gt;you get "scanned" topology&lt;/td&gt;
&lt;td&gt;rebuild from the form (Mesh2CAD route)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline / on-prem&lt;/td&gt;
&lt;td&gt;the whole chain is two cloud APIs&lt;/td&gt;
&lt;td&gt;local text-to-3D (e.g. TRELLIS.2 in-browser)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team doesn't use Claude Code&lt;/td&gt;
&lt;td&gt;it's a skill pack, not a GUI app&lt;/td&gt;
&lt;td&gt;drive the scripts yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean commercial rights&lt;/td&gt;
&lt;td&gt;assets come from multiple vendors&lt;/td&gt;
&lt;td&gt;verify each license&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one thing it is genuinely good at: &lt;strong&gt;you have an image — concept art, a film still, a photo of a room — and you need a prototype you can walk around in within five minutes&lt;/strong&gt; to align the space, test framing, or show a client. For that, it beats hand-building the scene by a wide margin.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Scope and sources: repo README, `.claude/skills/&lt;/em&gt;&lt;code&gt; and &lt;/code&gt;.claude/scripts/&lt;em&gt;&lt;code&gt; (2026-10-21, &lt;/code&gt;main&lt;code&gt;); World Labs docs (&lt;/code&gt;api/pricing&lt;code&gt;, &lt;/code&gt;marble/export/specs&lt;code&gt;, &lt;/code&gt;marble/export/gaussian-splat&lt;code&gt;, &lt;/code&gt;api/rendering-spz&lt;code&gt;); FAL model pages; Niantic &lt;/code&gt;spz&lt;code&gt; format notes and three.js &lt;/code&gt;SPZLoader`. **No end-to-end run was performed&lt;/em&gt;* — costs are structural estimates from public unit prices, and mesh/engine/license constraints come from documentation rather than our own tests. The author is a World Labs team member; this is their project, not an official World Labs product.*&lt;/p&gt;

&lt;p&gt;&lt;em&gt;More from us: &lt;a href="https://bimant.com/blog/image-blaster-image-to-3d-world-skill/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=image-blaster" rel="noopener noreferrer"&gt;the full Chinese write-up with the same billing breakdown&lt;/a&gt; · &lt;a href="https://bimant.com/blog/ai-3d/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=image-blaster" rel="noopener noreferrer"&gt;AI × 3D guides on BimAnt&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>3d</category>
      <category>gamedev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Compiling Clean Isn't Proof: Have AI Review a PLC Program Before You Commission It</title>
      <dc:creator>tia-forge</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:40:08 +0000</pubDate>
      <link>https://dev.to/tiaforge/compiling-clean-isnt-proof-have-ai-review-a-plc-program-before-you-commission-it-17fn</link>
      <guid>https://dev.to/tiaforge/compiling-clean-isnt-proof-have-ai-review-a-plc-program-before-you-commission-it-17fn</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: The most dangerous illusion when you inherit a TIA Portal program is "it compiles with 0 errors and 0 warnings, so it's fine." The compiler checks syntax and consistency — not cross-network logic bugs like "the infeed conveyor runs when no box is present." Lesson 5 of the free, text-based &lt;a href="https://tia.hubwiz.com/learn-lad/ai/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=ai-plc-program-review-lesson" rel="noopener noreferrer"&gt;PLC AI in Practice&lt;/a&gt; course series (&lt;a href="https://tia.hubwiz.com/learn-lad/ai/plc-program-review/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=ai-plc-program-review-lesson" rel="noopener noreferrer"&gt;Lesson 5 here&lt;/a&gt;, ~35 minutes) closes exactly that gap: it has AI perform a &lt;strong&gt;read-only&lt;/strong&gt; program review against a fixed checklist, and produce an evidence-backed, severity-graded, reproducible review report before commissioning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this lesson sits in the series
&lt;/h2&gt;

&lt;p&gt;"PLC AI in Practice" is a free, text-and-figure course for TIA Portal engineers. Its main thread breaks "daring to let AI touch a real project" into individually verifiable steps: understand a legacy project → assess change impact → write a block to a clean compile → get the program running in a virtual PLC. Lesson 5 is the last gate before commissioning: &lt;strong&gt;what in this program will go wrong, and what do you fix first?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The lesson deliberately separates "program review" from "code commentary." Asking an AI to review a PLC program with one sentence ("review this code please") gets you a pile of style notes — naming, comments, network splitting. All of that sounds reasonable, but not a single line of it &lt;em&gt;proves the machine will stop when it should&lt;/em&gt;. The course defines review strictly: &lt;strong&gt;read-only, checklist-driven, defects only if they come with a failure scenario&lt;/strong&gt;. Style preferences and candidates without failure scenarios are kept out of "findings."&lt;/p&gt;

&lt;h2&gt;
  
  
  The method: one task brief, four gates
&lt;/h2&gt;

&lt;p&gt;The core deliverable of the lesson isn't knowledge — it's a reusable &lt;strong&gt;review task brief&lt;/strong&gt; you can paste to an AI agent. Its structure is worth describing in its own right:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gate 1 — what to review.&lt;/strong&gt; A fixed 7-category checklist: structure &amp;amp; calls; state &amp;amp; retention (set/reset pairs, power-up recovery); timing &amp;amp; scan (double coils, edge storage, timer types); interlocks &amp;amp; safety; data &amp;amp; addressing (type/range, I/O addresses vs. hardware channels); maintainability; testability. The checklist stops the agent from reviewing only what it is comfortable with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gate 2 — evidence.&lt;/strong&gt; Every candidate finding must carry seven elements: evidence (block + network + tag), the failure scenario (which inputs/timing/hardware state triggers it and what happens), a severity grade (P0 downtime/safety → P3 needs confirmation), confidence (confirmed / inferred / cannot confirm), a minimal-change fix suggestion, a verification method, and — critically — a &lt;strong&gt;counter-evidence pass&lt;/strong&gt;: is there another network, an external write, or a process requirement that makes this conclusion wrong? "No failure scenario, no findings section" is a hard constraint in the brief.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gate 3 — global view.&lt;/strong&gt; The lesson builds an &lt;strong&gt;output → writers table&lt;/strong&gt;: for every output, list every network and instruction that writes it. Multi-driver outputs, broken set/reset pairs, and scan-order issues are invisible if you look at one network at a time; you have to reconcile per output, globally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gate 4 — read-only.&lt;/strong&gt; During review, no object is modified and the project is not saved; the report ends with a read-only declaration. Review first, produce the report; fixing is a separate, later process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The course actually ran it: target and scoreboard
&lt;/h2&gt;

&lt;p&gt;This isn't a pure methodology lesson. It runs the review against the &lt;strong&gt;Sorting Boxes (Factory IO)&lt;/strong&gt; open-source example: an OB1 with 18 ladder networks plus one SCL communications FC, 31 tags, and 5 system instance DBs. The example deliberately preserves real defects from the original project — ideal review practice material.&lt;/p&gt;

&lt;p&gt;Per the lesson's own published results (I have not independently re-run them; treat the numbers as the course's):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2 × P1&lt;/strong&gt;: right-hand sorting doesn't track box type — with a left-type box, both chains can fire simultaneously; and the infeed conveyor is driven by three networks, so it runs when no box is present, and its "stop" doesn't hold;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2 × P2&lt;/strong&gt;: the ejector gets a pulse exactly one scan period long (10 ms) — an actuator may never see it; plus a compiler warning "inputs or outputs used that do not exist in the configured hardware";&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 × P3&lt;/strong&gt;: no e-stop / safety logic inside the program — explicitly marked "cannot confirm": the original machine may have a hardwired safety circuit, and that's for electrical drawings and a safety review to decide;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 false positive, eliminated by counter-evidence&lt;/strong&gt;: the exported SCL text contains concatenation artifacts that look like compile failures — but when TIA actually compiles the block, it's 0 errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last item is, to me, the most valuable part of the whole lesson. &lt;strong&gt;The biggest risk of AI-assisted review is not missing a defect; it's treating "looks like a problem" as "confirmed defect."&lt;/strong&gt; The lesson dedicates an entire step to counter-evidence: for every candidate, ask three questions — is there another network or external write that reverses the conclusion? Is the signal maintained or a pulse — does the failure scenario still hold? Is there a process requirement that makes this intentional? Three scary-looking candidates get downgraded or eliminated that way, and only the provable ones survive into the report.&lt;/p&gt;

&lt;p&gt;This is a general lesson for anyone using AI to review code (well beyond PLC): &lt;strong&gt;trust the compiler for compile results — never substitute text export for an actual compile, and always keep "confirmed" and "inferred" as separate labels.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Compiling, simulation, and humans are not interchangeable
&lt;/h2&gt;

&lt;p&gt;The course hammers on the boundaries between three verification methods: &lt;strong&gt;compiling&lt;/strong&gt; catches syntax and consistency, but not logic errors; &lt;strong&gt;simulation / scan-by-scan reasoning&lt;/strong&gt; validates logical behavior, but not wiring, physical channels, or safety circuits; &lt;strong&gt;humans&lt;/strong&gt; own process intent, hardware configuration, and safety review. Every finding must state its verification method — none of the three may impersonate another.&lt;/p&gt;

&lt;p&gt;Safety is treated with particular rigor: the course's stance is "AI assists the review; conclusions must be human-verified." E-stop and safety functions must be confirmed by qualified personnel; the report is not a safety certification, and "the program doesn't contain it" must never be inverted into "the machine is unsafe." That red line matches what we've argued on this blog all along — AI assists, humans gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it's for, and what you need
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Good fit&lt;/strong&gt;: electrical/automation engineers inheriting someone else's (or their own past) TIA Portal projects; teams wanting AI in code review without knowing how to constrain it; anyone who wants to turn the 7-category checklist into a team review template — the course explicitly suggests using "7 categories + evidence + counter-evidence + severity" as an MR/review template. It also recommends taking one of &lt;em&gt;your own&lt;/em&gt; legacy projects, running the brief against it read-only, and only then deciding what's worth fixing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;: a Windows machine with TIA Portal V19, &lt;a href="https://tia.hubwiz.com/tia-link?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=ai-plc-program-review-lesson" rel="noopener noreferrer"&gt;TiaLink&lt;/a&gt; (an MCP gateway that connects TIA Portal to your AI agent; it runs locally, so project data stays in the plant), and any Streamable-HTTP MCP client (Claude Code, Cursor, VS Code, ...). The lesson is ~35 minutes, free, no videos, nothing to buy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not for you&lt;/strong&gt; if you're looking for a one-click "AI will optimize my code" button — the methodology here points the other way: zero changes during review, human verification of every finding before any fix enters a change process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading (all in Chinese, on our blog)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.hubwiz.com/blog/ai-plc-programming-7-lessons-verification/" rel="noopener noreferrer"&gt;AI PLC Programming: 7 Free Hands-On Lessons, Each With a Verifiable Gate&lt;/a&gt; — our overview of the whole 7-lesson series;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.hubwiz.com/blog/taking-over-legacy-plc-program-4-steps/" rel="noopener noreferrer"&gt;Taking Over a Legacy PLC Program: A 4-Step Forensics Routine&lt;/a&gt; — evidence gathering and baselining &lt;em&gt;before&lt;/em&gt; the review;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.hubwiz.com/blog/siemens-plc-ladder-example-library/" rel="noopener noreferrer"&gt;Learn-LAD Ladder Example Library&lt;/a&gt; — where the Sorting Boxes example comes from;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.hubwiz.com/blog/tia-portal-openness-mcp-write-guards/" rel="noopener noreferrer"&gt;AI Editing TIA Portal Projects: Write Protection in TIA Openness MCP Tools&lt;/a&gt; — write-protection design patterns when letting AI modify projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd rather read the primary source: &lt;a href="https://tia.hubwiz.com/learn-lad/ai/plc-program-review/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=ai-plc-program-review-lesson" rel="noopener noreferrer"&gt;Lesson 5, in Chinese, at Learn-LAD&lt;/a&gt; — the task brief and the report template are copy-paste ready.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: all measured numbers above (2×P1, 2×P2, 1×P3, 1 eliminated false positive; 18 networks; 31 tags; 5 system instance DBs; 10 ms pulse) come from the course page's own published run, not from an independent re-test by this publication. This article is a syndicated English translation of &lt;a href="https://www.hubwiz.com/blog/ai-plc-program-review-lesson/" rel="noopener noreferrer"&gt;our original Chinese post&lt;/a&gt; and originally appeared there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>plc</category>
      <category>automation</category>
      <category>programming</category>
    </item>
    <item>
      <title>DeepInfer: What's Actually Inside DeepSeek's Newly Open-Sourced Inference Engine</title>
      <dc:creator>tia-forge</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:13:46 +0000</pubDate>
      <link>https://dev.to/tiaforge/deepinfer-whats-actually-inside-deepseeks-newly-open-sourced-inference-engine-35m1</link>
      <guid>https://dev.to/tiaforge/deepinfer-whats-actually-inside-deepseeks-newly-open-sourced-inference-engine-35m1</guid>
      <description>&lt;p&gt;On October 20, 2026, DeepSeek open-sourced &lt;strong&gt;DeepInfer&lt;/strong&gt; — the inference framework it says powers its own large-scale production services — under the Apache 2.0 license. The repo is &lt;code&gt;deepseek-ai/DeepInfer&lt;/code&gt;, and it crossed 10,000 GitHub stars within days of the announcement (as of writing, ~10.5k stars / 926 forks).&lt;/p&gt;

&lt;p&gt;The one-line version from the official account:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Introducing DeepInfer, our in-house inference framework, now fully open source! Powering DeepSeek-V4.1-Flash at scale with high throughput..." — &lt;a href="https://x.com/deepseek_ai/status/2112476501065286085" rel="noopener noreferrer"&gt;@deepseek_ai on X&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There has been a wall of hot takes since (the &lt;a href="https://news.ycombinator.com/item?id=50163277" rel="noopener noreferrer"&gt;Hacker News thread&lt;/a&gt; passed 1,400 points). Most of them skip the README, which is actually quite specific about what this release is and — more interestingly — what it is &lt;em&gt;not&lt;/em&gt;. This post is a walkthrough of exactly that: what's in the box, what's deliberately left out, where it comes from, and who should actually deploy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DeepInfer is
&lt;/h2&gt;

&lt;p&gt;From the README, verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"DeepInfer is the high-performance inference framework that powers DeepSeek's large-scale production services. This open-source release provides offline inference with the core features needed for serving at scale."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two words in that sentence matter more than the rest:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"production"&lt;/strong&gt; — this is not a research playground. It's the same stack that serves DeepSeek's own API traffic, open-sourced as-is rather than rebuilt for external consumption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"offline inference"&lt;/strong&gt; — the release ships offline (batch-style) inference. A full online serving service needs components DeepInfer does &lt;em&gt;not&lt;/em&gt; include (more on that below).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It supports exactly one model family today: &lt;strong&gt;DeepSeek-V4.1-Flash&lt;/strong&gt; (default checkpoint: &lt;code&gt;deepseek-ai/DeepSeek-V4.1-Flash-1017&lt;/code&gt; on Hugging Face). This is a model-hardware co-optimized production engine, not a universal deployment framework for every model on HF.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the box
&lt;/h2&gt;

&lt;p&gt;The feature list from the README, with the configuration knobs that go with them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;How it's enabled&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Optimized kernels (FlashMLA, DeepGEMM, DeepEP, TileKernels, DeepSelect, custom TileLang kernels)&lt;/td&gt;
&lt;td&gt;built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Draft-token decoding + confidence-scheduled verification&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DEEPINFER_ENABLE_MTP_VARLEN=1&lt;/code&gt;; max draft length via &lt;code&gt;DEEPINFER_USE_NEXTN_DECODE&lt;/code&gt; (default 5)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallelism: attention DP, balanced CP, MoE EP + EPLB&lt;/td&gt;
&lt;td&gt;built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decoder SWA bounded replay (only replay a bounded suffix of the context)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DEEPINFER_YOCO_PREFILL_TRUNCATION_MODE=compact&lt;/code&gt;, &lt;code&gt;DEEPINFER_YOCO_PREFILL_KEEP_TOKENS=140&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asynchronous scheduling (overlap CPU batch scheduling with device execution)&lt;/td&gt;
&lt;td&gt;enabled by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch-invariant inference (identical results across batch compositions)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DEEPINFER_BATCH_INVARIANT=1&lt;/code&gt; (NVIDIA only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deterministic sampling (identical outputs for identical inputs and seeds)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DEEPINFER_BATCH_INVARIANT=1&lt;/code&gt; + fixed &lt;code&gt;seq_seed&lt;/code&gt; (NVIDIA only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured outputs (regex / JSON schema / EBNF grammar constraints)&lt;/td&gt;
&lt;td&gt;via &lt;code&gt;SamplingParams(so_args={...})&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of those deserve a note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batch invariance + deterministic sampling&lt;/strong&gt; kill a whole class of production headache. If you've ever been burned by "the same prompt gives a different answer depending on who else is in the batch," this is the guarantee that removes that bug class — the kind of property usually only achieved with bespoke deployment tricks. Note, though, that it's NVIDIA-only in this release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence-scheduled verification&lt;/strong&gt; is speculative decoding where the scheduler uses confidence estimates to decide how to spend verification budget — a scheduling refinement rather than a new algorithm, but the kind of thing that only gets built if you actually serve enormous volume.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it's built on — and the hardware bet
&lt;/h2&gt;

&lt;p&gt;The acknowledgments section settles the "is it a vLLM fork?" question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"DeepInfer was originally built primarily on &lt;a href="https://github.com/vllm-project/vllm" rel="noopener noreferrer"&gt;vLLM&lt;/a&gt;, with additional inspiration from &lt;a href="https://github.com/sgl-project/sglang" rel="noopener noreferrer"&gt;SGLang&lt;/a&gt; and &lt;a href="https://github.com/NVIDIA/TensorRT-LLM" rel="noopener noreferrer"&gt;TensorRT-LLM&lt;/a&gt;."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So yes — vLLM lineage. What DeepSeek added on top is a stack of self-optimized kernels (FlashMLA, DeepGEMM, DeepEP are DeepSeek's own projects) and a scheduler designed around their model's architecture.&lt;/p&gt;

&lt;p&gt;The hardware support is the political headline, though:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NVIDIA SM100 GPUs&lt;/strong&gt; (CUDA 13)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Huawei Ascend 950 NPUs&lt;/strong&gt; (CANN 9.2)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That second line is unusual for a top-tier open-source inference release: Ascend support sitting next to NVIDIA from day one, in a production engine. Requirements are strict on both sides — Linux, GCC ≥ 13.0, Python 3.12, and either CUDA 13 or CANN 9.2. This is not a laptop demo framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks: transparency instead of a hero number
&lt;/h2&gt;

&lt;p&gt;The README doesn't lead with a single "we're X% faster" number. Instead it publishes two Pareto plots — prefill throughput vs. latency, and decode throughput vs. per-user speed — measured on &lt;strong&gt;4 SM100 GPUs for prefill and 64 for decode&lt;/strong&gt;, each within one scale-up domain, with a &lt;code&gt;run_sweep.sh&lt;/code&gt; script so you can reproduce the configuration yourself.&lt;/p&gt;

&lt;p&gt;That's the right shape for claims like these: a Pareto curve with a reproduction script is auditable in a way a bar chart never is. Whether the curves hold up on &lt;em&gt;your&lt;/em&gt; interconnect is, of course, a different question — the sweep script exists precisely because the answer depends on your topology.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's deliberately missing
&lt;/h2&gt;

&lt;p&gt;This is the part of the README most coverage skipped. DeepInfer is open about requiring three subsystems for a complete inference service that are &lt;strong&gt;not&lt;/strong&gt; in this release:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A KV cache system&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VE/prefill/decode disaggregation&lt;/strong&gt; (though the README notes most required engine functionality is already implemented)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load balancing across data-parallel replicas&lt;/strong&gt; for both prefill and decode&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In other words: this is an &lt;em&gt;engine&lt;/em&gt;, not a turnkey server. If you were hoping to swap it in as the backend behind your OpenAI-compatible gateway tomorrow, you'll be building or bolting on those pieces. That's also a fair reading of DeepSeek's release philosophy: publish the hard, differentiating core; let the surrounding infrastructure stay open-ended.&lt;/p&gt;

&lt;p&gt;One caveat worth repeating: batch invariance and deterministic sampling are &lt;strong&gt;NVIDIA-only&lt;/strong&gt; in this release, and Ascend support, while present, carries exactly the same "read the fine print" weight as any first-version port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quickstart
&lt;/h2&gt;

&lt;p&gt;Build from source (create an empty Python 3.12 environment first):&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>AI Writes the PLC Code. Proving It Works Is the Hard Part — 7 Free Hands-On Lessons</title>
      <dc:creator>tia-forge</dc:creator>
      <pubDate>Tue, 20 Oct 2026 02:29:56 +0000</pubDate>
      <link>https://dev.to/tiaforge/ai-writes-the-plc-code-proving-it-works-is-the-hard-part-7-free-hands-on-lessons-25j6</link>
      <guid>https://dev.to/tiaforge/ai-writes-the-plc-code-proving-it-works-is-the-hard-part-7-free-hands-on-lessons-25j6</guid>
      <description>&lt;p&gt;&lt;strong&gt;The missing piece in AI-assisted PLC programming isn't code generation — it's proof.&lt;/strong&gt; &lt;a href="https://tia.hubwiz.com/learn-lad/ai/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=ai-plc-programming-7-lessons-verification" rel="noopener noreferrer"&gt;PLC AI 实战&lt;/a&gt; is a free, seven-lesson, text-and-screenshots course that breaks that problem into seven checkable endpoints: a compile with 0 errors and 0 warnings, a read-back that matches the spec, a scenario report that says PASS/FAIL, and a regression run that shows behavior didn't change. No videos, no course upsell, and nothing to download.&lt;/p&gt;

&lt;p&gt;I went through all seven lessons and pulled out the parts that matter if you already write PLC code for a living — plus the three failures the course deliberately shows instead of hiding.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The bottleneck moved from "write it" to "prove it"
&lt;/h2&gt;

&lt;p&gt;Almost every engineer's first reaction to AI-generated SCL is the same: &lt;em&gt;looks about right, but I don't dare use it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"Looks about right" comes from the model's strengths — interfaces, variable names, IF/THEN structure. "I don't dare" comes from the plant floor: a wrong PLC program doesn't cost you a compile error, it costs you a machine doing the wrong thing.&lt;/p&gt;

&lt;p&gt;The course is valuable because it shows three real failures rather than a demo where the AI gets it right the first time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Compiles clean, spec not implemented.&lt;/strong&gt; The requirement said "after the sensor triggers, output for 2 seconds." The implementation used &lt;code&gt;TON&lt;/code&gt;. The sensor is only high for one scan cycle, so the timer's &lt;code&gt;ET&lt;/code&gt; never reaches 2000 ms and the output never turns on once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compiles clean, nothing moves.&lt;/strong&gt; While refactoring a three-motor delayed-start sequence, &lt;code&gt;TP&lt;/code&gt; was written as &lt;code&gt;TON&lt;/code&gt;. Compile: 0 errors, 0 warnings. Press the button: none of the three motors start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compiles clean, the field never reads it.&lt;/strong&gt; When changing an IO address, the TIA compiler &lt;strong&gt;does not check whether a physical channel exists&lt;/strong&gt;. Point a tag at &lt;code&gt;I30.0&lt;/code&gt;, which this project doesn't have, and you still get 0 errors and 0 warnings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three have the same shape: &lt;strong&gt;the compiler cannot see them.&lt;/strong&gt; So the only defensible criterion is: every step needs evidence someone else can re-check — not the agent saying "done."&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Four layers of verification, and what each one does &lt;em&gt;not&lt;/em&gt; prove
&lt;/h2&gt;

&lt;p&gt;This is the table worth remembering. Each lesson tells you which layer you're on:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Who judges&lt;/th&gt;
&lt;th&gt;Proves&lt;/th&gt;
&lt;th&gt;Does not prove&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Structural validation (&lt;code&gt;scl_validate&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The cloud simulator's parser&lt;/td&gt;
&lt;td&gt;Source parses, structure is legal&lt;/td&gt;
&lt;td&gt;TIA compile rules, behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIA compile&lt;/td&gt;
&lt;td&gt;The TIA Portal compiler&lt;/td&gt;
&lt;td&gt;The block compiles in &lt;em&gt;your&lt;/em&gt; project (0 errors, 0 warnings)&lt;/td&gt;
&lt;td&gt;Behavior matches the spec; the address has a physical channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud simulation&lt;/td&gt;
&lt;td&gt;A scan-level simulator&lt;/td&gt;
&lt;td&gt;Under a given input sequence, output assertions hold (PASS/FAIL)&lt;/td&gt;
&lt;td&gt;Hardware, bus, HMI, safety circuits; also not the TIA compiler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual PLC / real hardware&lt;/td&gt;
&lt;td&gt;S7-PLCSIM Advanced / the machine&lt;/td&gt;
&lt;td&gt;Real scan behavior after download, diagnostics, faults&lt;/td&gt;
&lt;td&gt;Safety functions, interlocks, e-stops still need human review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A practical detail: &lt;code&gt;scl_validate&lt;/code&gt; returning &lt;code&gt;ok: true&lt;/code&gt; only means the source &lt;strong&gt;parses&lt;/strong&gt;. The timer call from lesson 1 that omits &lt;code&gt;PT&lt;/code&gt; will likely pass structural validation, while TIA reports &lt;code&gt;Parameter 'PT' has to be used.&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. How the seven lessons are ordered
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Lesson&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Endpoint (what you walk away with)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Beginner&lt;/td&gt;
&lt;td&gt;Write SCL: from requirement to 0 errors, 0 warnings&lt;/td&gt;
&lt;td&gt;TiaLink&lt;/td&gt;
&lt;td&gt;30 min&lt;/td&gt;
&lt;td&gt;A clean compile plus a requirement → implementation → verdict table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beginner&lt;/td&gt;
&lt;td&gt;Modify the first example in the library&lt;/td&gt;
&lt;td&gt;TiaLink&lt;/td&gt;
&lt;td&gt;20 min&lt;/td&gt;
&lt;td&gt;One added tag, one series contact, clean compile, structural read-back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intermediate&lt;/td&gt;
&lt;td&gt;Read an undocumented legacy TIA project&lt;/td&gt;
&lt;td&gt;TiaLink (read-only)&lt;/td&gt;
&lt;td&gt;25 min&lt;/td&gt;
&lt;td&gt;A project specification document + a human review list; zero changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intermediate&lt;/td&gt;
&lt;td&gt;Change one IO address — what breaks?&lt;/td&gt;
&lt;td&gt;TiaLink (read-only + dryRun)&lt;/td&gt;
&lt;td&gt;25 min&lt;/td&gt;
&lt;td&gt;Reference list, risk list, change plan, preflight result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intermediate&lt;/td&gt;
&lt;td&gt;Simulate before commissioning: download to a virtual PLC&lt;/td&gt;
&lt;td&gt;TiaLink + PLCSIM Advanced&lt;/td&gt;
&lt;td&gt;35 min&lt;/td&gt;
&lt;td&gt;A scenario table with &lt;strong&gt;measured&lt;/strong&gt; values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intermediate&lt;/td&gt;
&lt;td&gt;No TIA install: turn SCL into PASS/FAIL in the cloud&lt;/td&gt;
&lt;td&gt;Cloud simulation MCP&lt;/td&gt;
&lt;td&gt;35 min&lt;/td&gt;
&lt;td&gt;An ALL PASS report plus the scan count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hands-on&lt;/td&gt;
&lt;td&gt;Refactor without changing behavior: 3 copies → 1 FB&lt;/td&gt;
&lt;td&gt;TiaLink + PLCSIM&lt;/td&gt;
&lt;td&gt;40 min&lt;/td&gt;
&lt;td&gt;Baseline vs regression, point by point&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;About &lt;strong&gt;3.5 hours&lt;/strong&gt; total. The order isn't by topic — it's the order work actually happens: write → modify → read → assess impact → simulate → verify → regress. Every lesson ends with a verification checklist; if you can't tick it, you're not done.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The seven things that actually trip people up
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 First compile: 2 errors, 2 warnings
&lt;/h3&gt;

&lt;p&gt;The only input is one requirement: an &lt;code&gt;FB_Sorter&lt;/code&gt; with &lt;code&gt;Start&lt;/code&gt; / &lt;code&gt;SensorA&lt;/code&gt; / &lt;code&gt;SensorB&lt;/code&gt; / &lt;code&gt;Reset&lt;/code&gt; in, &lt;code&gt;DiverterA&lt;/code&gt; / &lt;code&gt;DiverterB&lt;/code&gt; / &lt;code&gt;Running&lt;/code&gt; out, interlocked, 2-second outputs, reset clears everything. Everything after that — read the project, add a CPU, write the block, import, compile, fix, read back, save — is the agent's job.&lt;/p&gt;

&lt;p&gt;Measured on &lt;strong&gt;TiaLink 1.1.0.0 + TIA Portal V19 + S7-1200 (CPU 1214C, V4.6)&lt;/strong&gt;. The first version reads like it satisfies the spec, and compiles to &lt;strong&gt;2 errors and 2 warnings&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[Error] Parameter 'PT' has to be used.&lt;/code&gt; — a timer call must supply &lt;code&gt;PT&lt;/code&gt; &lt;strong&gt;every time&lt;/strong&gt;, including the reset branch. The correct shape is one call per scan with reset folded into &lt;code&gt;IN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[Warning] The parameter '#Running' might not be initialized.&lt;/code&gt; — the output is read before it is definitely assigned. Carry the state in a Static variable with an initial value and assign the output unconditionally at the end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two engineering disciplines hide here as well: every import leaves behind an &lt;strong&gt;external source file&lt;/strong&gt; in the project (they pile up over iterations), and &lt;strong&gt;import ≠ save&lt;/strong&gt; — a successful import is not a saved project.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Modifying an example: don't import it into a live project
&lt;/h3&gt;

&lt;p&gt;The second lesson practices the more common job: &lt;strong&gt;take existing logic and change it.&lt;/strong&gt; The simplest library example (a single-button toggle, 4 networks in LAD) gets one new input: &lt;code&gt;Enable&lt;/code&gt; at &lt;code&gt;%I0.1&lt;/code&gt;, forcing the output off when 0 and preserving the toggle when 1.&lt;/p&gt;

&lt;p&gt;Two edits total: one new tag, and one series contact after the power rail in Network 2. After a clean compile, structural read-back confirms &lt;code&gt;Motor = Enable AND (Tag_1 OR Motor) AND NOT Tag_2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two genuinely annoying traps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UId&lt;/code&gt; must be a decimal integer&lt;/strong&gt; — hex is rejected outright;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TIA renumbers &lt;code&gt;UId&lt;/code&gt; after import&lt;/strong&gt;, so you cannot assert on &lt;code&gt;UId&lt;/code&gt;; assert on structure instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And one precondition: examples ship with &lt;code&gt;Main (OB1)&lt;/code&gt; and &lt;code&gt;Default tag table&lt;/code&gt;, so importing &lt;strong&gt;overwrites same-named objects&lt;/strong&gt;. Get a clean baseline compile first, so that when something breaks later you know whose fault it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Reading a legacy project: separate confirmed / inferred / unconfirmed
&lt;/h3&gt;

&lt;p&gt;The third lesson is read-only against an MIT-licensed sample project: 5 blocks (&lt;code&gt;OB1&lt;/code&gt; + &lt;code&gt;FB2/FB3&lt;/code&gt; + &lt;code&gt;DB12/DB13&lt;/code&gt;), 51 tags, 3 HMI screens with 40 variables. The output is a project specification, with zero changes to the project.&lt;/p&gt;

&lt;p&gt;Three classic legacy shapes show up immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The CPU is an F-type&lt;/strong&gt; (&lt;code&gt;1513F-1 PN&lt;/code&gt;, &lt;code&gt;6ES7 513-1FL02-0AB0&lt;/code&gt;, firmware V2.9) but there are no F-blocks. That doesn't mean the hardware is wrong, but it has to be listed as a safety item for a human to confirm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An FB with an empty interface is not an empty block&lt;/strong&gt;: &lt;code&gt;Auto_mode&lt;/code&gt; / &lt;code&gt;Manu_mode&lt;/code&gt; have no parameters at all; the logic reads and writes global variables (&lt;code&gt;ZS-*&lt;/code&gt;, &lt;code&gt;CV-*&lt;/code&gt;, &lt;code&gt;HMI_*&lt;/code&gt;, &lt;code&gt;SIM_*&lt;/code&gt;). Which means: track change impact through &lt;strong&gt;variables&lt;/strong&gt;, not through interfaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One giant network&lt;/strong&gt;: &lt;code&gt;Auto_mode&lt;/code&gt; has a single network with 36 operands; 2 of &lt;code&gt;Main&lt;/code&gt;'s 4 networks are empty. Treating that as "one rung" is guaranteed to be wrong — split it into functional segments (sensor chain, actuator chain, alarm/simulation, MATLAB handshake).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more reconciliation: &lt;strong&gt;6 of the 51 tags are not referenced by any PLC block&lt;/strong&gt;, and &lt;code&gt;HMI_PID&lt;/code&gt; deserves special mention — the HMI has a PID screen, but there is no PID block and the tag is unused. "PID may not be implemented" is a human-confirm item. A document that doesn't distinguish confirmed / inferred / unconfirmed is fiction.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 Change-impact analysis: a clean compile does not mean the field can read it
&lt;/h3&gt;

&lt;p&gt;The fourth lesson answers "can I touch it?" One candidate change: &lt;code&gt;ZS-101&lt;/code&gt; (Infeed Sensor) from &lt;code&gt;%I29.0&lt;/code&gt; to &lt;code&gt;%I29.4&lt;/code&gt;. Read-only throughout, with a &lt;code&gt;dryRun&lt;/code&gt; preflight at the end.&lt;/p&gt;

&lt;p&gt;The result is cleaner than expected — and more dangerous:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;References&lt;/strong&gt;: 6 in &lt;code&gt;Auto_mode&lt;/code&gt; + 2 in &lt;code&gt;Manu_mode&lt;/code&gt;, &lt;strong&gt;all reads&lt;/strong&gt;, and &lt;strong&gt;zero absolute addresses&lt;/strong&gt; inside blocks. So no block needs editing; only the tag table changes. That's what "impact assessed" means.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The HMI is symbolic&lt;/strong&gt; (&lt;code&gt;AddressAccessMode=Symbolic&lt;/code&gt;), so the address change follows automatically — but address strings hard-coded in screens or comments are &lt;strong&gt;not visible to the compiler&lt;/strong&gt; and need a human.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware mapping&lt;/strong&gt;: &lt;code&gt;%I29.0&lt;/code&gt; belongs to the ET200SP &lt;code&gt;DI 8x24VDC ST_2&lt;/code&gt; (Input29); &lt;code&gt;I29.0–3&lt;/code&gt; are &lt;code&gt;ZS-101..104&lt;/code&gt;, so &lt;code&gt;%I29.4&lt;/code&gt; is a free channel on the same module.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dangerous part&lt;/strong&gt;: TIA &lt;strong&gt;does not validate that the address has a physical channel&lt;/strong&gt;. Point it at a non-existent address and you still get a clean compile, while the field never sees the signal. Changing an address also means changing the &lt;em&gt;wiring ↔ address&lt;/em&gt; mapping — moving a terminal is something only a person or a drawing can confirm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dryRun&lt;/code&gt; returns&lt;/strong&gt; &lt;code&gt;{action: "overwrite", kind: "tagTable", tagCount: 27}&lt;/code&gt; — you thought you changed one line; the whole 27-tag table gets rewritten. Without this preflight, you find out after other tags silently revert.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4.5 Virtual PLC: behavior finally becomes visible
&lt;/h3&gt;

&lt;p&gt;The fifth lesson runs the program: the library example "exhaust fan control" (one network, &lt;code&gt;TOF&lt;/code&gt; of 1 minute) is downloaded into an &lt;strong&gt;S7-PLCSIM Advanced 6.0&lt;/strong&gt; instance, driven by a scenario table, with &lt;strong&gt;measured&lt;/strong&gt; values recorded.&lt;/p&gt;

&lt;p&gt;The first whole-project compile is &lt;strong&gt;0 errors, 1 warning&lt;/strong&gt;: &lt;code&gt;Inputs or outputs are used that do not exist in the configured hardware&lt;/code&gt; — the 1516 has no onboard I/O, so &lt;code&gt;%I0.0&lt;/code&gt; / &lt;code&gt;%Q0.0&lt;/code&gt; don't exist. Insert a DI and a DQ module (starting at 0) and it's 0/0.&lt;/p&gt;

&lt;p&gt;The order is fixed: &lt;strong&gt;register instance → power on → wait for Stop → download → RUN&lt;/strong&gt;. A freshly registered instance is empty, which is why you wait for Stop before downloading. &lt;code&gt;/sim/status&lt;/code&gt; returning 403 means exactly two things: TiaLink was started with &lt;code&gt;--no-sim&lt;/code&gt;, or PLCSIM Advanced isn't installed.&lt;/p&gt;

&lt;p&gt;Four scenarios surface something the compiler can't see and the diagram reads wrong:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Press the stop button and the fan doesn't stop immediately.&lt;/strong&gt; The &lt;code&gt;TOF&lt;/code&gt; input is &lt;code&gt;Turn_On_PB AND NOT Turn_Off_PB&lt;/code&gt; — for a &lt;code&gt;TOF&lt;/code&gt;, releasing the start button and pressing stop are &lt;strong&gt;the same falling edge&lt;/strong&gt;, so the fan keeps running for another minute either way. Reading the ladder, "stop means stop" is the natural (and wrong) conclusion.&lt;/p&gt;

&lt;p&gt;Which changes the deliverable: not "I looked at it, it's fine," but &lt;strong&gt;scenario table + measured values + one conclusion&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.6 Cloud simulation: define the word "trigger"
&lt;/h3&gt;

&lt;p&gt;The sixth lesson takes a lighter path: &lt;strong&gt;no TIA, no PLCSIM, no Windows.&lt;/strong&gt; The same SCL goes to a cloud simulation MCP endpoint, executed scan by scan, turned into PASS/FAIL by a scenario table.&lt;/p&gt;

&lt;p&gt;Setup is three steps: sign in, create a &lt;code&gt;sim_live_&lt;/code&gt; API key, add &lt;code&gt;https://tia.hubwiz.com/sim/mcp&lt;/code&gt; to any Streamable HTTP MCP client. The endpoint exposes six tools (&lt;code&gt;lad_*&lt;/code&gt; / &lt;code&gt;scl_*&lt;/code&gt;); this lesson uses &lt;code&gt;scl_validate&lt;/code&gt;, &lt;code&gt;scl_simulate&lt;/code&gt;, &lt;code&gt;scl_run_scenario&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Billing and fields worth knowing up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;resources/read&lt;/code&gt; are &lt;strong&gt;free&lt;/strong&gt;; each accepted &lt;code&gt;tools/call&lt;/code&gt; costs one credit. New accounts get 100; this lesson uses roughly 8–12.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tickMs&lt;/code&gt; defaults to 10 ms, so &lt;strong&gt;100 ticks = 1 second&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scl_simulate&lt;/code&gt; steps in &lt;code&gt;scans&lt;/code&gt;; &lt;code&gt;scl_run_scenario&lt;/code&gt; steps in &lt;code&gt;ticks&lt;/code&gt;. That's the easiest mistake to make.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first acceptance run produces the course's signature failure (indented block below — the source page prints it with full scan counts):&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASS  start latches, reset clears (3 step(s), 5 scan(s))
FAIL  sensor A pulse gives a 2 s diverter pulse: step 3 scan 100: DiverterA expected true, got false
FAIL  both sensors: A wins, only one output: step 2 scan 1: DiverterA expected true, got false
RESULT: FAIL (2 failure(s), 109 scan(s))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The same root cause as lesson 1: a &lt;code&gt;TON&lt;/code&gt; requires &lt;code&gt;IN&lt;/code&gt; to stay true for the full &lt;code&gt;PT&lt;/code&gt;, and the sensor is high for exactly one scan. "Trigger, then output for 2 seconds" was implemented as "hold it for 2 seconds, then output" — &lt;strong&gt;two different requirements&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Switching to &lt;code&gt;TP&lt;/code&gt; (fixed pulse width) passes the first three scenarios, and then a new boundary case fails: a &lt;code&gt;TP&lt;/code&gt; pulse is &lt;strong&gt;not&lt;/strong&gt; cut short by &lt;code&gt;IN := FALSE&lt;/code&gt;. &lt;code&gt;DiverterA := PulseA.Q AND NOT Reset&lt;/code&gt; merely &lt;strong&gt;masks&lt;/strong&gt; the output while the pulse keeps running, so the diverter comes back on when reset is released. The final design is &lt;strong&gt;latch + &lt;code&gt;TON&lt;/code&gt; to end the pulse&lt;/strong&gt;: make "currently diverting" an explicit &lt;code&gt;ActiveA&lt;/code&gt;/&lt;code&gt;ActiveB&lt;/code&gt; state, let &lt;code&gt;Reset&lt;/code&gt; clear the state, and interlock on the other side's &lt;code&gt;Active&lt;/code&gt; rather than its output.&lt;/p&gt;

&lt;p&gt;The ALL PASS run: &lt;strong&gt;8 scenarios + 16 sweep combinations + a 5,000-scan fuzz = 5,735 scan cycles&lt;/strong&gt;. The report replays identically on local &lt;code&gt;scl-sim 0.1.0&lt;/code&gt; and the hosted endpoint (2026-10-19): same SCL plus the same scenario file yields the same PASS/FAIL and scan counts.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.7 Refactor with regression: &lt;code&gt;TP&lt;/code&gt; typed as &lt;code&gt;TON&lt;/code&gt;, clean compile, zero motors
&lt;/h3&gt;

&lt;p&gt;The last lesson covers "change the structure, not the behavior." The target is a three-motor delayed-start sequence: one button opens a 20-second window, three motors start at +5 / +10 / +15 s, and everything stops after the window. The original writes &lt;code&gt;TON&lt;/code&gt; + coil three times in one network, differing only in &lt;code&gt;PT&lt;/code&gt; and output — classic copy-paste.&lt;/p&gt;

&lt;p&gt;The refactor collapses it into &lt;strong&gt;one parameterized FB&lt;/strong&gt; (&lt;code&gt;Enable&lt;/code&gt; / &lt;code&gt;Pt&lt;/code&gt; in, &lt;code&gt;Q&lt;/code&gt; out) instantiated three times, with tag table, symbols and addresses untouched.&lt;/p&gt;

&lt;p&gt;The author also crashed once, which is the point: &lt;code&gt;IEC_Timer_0_DB&lt;/code&gt; was called as &lt;code&gt;TON&lt;/code&gt; instead of &lt;code&gt;TP&lt;/code&gt;. Compile: 0 errors, 0 warnings. Press the button: no motor moves. Switching back to &lt;code&gt;TP&lt;/code&gt; and replaying the same scenario table brings behavior back point for point.&lt;/p&gt;

&lt;p&gt;Which is why the two disciplines matter more than the code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Store a behavior baseline first.&lt;/strong&gt; Same scenario table, run before and after, compare cell by cell. Refactoring without a baseline means using the plant as a test environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust only output bits and the scenario table.&lt;/strong&gt; If a structured member (&lt;code&gt;DB.Ton.ET&lt;/code&gt;) can't be read, change the evidence — don't fill the gap with "looks right."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Who does what: the agent acts, you judge
&lt;/h2&gt;

&lt;p&gt;The course deliberately minimizes the human's actions, because in industrial work the interesting question isn't "can the AI write code" but "which actions have to stay with a person."&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;The agent does&lt;/th&gt;
&lt;th&gt;You must do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Write SCL&lt;/td&gt;
&lt;td&gt;Read the project, add a CPU, generate the block, import, compile, self-fix from compiler output, clean up source files, save&lt;/td&gt;
&lt;td&gt;State the requirement; approve the "report before change" step; spot-check the read-back table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modify an example&lt;/td&gt;
&lt;td&gt;Update the tag table, insert a contact, compile, read back structure&lt;/td&gt;
&lt;td&gt;Confirm the boundary (only &lt;code&gt;Main&lt;/code&gt; + tag table); confirm it's not a live project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read a legacy project&lt;/td&gt;
&lt;td&gt;Inventory devices/blocks/tags/HMI, export, summarize per block and network, flag unreferenced tags&lt;/td&gt;
&lt;td&gt;Decide which findings escalate to safety items; review inferred vs unconfirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change impact&lt;/td&gt;
&lt;td&gt;Scan references, classify read/write and symbolic/absolute, map to hardware channels, &lt;code&gt;dryRun&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Decide whether to change it; confirm terminals and drawings; confirm saving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual PLC&lt;/td&gt;
&lt;td&gt;Add device, insert modules, import, compile, register, power on, download, read/write, fill the table&lt;/td&gt;
&lt;td&gt;Click the TIA authorization prompt; check that "measured" values really came from reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud simulation&lt;/td&gt;
&lt;td&gt;Write scenario JSON, run assertions, locate the failing scan&lt;/td&gt;
&lt;td&gt;Verify the scenario table covers the requirement — especially the definition of "trigger"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refactor + regression&lt;/td&gt;
&lt;td&gt;Save the baseline, write the parameterized FB, replace, rerun, diff&lt;/td&gt;
&lt;td&gt;Decide which blocks must not be "refactored while you're in there"; confirm replacing the original&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Short version: &lt;strong&gt;actions can be delegated, judgment cannot.&lt;/strong&gt; Your part boils down to three kinds of moves: state the requirement and the boundary, approve before a change is written and before it is saved, and verify the evidence instead of the claim. The checklists exist to turn "a human took a look" into "a human verified something."&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Four questions people ask
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need TIA Portal installed?&lt;/strong&gt;&lt;br&gt;
Not for the whole course. One of the seven lessons (cloud PASS/FAIL) never touches TIA — browser, account, and any Streamable HTTP MCP client. The other six need Windows plus TIA Portal V19.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can TiaLink change my project on its own?&lt;/strong&gt;&lt;br&gt;
It doesn't save silently. It exposes Openness operations as MCP tools, so writes are explicit: preflight before import, no overwrite of same-named objects by default, no auto-save afterwards. "Import ≠ save" appears more than once in the seven lessons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If cloud simulation passes, is the program fine?&lt;/strong&gt;&lt;br&gt;
No. Cloud simulation runs SCL scan semantics only — no bus, HMI, drives, safety circuits, and it is not the TIA compiler. What it is very good at is exactly the "compiles clean, behavior broken" class of bug, like &lt;code&gt;TON&lt;/code&gt; where the requirement said "trigger."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can AI-written blocks go straight into production?&lt;/strong&gt;&lt;br&gt;
I wouldn't. The course's stance is consistent: AI assists, the compiler and read-back decide, every change gets human review, and safety functions, interlocks and e-stops are not delegated. What ships isn't "AI-written code" — it's code that passed the acceptance chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Two ways to start
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Path A — Windows + TIA Portal V19&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a sandbox project, start TiaLink on the same machine; the page header auto-detects the connection and project name.&lt;/li&gt;
&lt;li&gt;Add a Streamable HTTP MCP server at &lt;code&gt;http://localhost:8080/mcp&lt;/code&gt; with a &lt;code&gt;Bearer&lt;/code&gt; token; on another machine, replace &lt;code&gt;localhost&lt;/code&gt; with the TIA PC's LAN address.&lt;/li&gt;
&lt;li&gt;Start with "modify the first example" (20 minutes, smallest diff, fastest verification), then go back to "write SCL to 0/0" (30 minutes).&lt;/li&gt;
&lt;li&gt;If the health check flags the allow-list, the first write triggers a TIA authorization dialog that only a human can confirm; running &lt;code&gt;TiaLink.Helper.exe&lt;/code&gt; once avoids it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Path B — no TIA available yet&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the cloud simulator, sign in with a QR code, create an API key (shown once — don't commit it).&lt;/li&gt;
&lt;li&gt;Set the MCP endpoint to &lt;code&gt;https://tia.hubwiz.com/sim/mcp&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Do the "no TIA install" lesson with the same &lt;code&gt;FB_Sorter&lt;/code&gt; that compiled clean in path A — and watch it fail twice before it passes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  8. What the course deliberately does not provide
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No downloads.&lt;/strong&gt; By design: no files land on disk; import goes through TiaLink with a preflight, and nothing is auto-saved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud simulation only accepts examples or desensitized code.&lt;/strong&gt; Don't paste a customer project; lessons that touch TIA run entirely on your machine, and project data stays in-house.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passing simulation is not commissioning.&lt;/strong&gt; PLCSIM only simulates CPU behavior; bus, HMI, drives and safety circuits are out of scope. Cloud simulation is narrower still.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every change needs human review.&lt;/strong&gt; Safety functions, interlocks, e-stops are not delegated; AI output is "unverified" by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version-specific.&lt;/strong&gt; Everything was measured on TiaLink 1.1.0.0 + TIA Portal V19 + S7-PLCSIM Advanced 6.0. Different versions change error text and tool names — which is exactly why the course insists on structured results instead of matching error strings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Who it's for, and who it isn't
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For&lt;/strong&gt;: engineers who know TIA Portal but don't trust AI-written blocks in a real project; people inheriting a legacy program who want a specification before touching it; anyone training juniors who needs a checkable practice path; technical leads deciding how far AI can go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not for&lt;/strong&gt;: people who have never opened TIA Portal (start with the &lt;a href="https://tia.hubwiz.com/learn-lad/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=ai-plc-programming-7-lessons-verification" rel="noopener noreferrer"&gt;60-example ladder library&lt;/a&gt;); anyone looking for "one-click ladder generation, download the project"; anyone who wants to skip human review and safety validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Takeaway
&lt;/h2&gt;

&lt;p&gt;Read together, the seven lessons aren't about how much the AI can write. They describe &lt;strong&gt;an auditable delivery chain&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;write → compile → read back → assess impact → simulate → PASS scenarios → regression shows behavior unchanged.&lt;/p&gt;

&lt;p&gt;Every step has a criterion that doesn't depend on the AI claiming success. That's the part worth copying, even if you never use TiaLink or the cloud simulator: put evidence at every step, and AI actually gets to enter an engineering workflow.&lt;/p&gt;

&lt;p&gt;Start with the 20-minute &lt;a href="https://tia.hubwiz.com/learn-lad/ai/modify-example/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=ai-plc-programming-7-lessons-verification" rel="noopener noreferrer"&gt;modify-an-example lesson&lt;/a&gt;, or the 35-minute &lt;a href="https://tia.hubwiz.com/learn-lad/ai/cloud-sim-scenarios/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=ai-plc-programming-7-lessons-verification" rel="noopener noreferrer"&gt;cloud PASS/FAIL lesson&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verification notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Course structure, lesson steps and all measured numbers (2 errors / 2 warnings, 36 operands, 51 tags with 6 unreferenced, &lt;code&gt;dryRun&lt;/code&gt; rewriting 27 tags, 5,735 scan cycles, &lt;code&gt;1513F-1 PN&lt;/code&gt;, &lt;code&gt;1516-3 PN/DP&lt;/code&gt;) come from the official course pages at &lt;a href="https://tia.hubwiz.com/learn-lad/ai/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=ai-plc-programming-7-lessons-verification" rel="noopener noreferrer"&gt;tia.hubwiz.com/learn-lad/ai/&lt;/a&gt; and its seven sub-pages, checked on 2026-10-20.&lt;/li&gt;
&lt;li&gt;The stated test environment is TiaLink 1.1.0.0 + TIA Portal V19 + S7-PLCSIM Advanced 6.0; the cloud simulation report replays identically on &lt;code&gt;scl-sim 0.1.0&lt;/code&gt; and the hosted endpoint (2026-10-19).&lt;/li&gt;
&lt;li&gt;This article does not independently reproduce the measurements; numbers and conclusions follow the official pages and may change with tool versions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further reading (all in Chinese)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/ai-industrial-stack/" rel="noopener noreferrer"&gt;AI × Industrial / PLC guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/siemens-plc-ladder-example-library/" rel="noopener noreferrer"&gt;Siemens PLC ladder logic example library: 60 importable exercises&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/taking-over-legacy-plc-program-4-steps/" rel="noopener noreferrer"&gt;Taking over someone else's PLC program: 4 evidence steps before the first edit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/lad-to-scl-migration-guide-tia-portal/" rel="noopener noreferrer"&gt;Ladder to SCL migration guide: what converts, what must be refactored, how to verify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/virtual-plc-lab-factory-io-openplc-modbus-tcp/" rel="noopener noreferrer"&gt;Practicing PLC without hardware: Factory I/O + OpenPLC virtual lab&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article was originally published in Chinese on &lt;a href="https://www.hubwiz.com/blog/ai-plc-programming-7-lessons-verification/" rel="noopener noreferrer"&gt;hubwiz.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>plc</category>
      <category>automation</category>
      <category>programming</category>
    </item>
    <item>
      <title>Siemens PLC Ladder Logic Examples: 60 Exercises You Can Import Into TIA Portal</title>
      <dc:creator>tia-forge</dc:creator>
      <pubDate>Sat, 17 Oct 2026 12:47:09 +0000</pubDate>
      <link>https://dev.to/tiaforge/siemens-plc-ladder-logic-examples-60-exercises-you-can-import-into-tia-portal-4kjg</link>
      <guid>https://dev.to/tiaforge/siemens-plc-ladder-logic-examples-60-exercises-you-can-import-into-tia-portal-4kjg</guid>
      <description>&lt;p&gt;&lt;strong&gt;The hard part of learning ladder logic isn't memorizing contact symbols — it's getting from a one-line requirement to a program that actually runs in TIA Portal.&lt;/strong&gt; &lt;a href="https://tia.hubwiz.com/learn-lad/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;Learn-LAD&lt;/a&gt; breaks that step into &lt;strong&gt;60 worked examples&lt;/strong&gt;. Each one gets its own page: the ladder diagram, a network-by-network walkthrough (purpose / condition → action / why / watch out / logic expression), a signal table and a variable table, common questions, and variants. Install &lt;a href="https://tia.hubwiz.com/tia-link?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;TiaLink&lt;/a&gt; and you can also &lt;strong&gt;pre-check the example's objects and then import them&lt;/strong&gt; into the TIA Portal project you have open — same-named objects are not overwritten by default, and the project is not saved automatically after the import.&lt;/p&gt;

&lt;p&gt;It's not a "watch the video, still can't write it" tutorial, and it's not an archive of files you download and forget. It's closer to a &lt;strong&gt;problem set with answers and explanations&lt;/strong&gt;: the tasks mirror real machine behavior, and the answers can be read, modified, or imported into a project to check against.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Where learning ladder logic actually gets stuck (it's rarely syntax)
&lt;/h2&gt;

&lt;p&gt;Contacts, coils, normally open / normally closed — an afternoon is enough to memorize all of it. The real sticking points show up later:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You can read it, but you can't write it.&lt;/strong&gt; You can explain any single network. But give yourself one sentence — "after the start button is pressed, run the motor for 10 seconds, wait 10 seconds, then stop" — and you don't know what belongs in the first rung.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can copy it, but you can't modify it.&lt;/strong&gt; Someone else's program runs fine in your project. Change a timer value, add an interlock, add a mode — and you don't dare touch it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You've practiced, but not correctly.&lt;/strong&gt; The snippets you find online are scattered: no variable table, no addresses, no explanation of why it's written that way. You finish practicing and still don't know whether you got it right.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these three is a syntax problem. They're all the same missing thing: &lt;strong&gt;complete, verifiable, usable examples&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Five criteria for judging a PLC example library
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;th&gt;How Learn-LAD does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real ladder diagrams&lt;/td&gt;
&lt;td&gt;Not pseudocode or a spoken description — you can trace the contacts&lt;/td&gt;
&lt;td&gt;One ladder screenshot per network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network-by-network explanation&lt;/td&gt;
&lt;td&gt;Explains &lt;em&gt;why&lt;/em&gt; it's written this way, not just the answer&lt;/td&gt;
&lt;td&gt;Purpose / condition → action / why / watch out / logic expression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signal table and variable table&lt;/td&gt;
&lt;td&gt;Real addresses and data types, ready to land in a project&lt;/td&gt;
&lt;td&gt;Signal table + variable table + instance DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Common questions and variants&lt;/td&gt;
&lt;td&gt;Tells you in advance where people go wrong and how to extend it&lt;/td&gt;
&lt;td&gt;Two fixed sections on every page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can go into your own project&lt;/td&gt;
&lt;td&gt;Otherwise you never leave "reading other people's code"&lt;/td&gt;
&lt;td&gt;One-click import into TIA Portal via TiaLink&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those five criteria are also the order of this article: what it is, what's on one page, and how to turn it into your own practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What Learn-LAD is: one screen
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Entry&lt;/td&gt;
&lt;td&gt;&lt;a href="https://tia.hubwiz.com/learn-lad/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;tia.hubwiz.com/learn-lad&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;60 examples&lt;/strong&gt;: beginner &lt;strong&gt;23&lt;/strong&gt;, intermediate &lt;strong&gt;33&lt;/strong&gt;, advanced &lt;strong&gt;4&lt;/strong&gt; (verified 2026-10-17)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU tags&lt;/td&gt;
&lt;td&gt;S7-1200: 57, S7-1500: 2, S7-300: 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-block projects&lt;/td&gt;
&lt;td&gt;4 examples are tagged "multi-block", made of a main program + function blocks / functions + data blocks, and may include SCL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page structure&lt;/td&gt;
&lt;td&gt;Overview → signal table → workflow → network-by-network walkthrough → variables and data blocks → common questions → variants and extensions → related examples → next step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How to get it&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Read directly on the page&lt;/strong&gt;; program objects are imported through TiaLink — there is no file download&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import requirement&lt;/td&gt;
&lt;td&gt;A Windows machine with a licensed TIA Portal install (TiaLink is currently built on V19 and Openness)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One thing to make clear up front: &lt;strong&gt;it is not a simulator, and it doesn't hand you a project file that opens with a double click.&lt;/strong&gt; Every example can be read on the page; to get it into a project you go through TiaLink. That's both the limitation and the main difference from a "material pack": there's a pre-check before anything enters your project, and you save explicitly after.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. What the 60 examples cover
&lt;/h2&gt;

&lt;p&gt;Grouped by learning goal, they form roughly this ramp:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Learning goal&lt;/th&gt;
&lt;th&gt;Representative examples&lt;/th&gt;
&lt;th&gt;What you practice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Logic fundamentals&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://tia.hubwiz.com/learn-lad/circuit-to-ladder/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;Circuit diagram → ladder&lt;/a&gt;, &lt;a href="https://tia.hubwiz.com/learn-lad/logic-basics/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;Basic digital logic (AND/OR)&lt;/a&gt;, &lt;a href="https://tia.hubwiz.com/learn-lad/technician-logics/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;Technician logic drills&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Series/parallel, normally closed contacts, the mapping between Boolean expressions and ladder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buttons and state retention&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://tia.hubwiz.com/learn-lad/toggle-output/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;Toggle output with one button&lt;/a&gt;, button-controlled solenoid valve, switch logic, oil pump control&lt;/td&gt;
&lt;td&gt;Self-latching, set/reset, edge detection, "press once on, press again off"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timers and counters&lt;/td&gt;
&lt;td&gt;Coffee grinder timer, flashing indicator, automatic coffee machine, &lt;a href="https://tia.hubwiz.com/learn-lad/traffic-light/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;traffic light control&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;TON / TOF / TP differences, cascaded timers, counter-based phase switching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interlocks and sequence control&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://tia.hubwiz.com/learn-lad/level-pump-control/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;Level pump start/stop&lt;/a&gt;, assembly line control, conveyor sorting, bottle filling and capping&lt;/td&gt;
&lt;td&gt;Condition confirmation, interlocks, anti-short-cycling, multi-step sequences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-block and SCL&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://tia.hubwiz.com/learn-lad/scl-training/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;SCL training: storage function block&lt;/a&gt;, SCL intro: FB calls, Factory IO connection, &lt;a href="https://tia.hubwiz.com/learn-lad/sorting-boxes/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;Sorting boxes (Factory IO)&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;LAD + SCL mixed, FB/FC/DB, main-program scheduling and layered business FBs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The size distribution shows the ramp too: beginner examples are mostly &lt;strong&gt;1 block / 2–4 networks&lt;/strong&gt;; intermediate ones reach &lt;strong&gt;7–8 networks&lt;/strong&gt;; the traffic light in the advanced row is &lt;strong&gt;8 visible networks + 7 imported objects&lt;/strong&gt;, and the Factory IO sorting box is &lt;strong&gt;2 blocks / 19 networks&lt;/strong&gt;, covering start/stop, infeed, left/right sorting, counting and outfeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Inside one page: the traffic light example
&lt;/h2&gt;

&lt;p&gt;An example page doesn't just paste a ladder diagram and stop. The traffic light page, for instance, contains:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Overview&lt;/strong&gt;: three phases (east → west → north), 20 seconds each (15 s green + 5 s yellow), implemented with SR start/stop + a counter for phase switching + three TP timers + comparison instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal table&lt;/strong&gt;: type (I / Q / M / TP / CTU), meaning and role of every point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow&lt;/strong&gt;: the full sequence from START setting STARTED, through the three phases running, to the counter returning to 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network-by-network walkthrough&lt;/strong&gt;: every network follows the same template.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, network 2 (the phase counter) reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Purpose&lt;/strong&gt;: increment the counter at the end of each phase; use CV to select the next phase.&lt;br&gt;
&lt;strong&gt;Condition → action&lt;/strong&gt;: falling edges of &lt;code&gt;STEP1.Q&lt;/code&gt;, &lt;code&gt;STEP2.Q&lt;/code&gt;, &lt;code&gt;STEP3.Q&lt;/code&gt; (NContact) in parallel into the CU input of a CTU; &lt;code&gt;PV = 3&lt;/code&gt;, and CV wraps to 0 after 3.&lt;br&gt;
&lt;strong&gt;Why&lt;/strong&gt;: one counter instead of three interlocked state bits keeps the phase logic much simpler; &lt;code&gt;STEP*_FINISHED&lt;/code&gt; are the edge-memory bits for the three NContacts.&lt;br&gt;
&lt;strong&gt;Watch out&lt;/strong&gt;: NContact detects the &lt;em&gt;falling&lt;/em&gt; edge — the instant the timer pulse ends — not the moment the timer starts.&lt;br&gt;
&lt;strong&gt;Logic&lt;/strong&gt;: &lt;code&gt;COUNTER_STEP = CTU(CU = FALLING(STEP1.Q) OR FALLING(STEP2.Q) OR FALLING(STEP3.Q), PV = 3)&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That short passage tells you three things: &lt;strong&gt;what the network is for, why it uses a counter instead of state bits, and the point people most often misread (the falling edge)&lt;/strong&gt;. That is far more productive than copying the diagram once.&lt;/p&gt;

&lt;p&gt;Further down the page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Variables and data blocks&lt;/strong&gt;: the variable table (with &lt;code&gt;%M&lt;/code&gt; addresses) plus the four timer / counter instance DBs TIA generates automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Common questions&lt;/strong&gt;: where to look first if phases don't switch, what to do when comments and phases don't line up, how to wire the lamp outputs from &lt;code&gt;%M&lt;/code&gt;, which values to change together when you change a phase duration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variants and extensions&lt;/strong&gt;: four phases / with left turn, night-time flashing yellow, pedestrian lights — each with a direction for the change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Related examples and next steps&lt;/strong&gt;: the "what to practice next" list, already ordered.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Two details worth noticing
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;First, the examples flag their own flaws.&lt;/strong&gt; The traffic light page says in its common questions that the comment numbering on Networks 3/4 is off in the original project and to read it via &lt;code&gt;CV&lt;/code&gt; and the actual network logic; the trash compactor cycle notes "the output uses a &lt;code&gt;%I&lt;/code&gt; address, likely a typo"; the delayed lamp page notes "the lamp output in the point list is never driven in the block." &lt;strong&gt;Marking defects instead of hiding them is good for learners&lt;/strong&gt; — real projects contain programs like this, and learning to spot them is more useful than pretending they don't exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, the page gives you a checkable logic expression.&lt;/strong&gt; Besides the ladder diagram, every network has a line like &lt;code&gt;STARTED = SR(SET = START, RESET = STOP)&lt;/code&gt;. Its job isn't to replace the diagram — it's to let you cross-check between diagram and semantics: if the diagram doesn't make sense, read the expression; if the expression doesn't, go back to the diagram.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. How to use it: three routes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Route A: read only (teaching, prepping, quick reference)
&lt;/h3&gt;

&lt;p&gt;Nothing to install — just open the pages. Good for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Self-study, working up the difficulty curve, and asking after each page "what happens if I remove XX?";&lt;/li&gt;
&lt;li&gt;Training / classroom use, with a page as the worked example, the signal table as the board notes, and the common questions as discussion material;&lt;/li&gt;
&lt;li&gt;On the job, when you need a small piece of logic (flashing, off-delay, level interlock) — come here and compare how it's written.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Route B: import into your own project (the recommended path)
&lt;/h3&gt;

&lt;p&gt;This is the biggest difference between Learn-LAD and an ordinary example site. It's four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install and run &lt;a href="https://tia.hubwiz.com/tia-link?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;TiaLink&lt;/a&gt; on Windows (default service address &lt;code&gt;http://localhost:8080&lt;/code&gt;), and open the target TIA Portal project you want to import into at the same time.&lt;/li&gt;
&lt;li&gt;On the example page click "TiaLink settings", confirm the address and test the connection. (If you started TiaLink with &lt;code&gt;--api-token&lt;/code&gt;, enter the same value there; it's stored only in your browser's localStorage, never uploaded.)&lt;/li&gt;
&lt;li&gt;Click "Import into my TIA project": &lt;strong&gt;pre-check first, then import&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Back in TIA Portal, &lt;strong&gt;save explicitly&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three safe defaults to remember: &lt;strong&gt;there's a pre-check before import; same-named objects are not overwritten by default; nothing is saved automatically after import&lt;/strong&gt; — so if you don't like the result, you can just abandon it.&lt;/p&gt;

&lt;p&gt;After importing, take one more step: run it in S7-PLCSIM, force a few inputs, and see whether the output changes match the "workflow" section of the page. &lt;strong&gt;Read → import → verify in simulation&lt;/strong&gt; — only after that loop is an example actually learned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Route C: turn it into your own version
&lt;/h3&gt;

&lt;p&gt;Every example page ends with "Variants and extensions". For the traffic light: add a phase, add night-time flashing yellow, add pedestrian lights. In each case you change a parameter or add a mode bit first, then extend the output mapping. Verify in simulation again — &lt;strong&gt;that's the transition from "reading someone's program" to "defining your own requirement and verifying it yourself"&lt;/strong&gt;, which is the jump the library is ultimately there to help you make.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Who it's for&lt;/th&gt;
&lt;th&gt;What you need&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A Read only&lt;/td&gt;
&lt;td&gt;Self-study, teaching, quick reference&lt;/td&gt;
&lt;td&gt;A browser&lt;/td&gt;
&lt;td&gt;Understanding + a logic explanation you can check against&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B Import&lt;/td&gt;
&lt;td&gt;Engineers / students with a TIA Portal setup&lt;/td&gt;
&lt;td&gt;Windows + TIA Portal + TiaLink&lt;/td&gt;
&lt;td&gt;Real project objects you can compile and simulate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C Modify&lt;/td&gt;
&lt;td&gt;People who want to practice independent design&lt;/td&gt;
&lt;td&gt;Everything in B + a bit of patience&lt;/td&gt;
&lt;td&gt;Your own variant and a verification habit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  6. A 4-week practice plan (copy it as-is)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Week&lt;/th&gt;
&lt;th&gt;Suggested examples&lt;/th&gt;
&lt;th&gt;Weekly acceptance test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Week 1&lt;/td&gt;
&lt;td&gt;Circuit → ladder, basic digital logic, technician logic drills, toggle output&lt;/td&gt;
&lt;td&gt;Without looking anything up, draw &lt;code&gt;Y=(A OR B) AND (NOT C OR D)&lt;/code&gt; as ladder and explain the role of every NC contact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Week 2&lt;/td&gt;
&lt;td&gt;Coffee grinder timer, flashing indicator, automatic door with delay, switch + timed output&lt;/td&gt;
&lt;td&gt;Explain what TON / TOF / TP each do in that example; change a time parameter and predict the output change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Week 3&lt;/td&gt;
&lt;td&gt;Level pump control, assembly line control, traffic light, multi-stage conveyor&lt;/td&gt;
&lt;td&gt;Point out each example's interlocks; say what happens if a sensor chatters, and which part of the example exists for exactly that&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Week 4&lt;/td&gt;
&lt;td&gt;SCL training: storage FB, Factory IO connection, Sorting boxes (Factory IO)&lt;/td&gt;
&lt;td&gt;Explain the division of labor between main program and FBs; import an example, compile it, and run the basic flow in simulation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  7. Boundaries and caveats (read before you practice)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;These are teaching examples, not safety programs you can put into production.&lt;/strong&gt; Emergency stop, safety circuits and hard interlocks are hardware and functional-safety scope; no example replaces safety design and acceptance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Many outputs use &lt;code&gt;%M&lt;/code&gt; intermediate bits&lt;/strong&gt;; map them to &lt;code&gt;%Q&lt;/code&gt; for your own hardware before connecting real devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import has environment prerequisites&lt;/strong&gt;: TiaLink runs on the same machine as TIA Portal and is currently built on TIA Portal V19; MCP / clients can connect from another machine over the LAN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import does not equal trustworthy&lt;/strong&gt;: pre-check, compile, simulation and human review are still all required. Automation saves you repeated copying and address mistakes; engineering responsibility stays with the engineer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No file downloads&lt;/strong&gt; — examples only enter a project via TiaLink, though everything is readable on the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The library keeps changing&lt;/strong&gt;: the counts, categories and CPU tags in this article are as of the site on 2026-10-17; check the site for the current state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q1: Does the library cost anything?&lt;/strong&gt;&lt;br&gt;
The example pages are free to read. Importing depends on TiaLink — see the &lt;a href="https://tia.hubwiz.com/tia-link?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;TiaLink product page&lt;/a&gt; for current availability and licensing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q2: Can I use it without TIA Portal installed?&lt;/strong&gt;&lt;br&gt;
Yes, for reading and learning. But importing requires a licensed TIA Portal on the same machine, because TiaLink wraps TIA Portal's Openness capabilities and has to live next to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q3: Can I download the program files from the site?&lt;/strong&gt;&lt;br&gt;
No. It's designed that way: no files land on disk, everything goes through TiaLink, so there's a pre-check before import and no automatic save after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q4: Will importing overwrite blocks with the same name in my project?&lt;/strong&gt;&lt;br&gt;
By default, no — same-named objects are not overwritten, and there's a pre-check first. Even after the import completes, the project isn't saved automatically, so you can inspect before deciding to keep it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q5: Can I use these examples directly on my machines?&lt;/strong&gt;&lt;br&gt;
Not recommended as-is. The point of a teaching example is to make the logic clear; before real hardware you still need I/O mapping, a safety assessment, commissioning and acceptance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q6: Is it suitable for teachers?&lt;/strong&gt;&lt;br&gt;
Yes. Every page has a signal table, workflow, network-by-network explanation and common questions, usable as worked examples, homework and discussion material; the import feature lets students bring the answers into their own project and verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Summary
&lt;/h2&gt;

&lt;p&gt;What's missing when people learn ladder logic usually isn't "more knowledge" — it's &lt;strong&gt;enough complete, self-checkable practice&lt;/strong&gt;. Learn-LAD puts 60 exercises into one consistent format: clear requirement, legible diagram, an explanation for every rung, address tables, and the usual pitfalls. Add TiaLink's "pre-check → import → explicit save" and the gap between "I understand it" and "it runs" is no longer a manual retyping exercise.&lt;/p&gt;

&lt;p&gt;Where to start? Begin with &lt;a href="https://tia.hubwiz.com/learn-lad/toggle-output/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;toggle output&lt;/a&gt; — only 4 networks and 2 imported objects, but it already uses self-latching, edges and state evaluation. Then take on &lt;a href="https://tia.hubwiz.com/learn-lad/traffic-light/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;traffic light control&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verification notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The example library, page structure and counts come from the official Learn-LAD site &lt;a href="https://tia.hubwiz.com/learn-lad/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;tia.hubwiz.com/learn-lad&lt;/a&gt;, verified 2026-10-17; the import mechanism and version dependencies come from the &lt;a href="https://tia.hubwiz.com/tia-link?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;TiaLink product page&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Example counts, categories and CPU tags are public site data; the current site version is authoritative.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/ai-industrial-stack/?utm_source=devto&amp;amp;utm_medium=syndication" rel="noopener noreferrer"&gt;AI × Industrial / PLC: the full guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/virtual-plc-lab-factory-io-openplc-modbus-tcp/?utm_source=devto&amp;amp;utm_medium=syndication" rel="noopener noreferrer"&gt;Practice PLC without hardware: Factory I/O + OpenPLC virtual lab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/lad-to-scl-migration-guide-tia-portal/?utm_source=devto&amp;amp;utm_medium=syndication" rel="noopener noreferrer"&gt;Ladder → SCL migration in practice: what converts, what must be refactored, how to verify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/taking-over-legacy-plc-program-4-steps/?utm_source=devto&amp;amp;utm_medium=syndication" rel="noopener noreferrer"&gt;Taking over someone else's PLC program: 4 things to do before you change a line&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/top7-ladder-logic-mistakes-that-trip-up-new-programmers/?utm_source=devto&amp;amp;utm_medium=syndication" rel="noopener noreferrer"&gt;The 7 ladder logic mistakes that trip up new programmers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hubwiz.com/blog/tialink-give-your-ai-agent-hands-to-operate-tia-portal/?utm_source=devto&amp;amp;utm_medium=syndication" rel="noopener noreferrer"&gt;Give your AI agent hands to operate TIA Portal&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools: &lt;a href="https://tia.hubwiz.com/learn-lad/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;Learn-LAD example library&lt;/a&gt; ｜ &lt;a href="https://tia.hubwiz.com/tia-link?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;TiaLink&lt;/a&gt; ｜ &lt;a href="https://tia.hubwiz.com/lad2scl?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;Ladder → SCL&lt;/a&gt; ｜ &lt;a href="https://tia.hubwiz.com/txt2lad?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;Text → ladder&lt;/a&gt; ｜ &lt;a href="https://tia.hubwiz.com/tia-forge?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=siemens-plc-ladder-example-library" rel="noopener noreferrer"&gt;TIA Forge&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Chinese original of this article is here: &lt;a href="https://www.hubwiz.com/blog/siemens-plc-ladder-example-library/?utm_source=devto&amp;amp;utm_medium=syndication" rel="noopener noreferrer"&gt;西门子 PLC 梯形图示例库：60 个可导入博途的练习题&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Learn-LAD and TiaLink are built by our team at Hubwiz (汇智网).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>plc</category>
      <category>automation</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>An AI Agent Programmed a Siemens S7-1500 Through an MCP Bridge — Here's the SCL and the Race Conditions It Caught</title>
      <dc:creator>tia-forge</dc:creator>
      <pubDate>Mon, 12 Oct 2026 15:00:26 +0000</pubDate>
      <link>https://dev.to/tiaforge/an-ai-agent-programmed-a-siemens-s7-1500-through-an-mcp-bridge-heres-the-scl-and-the-race-fb7</link>
      <guid>https://dev.to/tiaforge/an-ai-agent-programmed-a-siemens-s7-1500-through-an-mcp-bridge-heres-the-scl-and-the-race-fb7</guid>
      <description>&lt;p&gt;We built an MCP server that drives &lt;strong&gt;Siemens TIA Portal V19 through the Openness API&lt;/strong&gt;, pointed an LLM agent at it, and gave it a real engineering task: implement a conveyor-sorting control program for an &lt;strong&gt;S7-1500 (CPU 1511-1 PN)&lt;/strong&gt;, verified in &lt;strong&gt;S7-PLCSIM&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not "generate a snippet." The whole loop:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;add CPU → compile → start simulator → create FB/UDTs → write SCL → compile → download → verify.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I expected the code to be the interesting part. It wasn't. The interesting part is &lt;em&gt;which&lt;/em&gt; engineering decisions the agent made, and where it drew a line and refused to cross it.&lt;/p&gt;

&lt;p&gt;Here's what happened, with the actual code and the failure modes it flagged.&lt;/p&gt;

&lt;h2&gt;
  
  
  The toolchain
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent&lt;/strong&gt;: an LLM agent (WorkBuddy) that reads the spec, plans steps, writes code and tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bridge&lt;/strong&gt;: an MCP server (TiaLink) exposing TIA Portal operations over MCP — add device, create block, write SCL, compile, download, read back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target&lt;/strong&gt;: TIA Portal V19 with Openness, S7-PLCSIM for verification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bridge runs on the same machine as TIA Portal (an industrial PC); the agent can run elsewhere as long as it can reach the bridge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt
&lt;/h2&gt;

&lt;p&gt;Hardware and network configuration were already done. The task:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SCL &lt;strong&gt;state machine&lt;/strong&gt;, with state numbers identical to the spec (so HMI, alarms, tests and field debugging all reference the same numbering)&lt;/li&gt;
&lt;li&gt;Logic as a &lt;strong&gt;reusable FB with its own instance DB&lt;/strong&gt; (line #2 becomes a second instance DB, not copy-paste)&lt;/li&gt;
&lt;li&gt;Clear separation of &lt;code&gt;i_&lt;/code&gt; inputs / &lt;code&gt;q_&lt;/code&gt; outputs / &lt;code&gt;stat_&lt;/code&gt; internals&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;IEC timers&lt;/strong&gt; (&lt;code&gt;TON_TIME&lt;/code&gt;) — no hand-rolled scan-cycle accumulation&lt;/li&gt;
&lt;li&gt;Implement: entry de-dup, quality-result latch, dual-cylinder interlock, fault latch, manual reset, normal stop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every output must have a deterministic value every scan&lt;/strong&gt; — no reliance on last-cycle residue&lt;/li&gt;
&lt;li&gt;Emergency stop is only read as a &lt;strong&gt;safety-circuit status input&lt;/strong&gt;; ordinary PLC code is not a safety function&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Deliverables: FB SCL source, UDTs, state-transition doc, ≥10 automated test cases, boundary/race analysis.&lt;/p&gt;

&lt;p&gt;And one line at the end:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"First add a CPU and check whether the simulator works."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That line is the part I actually cared about testing. Config tasks are judged from a screenshot. &lt;strong&gt;Programs are judged by behavior.&lt;/strong&gt; So: does the agent build the verification environment first, or just start writing code?&lt;/p&gt;

&lt;h2&gt;
  
  
  The sequence it chose
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Check whether a CPU already existed — only then add &lt;code&gt;PLC_1&lt;/code&gt; (CPU 1511-1 PN)&lt;/li&gt;
&lt;li&gt;Compile, start &lt;strong&gt;S7-PLCSIM&lt;/strong&gt;, confirm download/run works&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;FB_ConveyorSort&lt;/code&gt; and write the state machine in SCL&lt;/li&gt;
&lt;li&gt;Create the needed UDTs&lt;/li&gt;
&lt;li&gt;Call the FB in &lt;code&gt;OB1&lt;/code&gt; with instance DB &lt;code&gt;DB_ConveyorSort_1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Compile + download — no errors&lt;/li&gt;
&lt;li&gt;Deliver the state transition table, &lt;strong&gt;14 test cases&lt;/strong&gt;, and a race/boundary analysis&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It built the verification environment first. Good sign.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state machine
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Enter when&lt;/th&gt;
&lt;th&gt;Main action&lt;/th&gt;
&lt;th&gt;Exit → target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;INIT&lt;/td&gt;
&lt;td&gt;first scan&lt;/td&gt;
&lt;td&gt;clear latches, fault code, commands&lt;/td&gt;
&lt;td&gt;unconditional → 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;READY&lt;/td&gt;
&lt;td&gt;power-on / reset / after stop&lt;/td&gt;
&lt;td&gt;conveyor stopped, wait for start&lt;/td&gt;
&lt;td&gt;start → 20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;FEED&lt;/td&gt;
&lt;td&gt;after start&lt;/td&gt;
&lt;td&gt;conveyor runs; entry de-dup; latch quality; sort timeout&lt;/td&gt;
&lt;td&gt;at sort pos → 30; timeout → 90; stop → 100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;CHECK&lt;/td&gt;
&lt;td&gt;at sort position&lt;/td&gt;
&lt;td&gt;stop conveyor, branch on latched result&lt;/td&gt;
&lt;td&gt;OK → 50; NG → 60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;PUSH_A&lt;/td&gt;
&lt;td&gt;judged OK&lt;/td&gt;
&lt;td&gt;cylinder A extend, push hold&lt;/td&gt;
&lt;td&gt;extended + hold done → 70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;PUSH_B&lt;/td&gt;
&lt;td&gt;judged NG&lt;/td&gt;
&lt;td&gt;cylinder B extend, push hold&lt;/td&gt;
&lt;td&gt;extended + hold done → 70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;RETRACT&lt;/td&gt;
&lt;td&gt;push done&lt;/td&gt;
&lt;td&gt;cylinder retract, retract timeout&lt;/td&gt;
&lt;td&gt;retracted → 80; timeout → 90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;DISCHARGE&lt;/td&gt;
&lt;td&gt;retracted&lt;/td&gt;
&lt;td&gt;wait for part to leave sort position&lt;/td&gt;
&lt;td&gt;sort photo-eye clear → 20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;FAULT&lt;/td&gt;
&lt;td&gt;any fault / safety circuit open&lt;/td&gt;
&lt;td&gt;all outputs off, latch fault&lt;/td&gt;
&lt;td&gt;reset + conditions OK → 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;STOPPED&lt;/td&gt;
&lt;td&gt;normal stop&lt;/td&gt;
&lt;td&gt;outputs off, restartable&lt;/td&gt;
&lt;td&gt;start → 20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The SCL
&lt;/h2&gt;

&lt;p&gt;Here's the full function block it delivered. Two things to look at as you read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Section 0&lt;/strong&gt; writes every output to a deterministic value at the top of the cycle (requirement #6).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Section 7&lt;/strong&gt; is the final output mapping with the interlock and the same determinism guarantee.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight pascal"&gt;&lt;code&gt;&lt;span class="n"&gt;FUNCTION_BLOCK&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;FB_ConveyorSort&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;
&lt;span class="cm"&gt;{ S7_Optimized_Access := 'TRUE' }&lt;/span&gt;
&lt;span class="n"&gt;VERSION&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;

&lt;span class="n"&gt;VAR_INPUT&lt;/span&gt;
   &lt;span class="n"&gt;i_xSafetyOK&lt;/span&gt;     &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// safety circuit healthy (1 = closed, run allowed)
&lt;/span&gt;   &lt;span class="n"&gt;i_xStart&lt;/span&gt;        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// start (edge-triggered)
&lt;/span&gt;   &lt;span class="n"&gt;i_xStop&lt;/span&gt;         &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// normal stop (edge-triggered)
&lt;/span&gt;   &lt;span class="n"&gt;i_xReset&lt;/span&gt;        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// fault reset (edge-triggered)
&lt;/span&gt;   &lt;span class="n"&gt;i_xPartAtEntry&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// entry photo-eye
&lt;/span&gt;   &lt;span class="n"&gt;i_xPartAtSort&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// sort-position photo-eye
&lt;/span&gt;   &lt;span class="n"&gt;i_xQualityOK&lt;/span&gt;    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// quality result: 1 = OK
&lt;/span&gt;   &lt;span class="n"&gt;i_xCylA_ExtEnd&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// cylinder A extended limit
&lt;/span&gt;   &lt;span class="n"&gt;i_xCylA_RetEnd&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// cylinder A retracted limit
&lt;/span&gt;   &lt;span class="n"&gt;i_xCylB_ExtEnd&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// cylinder B extended limit
&lt;/span&gt;   &lt;span class="n"&gt;i_xCylB_RetEnd&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// cylinder B retracted limit
&lt;/span&gt;   &lt;span class="n"&gt;i_tSortTimeout&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// sort timeout
&lt;/span&gt;   &lt;span class="n"&gt;i_tPushHold&lt;/span&gt;     &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;800&lt;/span&gt;&lt;span class="n"&gt;MS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// push hold time
&lt;/span&gt;   &lt;span class="n"&gt;i_tRetractTO&lt;/span&gt;    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// retract timeout
&lt;/span&gt;&lt;span class="n"&gt;END_VAR&lt;/span&gt;

&lt;span class="n"&gt;VAR_OUTPUT&lt;/span&gt;
   &lt;span class="n"&gt;q_xConveyor&lt;/span&gt;     &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xCylA_Extend&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xCylA_Retract&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xCylB_Extend&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xCylB_Retract&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xFault&lt;/span&gt;        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xReady&lt;/span&gt;        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_wState&lt;/span&gt;        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Word&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;END_VAR&lt;/span&gt;

&lt;span class="k"&gt;VAR&lt;/span&gt;
   &lt;span class="n"&gt;stat_wState&lt;/span&gt;     &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Word&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// current state
&lt;/span&gt;   &lt;span class="n"&gt;stat_wFaultCode&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Word&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// fault code
&lt;/span&gt;   &lt;span class="n"&gt;stat_xFault&lt;/span&gt;     &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// latched fault
&lt;/span&gt;   &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// cylinder A extend request
&lt;/span&gt;   &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// cylinder B extend request
&lt;/span&gt;   &lt;span class="n"&gt;stat_xQualityNG&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// latched quality result (1 = reject)
&lt;/span&gt;   &lt;span class="n"&gt;stat_xPartSeen&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// entry de-dup latch
&lt;/span&gt;   &lt;span class="n"&gt;stat_xInTransit&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// part in transit
&lt;/span&gt;   &lt;span class="n"&gt;stat_xStartEdge&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;stat_xStopEdge&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;stat_xResetEdge&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;stat_xStartOld&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;stat_xStopOld&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;stat_xResetOld&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;stat_tSortTO&lt;/span&gt;    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TON_TIME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// sort timeout
&lt;/span&gt;   &lt;span class="n"&gt;stat_tPush&lt;/span&gt;      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TON_TIME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// push hold
&lt;/span&gt;   &lt;span class="n"&gt;stat_tRetract&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TON_TIME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// retract timeout
&lt;/span&gt;&lt;span class="n"&gt;END_VAR&lt;/span&gt;

&lt;span class="k"&gt;BEGIN&lt;/span&gt;
   &lt;span class="c1"&gt;// ---------- 0. write every output to a deterministic value each cycle ----------
&lt;/span&gt;   &lt;span class="n"&gt;q_xConveyor&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xCylA_Extend&lt;/span&gt;  &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xCylA_Retract&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xCylB_Extend&lt;/span&gt;  &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xCylB_Retract&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xFault&lt;/span&gt;        &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xReady&lt;/span&gt;        &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_wState&lt;/span&gt;        &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;stat_wState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="c1"&gt;// ---------- 1. button edge extraction ----------
&lt;/span&gt;   &lt;span class="n"&gt;stat_xStartEdge&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_xStart&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;stat_xStartOld&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;stat_xStopEdge&lt;/span&gt;  &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_xStop&lt;/span&gt;  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;stat_xStopOld&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;stat_xResetEdge&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_xReset&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;stat_xResetOld&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;stat_xStartOld&lt;/span&gt;  &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_xStart&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;stat_xStopOld&lt;/span&gt;   &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_xStop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;stat_xResetOld&lt;/span&gt;  &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_xReset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="c1"&gt;// ---------- 2. safety circuit: read-only, do NOT implement a safety function ----------
&lt;/span&gt;   &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;i_xSafetyOK&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
      &lt;span class="n"&gt;stat_wFaultCode&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;0001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// safety circuit open
&lt;/span&gt;      &lt;span class="n"&gt;stat_xFault&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="c1"&gt;// ---------- 3. sensor plausibility checks ----------
&lt;/span&gt;   &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;i_xCylA_ExtEnd&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;i_xCylA_RetEnd&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
      &lt;span class="n"&gt;stat_wFaultCode&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;0002&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// cylinder A contradictory position sensors
&lt;/span&gt;      &lt;span class="n"&gt;stat_xFault&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;i_xCylB_ExtEnd&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;i_xCylB_RetEnd&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
      &lt;span class="n"&gt;stat_wFaultCode&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;0003&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// cylinder B contradictory position sensors
&lt;/span&gt;      &lt;span class="n"&gt;stat_xFault&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="c1"&gt;// ---------- 4. dual-cylinder interlock check (software supplement, NOT a hard interlock) ----------
&lt;/span&gt;   &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
      &lt;span class="n"&gt;stat_wFaultCode&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;0010&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// both cylinders requested to extend
&lt;/span&gt;      &lt;span class="n"&gt;stat_xFault&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="c1"&gt;// ---------- 5. fault reset (requires human confirmation AND recovered conditions) ----------
&lt;/span&gt;   &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;stat_xFault&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
      &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;stat_xResetEdge&lt;/span&gt;
         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;i_xSafetyOK&lt;/span&gt;
         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i_xCylA_ExtEnd&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;i_xCylA_RetEnd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i_xCylB_ExtEnd&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;i_xCylB_RetEnd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
         &lt;span class="n"&gt;stat_xFault&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_wFaultCode&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;0000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_wState&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="c1"&gt;// ---------- 6. state machine ----------
&lt;/span&gt;   &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;stat_xFault&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
      &lt;span class="n"&gt;stat_wState&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// fault has priority, force fault state
&lt;/span&gt;   &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;stat_wState&lt;/span&gt; &lt;span class="k"&gt;OF&lt;/span&gt;
      &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;// INIT on power-up
&lt;/span&gt;         &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt;   &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt;   &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xQualityNG&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xPartSeen&lt;/span&gt;  &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xInTransit&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_wFaultCode&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;0000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_wState&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// READY waiting for start
&lt;/span&gt;         &lt;span class="n"&gt;q_xReady&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;stat_xStartEdge&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;stat_wState&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// FEED conveying
&lt;/span&gt;         &lt;span class="n"&gt;q_xConveyor&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;q_xReady&lt;/span&gt;    &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;stat_xStopEdge&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;stat_wState&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="k"&gt;ELSE&lt;/span&gt;
            &lt;span class="c1"&gt;// entry de-dup: latch only on "new part entered"
&lt;/span&gt;            &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;i_xPartAtEntry&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;stat_xPartSeen&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
               &lt;span class="n"&gt;stat_xPartSeen&lt;/span&gt;  &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
               &lt;span class="n"&gt;stat_xInTransit&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
               &lt;span class="n"&gt;stat_xQualityNG&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;i_xQualityOK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// latch quality result
&lt;/span&gt;            &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;i_xPartAtEntry&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
               &lt;span class="n"&gt;stat_xPartSeen&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// part left entry, allow the next one
&lt;/span&gt;            &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="c1"&gt;// sort timeout
&lt;/span&gt;            &lt;span class="n"&gt;stat_tSortTO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;stat_xInTransit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PT&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_tSortTimeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;stat_tSortTO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Q&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
               &lt;span class="n"&gt;stat_wFaultCode&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;0020&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// sort timeout
&lt;/span&gt;               &lt;span class="n"&gt;stat_xFault&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
               &lt;span class="n"&gt;stat_wState&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;ELSIF&lt;/span&gt; &lt;span class="n"&gt;i_xPartAtSort&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
               &lt;span class="n"&gt;stat_xInTransit&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
               &lt;span class="n"&gt;stat_tSortTO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PT&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_tSortTimeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
               &lt;span class="n"&gt;stat_wState&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// CHECK decide on latched result
&lt;/span&gt;         &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_tPush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PT&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_tPushHold&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
         &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;stat_xQualityNG&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;stat_wState&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                        &lt;span class="c1"&gt;// reject -&amp;gt; cylinder B
&lt;/span&gt;         &lt;span class="k"&gt;ELSE&lt;/span&gt;
            &lt;span class="n"&gt;stat_wState&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                        &lt;span class="c1"&gt;// OK     -&amp;gt; cylinder A
&lt;/span&gt;         &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// PUSH_A extend A for a good part
&lt;/span&gt;         &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                      &lt;span class="c1"&gt;// interlock: B stays retracted
&lt;/span&gt;         &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_xCylB_RetEnd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// only allow A after B is confirmed retracted
&lt;/span&gt;         &lt;span class="n"&gt;stat_tPush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PT&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_tPushHold&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
         &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;i_xCylA_ExtEnd&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;stat_tPush&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Q&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                   &lt;span class="c1"&gt;// push done, retract
&lt;/span&gt;            &lt;span class="n"&gt;stat_wState&lt;/span&gt;   &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;70&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// PUSH_B extend B for a reject
&lt;/span&gt;         &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                      &lt;span class="c1"&gt;// interlock: A stays retracted
&lt;/span&gt;         &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_xCylA_RetEnd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// only allow B after A is confirmed retracted
&lt;/span&gt;         &lt;span class="n"&gt;stat_tPush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PT&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_tPushHold&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
         &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;i_xCylB_ExtEnd&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;stat_tPush&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Q&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;stat_wState&lt;/span&gt;   &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;70&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="m"&gt;70&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// RETRACT confirm retraction
&lt;/span&gt;         &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_tRetract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PT&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_tRetractTO&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
         &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stat_xQualityNG&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;i_xCylB_RetEnd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;stat_xQualityNG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;i_xCylA_RetEnd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;stat_tRetract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PT&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_tRetractTO&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;stat_wState&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;stat_tRetract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Q&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;stat_wFaultCode&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;0030&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// retract timeout
&lt;/span&gt;            &lt;span class="n"&gt;stat_xFault&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;stat_wState&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="m"&gt;80&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// DISCHARGE wait for the part to leave the sort position
&lt;/span&gt;         &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;i_xPartAtSort&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;stat_wState&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="m"&gt;90&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// FAULT latched
&lt;/span&gt;         &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_tSortTO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PT&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_tSortTimeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
         &lt;span class="n"&gt;stat_tPush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PT&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_tPushHold&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
         &lt;span class="n"&gt;stat_tRetract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PT&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_tRetractTO&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// STOPPED normal stop
&lt;/span&gt;         &lt;span class="n"&gt;q_xReady&lt;/span&gt;    &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_xInTransit&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_tSortTO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PT&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i_tSortTimeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
         &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;stat_xStartEdge&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;stat_wState&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;END_IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;ELSE&lt;/span&gt;
         &lt;span class="n"&gt;stat_wFaultCode&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;00F&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                   &lt;span class="c1"&gt;// illegal state
&lt;/span&gt;         &lt;span class="n"&gt;stat_xFault&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         &lt;span class="n"&gt;stat_wState&lt;/span&gt;     &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;END_CASE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="c1"&gt;// ---------- 7. output mapping: interlock + deterministic value each cycle ----------
&lt;/span&gt;   &lt;span class="n"&gt;q_xCylA_Extend&lt;/span&gt;  &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;i_xCylB_RetEnd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xCylB_Extend&lt;/span&gt;  &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;stat_xCmdBExt&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;stat_xCmdAExt&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;i_xCylA_RetEnd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xCylA_Retract&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;q_xCylA_Extend&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_xCylB_Retract&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;q_xCylB_Extend&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="n"&gt;q_xFault&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;stat_xFault&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;q_wState&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;stat_wState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;END_FUNCTION_BLOCK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: this assumes double-acting cylinders where retract is the de-energized safe direction. For spring-return cylinders, the "retract output" should be forced to 0 (de-energized = retracted) rather than commanded.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The UDTs
&lt;/h2&gt;

&lt;p&gt;A parameter UDT so multiple lines can reuse the same FB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight pascal"&gt;&lt;code&gt;&lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;UDT_SortParams&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;VERSION&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;
   &lt;span class="n"&gt;STRUCT&lt;/span&gt;
      &lt;span class="n"&gt;SortTimeout&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// sort timeout
&lt;/span&gt;      &lt;span class="n"&gt;PushHold&lt;/span&gt;    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;800&lt;/span&gt;&lt;span class="n"&gt;MS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// push hold time
&lt;/span&gt;      &lt;span class="n"&gt;RetractTO&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// retract timeout
&lt;/span&gt;   &lt;span class="n"&gt;END_STRUCT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;END_TYPE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A status UDT for HMI display and online diagnostics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight pascal"&gt;&lt;code&gt;&lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;UDT_SortStatus&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;VERSION&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;
   &lt;span class="n"&gt;STRUCT&lt;/span&gt;
      &lt;span class="n"&gt;State&lt;/span&gt;        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Word&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// current state
&lt;/span&gt;      &lt;span class="n"&gt;FaultCode&lt;/span&gt;    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Word&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// fault code
&lt;/span&gt;      &lt;span class="n"&gt;FaultLatched&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// fault latched
&lt;/span&gt;      &lt;span class="n"&gt;QualityNG&lt;/span&gt;    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// quality result latched
&lt;/span&gt;      &lt;span class="n"&gt;PartSeen&lt;/span&gt;     &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// entry de-dup latch
&lt;/span&gt;      &lt;span class="n"&gt;InTransit&lt;/span&gt;    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// part in transit
&lt;/span&gt;   &lt;span class="n"&gt;END_STRUCT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;END_TYPE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The 14 test cases it shipped
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Precondition&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TC-01&lt;/td&gt;
&lt;td&gt;Power-on init&lt;/td&gt;
&lt;td&gt;downloaded to sim&lt;/td&gt;
&lt;td&gt;first scan&lt;/td&gt;
&lt;td&gt;state 10, all outputs 0, no fault&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-02&lt;/td&gt;
&lt;td&gt;Normal start&lt;/td&gt;
&lt;td&gt;state 10&lt;/td&gt;
&lt;td&gt;press start&lt;/td&gt;
&lt;td&gt;state 20, conveyor runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-03&lt;/td&gt;
&lt;td&gt;Good part sort&lt;/td&gt;
&lt;td&gt;state 20&lt;/td&gt;
&lt;td&gt;part at entry, quality OK, reaches sort pos&lt;/td&gt;
&lt;td&gt;20→30→50→70→80→20, cylinder A extends then retracts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-04&lt;/td&gt;
&lt;td&gt;Bad part sort&lt;/td&gt;
&lt;td&gt;state 20&lt;/td&gt;
&lt;td&gt;part at entry, quality NG&lt;/td&gt;
&lt;td&gt;20→30→60→70→80→20, cylinder B extends then retracts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-05&lt;/td&gt;
&lt;td&gt;Entry de-dup&lt;/td&gt;
&lt;td&gt;state 20&lt;/td&gt;
&lt;td&gt;part sits at entry for 100 scans&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PartSeen&lt;/code&gt; latches once, no repeat triggering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-06&lt;/td&gt;
&lt;td&gt;Sort timeout&lt;/td&gt;
&lt;td&gt;state 20&lt;/td&gt;
&lt;td&gt;part doesn't reach sort pos within 5 s&lt;/td&gt;
&lt;td&gt;state 90, fault 16#0020 latched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-07&lt;/td&gt;
&lt;td&gt;Fault latch + reset&lt;/td&gt;
&lt;td&gt;state 90, conditions recovered&lt;/td&gt;
&lt;td&gt;press start, then reset&lt;/td&gt;
&lt;td&gt;start ignored; reset + conditions OK → state 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-08&lt;/td&gt;
&lt;td&gt;Reset refused&lt;/td&gt;
&lt;td&gt;state 90, safety circuit still open&lt;/td&gt;
&lt;td&gt;press reset&lt;/td&gt;
&lt;td&gt;reset ineffective, stays in state 90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-09&lt;/td&gt;
&lt;td&gt;Normal stop + resume&lt;/td&gt;
&lt;td&gt;state 20&lt;/td&gt;
&lt;td&gt;press stop, then start&lt;/td&gt;
&lt;td&gt;stop → state 100 outputs safe; start → state 20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-10&lt;/td&gt;
&lt;td&gt;Dual-cylinder interlock&lt;/td&gt;
&lt;td&gt;state 20&lt;/td&gt;
&lt;td&gt;force both extend requests = 1&lt;/td&gt;
&lt;td&gt;fault 16#0010, neither cylinder extends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-11&lt;/td&gt;
&lt;td&gt;Contradictory position sensors&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;force A ext-end and ret-end = 1 together&lt;/td&gt;
&lt;td&gt;fault 16#0002 latched, state 90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-12&lt;/td&gt;
&lt;td&gt;Safety circuit open&lt;/td&gt;
&lt;td&gt;state 20&lt;/td&gt;
&lt;td&gt;&lt;code&gt;i_xSafetyOK = 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;outputs drop immediately, state 90 latched; needs reset after recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-13&lt;/td&gt;
&lt;td&gt;Retract timeout&lt;/td&gt;
&lt;td&gt;state 70&lt;/td&gt;
&lt;td&gt;cylinder doesn't retract within 3 s&lt;/td&gt;
&lt;td&gt;fault 16#0030, state 90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-14&lt;/td&gt;
&lt;td&gt;Output determinism&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;single-step and watch outputs&lt;/td&gt;
&lt;td&gt;every scan all outputs have defined values, no residue&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The race conditions it flagged (the actually useful part)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Entry de-dup vs. sensor sticking&lt;/strong&gt; — latching until the part leaves the entry photo-eye de-dupes naturally, but &lt;em&gt;only if the part actually leaves&lt;/em&gt;; otherwise later parts get missed. The process must guarantee single-piece spacing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to latch the quality result&lt;/strong&gt; — it latches at the &lt;strong&gt;entry&lt;/strong&gt;, not at the sort position, because the quality system's result may not be stable by the time the part arrives, and you'd race the position signal. If quality detection is slow, use a handshake/queue instead of just delaying the read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arrival vs. timeout in the same scan&lt;/strong&gt; — IEC &lt;code&gt;TON&lt;/code&gt; sets &lt;code&gt;Q = TRUE&lt;/code&gt; on the scan it reaches &lt;code&gt;PT&lt;/code&gt;. It checks timeout &lt;em&gt;before&lt;/em&gt; arrival and explicitly resets the timer on arrival. If you want "arrival wins," you must reorder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-trigger after push&lt;/strong&gt; — the part can still briefly cover the sort photo-eye. Going 70→20 directly would cause a double-sort. State 80 (DISCHARGE) waits for the eye to clear before re-arming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interlock layering&lt;/strong&gt; — software only stops "logically simultaneous requests." It does &lt;strong&gt;not&lt;/strong&gt; replace pneumatic/electrical hard interlocks or mechanical limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Button priority in the same scan&lt;/strong&gt; — fault/safety &amp;gt; stop &amp;gt; reset &amp;gt; start, and this must be frozen in the requirements or implementations will diverge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Stop" has two meanings&lt;/strong&gt; — immediate vs. finish-current-part. It implements immediate (state 100); a controlled finish needs a &lt;code&gt;StopPending&lt;/code&gt; flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PT = 0&lt;/code&gt; boundary&lt;/strong&gt; — &lt;code&gt;TON&lt;/code&gt; fires immediately, which can turn a timeout into a guaranteed fault. Validate the lower bound if the time is HMI-writable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Held reset button&lt;/strong&gt; — edge-triggered, so a long press doesn't keep masking a recurring fault.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-scan init&lt;/strong&gt; — state 0 does init; a proper project would also use &lt;code&gt;OB100&lt;/code&gt;. Kept state 0 so init stays visible and testable inside the state machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple instances&lt;/strong&gt; — timers and edge memories live in FB statics, so &lt;strong&gt;never share one instance DB across lines&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sim ≠ field&lt;/strong&gt; — the simulator doesn't model photo-eye chatter, pneumatic delay, or safety-circuit response time. All times must be tuned on the real machine.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The three things it explicitly refused to do
&lt;/h2&gt;

&lt;p&gt;This is the part I think matters most, and why I wrote this up at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Emergency stop&lt;/strong&gt; must be realized by a hardware safety circuit (safety relay / safety PLC). The program only reads &lt;code&gt;i_xSafetyOK&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual-cylinder hard interlock&lt;/strong&gt; must be pneumatic or electrical. The software interlock is &lt;em&gt;supplementary&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time parameters and sensor tuning&lt;/strong&gt; must be done on the real machine. The simulator can't give you real chatter or pneumatic delay.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It did the software part and handed the responsibility it can't carry back to a human. It didn't pretend to have "finished" something it hadn't.&lt;/p&gt;

&lt;p&gt;That's the property I'd want from any code generator touching industrial control: generate the verifiable part, and be explicit about what it can't verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The simulator passing does &lt;strong&gt;not&lt;/strong&gt; mean it's field-ready.&lt;/li&gt;
&lt;li&gt;I only checked that the code compiles, downloads, and matches the test cases at a logical level — I did not bench-test on real hardware.&lt;/li&gt;
&lt;li&gt;The agent's self-reported step log is in the full write-up; treat model self-reports with the usual skepticism.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Full write-up with screenshots, the complete source, and the agent's step log: &lt;a href="https://www.hubwiz.com/blog/ai-powered-plc-programming-with-workbuddy-and-tialink/?utm_source=devto&amp;amp;utm_medium=syndication" rel="noopener noreferrer"&gt;AI-powered PLC programming with WorkBuddy + TiaLink&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on TiaLink, the MCP bridge that drives TIA Portal, built by our team at 汇智网 (hubwiz.com). The agent used in this experiment was Tencent's WorkBuddy.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>automation</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
