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.
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.
How this was measured. 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.
What the gateway gives an agent
Five groups of capability, all against the project that is currently open in TIA Portal:
| Capability | What it does |
|---|---|
| Read | project tree, block contents, tag tables, UDTs, cross-references, HMI screens, hardware config |
| Write | generate or rewrite SCL, create blocks and tables, write back, compile one block or the whole PLC |
| Configure | add a CPU, plug modules, create subnets, wire IO systems, set PROFINET device names |
| Verify | download to S7-PLCSIM Advanced, read and write variables, run scenarios |
| Archive | export SimaticML for offline diffing and version control |
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.
Hooking it up
With Claude Code, it is two commands:
claude mcp add --transport http tialink http://192.168.1.110:8080/mcp
claude mcp list # confirm it connected, and how many tools it sees
Other clients differ only in where the config lives: Cursor (.cursor/mcp.json), Claude Desktop (claude_desktop_config.json), or a UI connector in WorkBuddy/CodeBuddy (remember to click Enable — an added-but-disabled connector shows no tools and looks broken).
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.
One requirement, one prompt
The requirement, as an engineer would write it:
- One start button starts motor 1; motor 1 running for 3 s starts motor 2 automatically.
- The stop button (normally closed) or a fault on either motor stops both immediately.
- Each motor has a run output; timing uses IEC timers.
- The logic lives in a reusable FB; OB1 calls it.
- I/O on M area:
M0.0start,M0.1stop (normally closed),M0.2fault,M0.3motor 1,M0.4motor 2.
The prompt we actually sent (verbatim):
【需求】两台电机的顺序启动控制
- 一台启动按钮启动 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 源码和实测数值
No SCL template, no step list — the sequence was the agent's choice.
What it produced
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
And OB1, wiring it to the M area:
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 => %M0.3,
Motor2 => %M0.4);
END_ORGANIZATION_BLOCK
Compile: state=Success, errorCount=0, warningCount=0, both Main (OB1) and FB_MotorSeq (FB1) cleaned.
Two details worth reading in that code: the start is edge-triggered (R_TRIG) — otherwise holding the button keeps it latched — and stop/fault runs through #statPermit, a path that never touches the timer. That second one is exactly what the third scenario below checks.
How long each step took
| Step | Time |
|---|---|
| health probe (attached, project, device count, prerequisites) | < 1 s |
| look up the CPU model in the hardware catalogue | ~1 s |
| create the CPU (CPU 1516-3 PN/DP, FW V2.9) | 2.2 s |
| import the FB from SCL source | 11.8 s |
| import the instance DB and OB1 | 0.6 s each |
| compile | 1.4 s |
| create a virtual PLC and power it on | ~6 s |
| download to S7-PLCSIM Advanced | 9.6 s |
| run, write variables, read variables | < 100 ms per call |
One ordering constraint you cannot skip: import does not compile. Right after import the block is not marked consistent, and even exporting it is refused (Block is inconsistent (not compiled)). Import → compile → read back, in that order.
Three scenarios
A green compile only says the code is acceptable to the PLC. These three runs say what it does.
| Scenario | Action | Observed | Read as |
|---|---|---|---|
| Start |
M0.1=1, pulse M0.0
|
motor 1 goes to 1 at once; motor 2 at +3.12 s; both hold after the button is released | delay is fed by "motor 1 running", not by the button |
| Stop | both running, M0.1 → 0
|
both drop within one sample; restoring M0.1=1 does not restart them |
the stop path is not gated by the timer |
| Fault | both running, M0.2 → 1
|
both drop within 13 ms (one scan); clearing the fault does not restart | fault is an independent cut-out, and it does not self-heal |
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.
Beyond "Success"
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 TON and no stray instance, and the block metadata says consistent. Sizes for the record: Main 7,766 B, FB_MotorCtrl 15,978 B (the earlier single-motor variant), DB_MotorCtrl 6,903 B.
The gateway's own notes make one behaviour explicit: nothing is persisted until you save. 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.
A checklist before you let it touch a real project
- Read-only first. Have it read the tree, blocks and cross-references; confirm it understood your project before it writes anything.
- Preview writes for a week. Compare its previewed diff against what a person would change.
- Read back after every write. Export or compare hashes; if it does not match, treat it as not written.
- One project, one session, one writer at a time. The Openness side is serial; parallel agents produce half-finished work.
- Compile as a gate. Passing does not mean correct, but failing must block the commit. This is the only gate you can fully automate.
- Archive before changing. VCI or Git, exported as diffable text.
- Keep an audit trail. Who, when, in which mode, which object.
- Do not hand download or go-online to an unattended agent. Man in the loop, one step at a time, with a rollback plan.
And the rule that outranks all of them: AI-generated SCL gets a human review, especially safety logic (emergency stop, interlocks, F-programs). The tool guarantees it was written, not that it is right.
Versions and boundaries
| Item | What we ran |
|---|---|
| TIA Portal | V19 / V20 supported; this run was V19 with Openness 19.0 |
| Simulator | S7-PLCSIM Advanced 6.0, Softbus |
| CPU firmware | matters more than expected — a V1.1 device was refused by the simulator, V2.9 worked |
| Deployment | the gateway runs on the same Windows machine as TIA Portal; the agent can be anywhere that can reach its HTTP port |
| Tool count | 34 |
The deployment line surprises people: the gateway has to be on the TIA machine, the agent does not. Every call in this article was made from a different computer over HTTP.
Run it yourself
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 http://localhost:8080/, and the client side is the one line of config from earlier.
Setup and version support: tia.hubwiz.com/tia-link. If you want the agent to understand the project before it starts writing, TIA-Context feeds it blocks, tag tables and call relationships.
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.
Further reading (in Chinese): AI changes a TIA Portal program: write guards and boundaries · OPC UA MCP hands-on · AI × industrial / PLC guide
Timings and error strings come from the 2026-10-27 run. AI-generated PLC code needs human review before it goes anywhere near production.

Top comments (0)