cd /news/artificial-intelligence/ai-plc-programming-the-missing-layer… Β· home β€Ί topics β€Ί artificial-intelligence β€Ί article
[ARTICLE Β· art-163881] src=ainexusdaily.vercel.app β†— pub= topic=artificial-intelligence verified=true sentiment=Β· neutral

AI + PLC Programming: The Missing Layer Is Project Context, Not Model Quality

An analysis of an MIT-licensed open-source TIA Portal V19 project, PLC_SCADA_HMI (rassulz/multirobot_painting_control_system) on an S7-1500 station with a CPU 1513F-1 PN, found that AI agents answering PLC programming questions fail because of missing project context rather than model quality. Building a graph of the project took 1.2 seconds and produced 227 nodes and 517 edges β€” 220 containment, 2 calls, 2 instantiations, 87 reads, 52 writes and 154 wires β€” plus 8 unresolved references (7 "pending", 1 "external"), a relationship layer more than twice the size of the object layer. The analysis argues that pasting exports, grepping, or letting an agent browse silently loses cross-block coupling, direction and freshness, and that a graph needs provenance labels on every edge so an agent can distinguish a parsed claim from an inference.

read12 min views9 publishedNov 4, 2026
AI + PLC Programming: The Missing Layer Is Project Context, Not Model Quality
Image: Ainexusdaily (auto-discovered)

Ask an AI agent "who reads and who writes additional_variables.tag3?" in a real TIA Portal project, and you can tell within one reply whether it is guessing. A grounded answer is four rows: two contacts reading it at lines 152 and 222, two coils writing it at lines 217 and 262, all inside Auto_mode,

network 1. An ungrounded answer is a paragraph that sounds right. That gap β€” four rows versus a paragraph β€” is not a model-quality problem. Current models write SCL fine.

It is a project-context problem: a PLC project is a graph, and we keep handing the model a stack of files. Approach Answers well Silently loses Paste the export into the prompt "What does this network do?" Anything over the window; all cross-block coupling once you truncate Global search / grep "Where does this string appear?" Direction (read vs. write, call vs. instance), and the absence of an expected relation Let the agent browse and remember Short sessions, one block at a time Freshness β€” it may be reasoning over what it read 40 turns ago, and it will not say so Chunking is the worst of the three, because the relationships you most need are exactly the ones that cross the chunk boundary.

In a ladder program that boundary is everywhere: a coil in network N3 writes a tag that a contact in network N7 reads, and the tag itself is defined in a tag table that is a third file. The graph we used has four kinds of content, and you need all four: Part What's in it What it answers Nodes Blocks, networks, interface members, tags, LAD parts (contacts/coils/boxes), SCL statements "What is in this project?" Edges Call, instantiate, type, read, write, wire "Who calls whom, who writes this tag?" Provenance A label on every edge: parsed from a call / parsed from an operand / structural containment / tag-table membership / wire "Is this claim evidence, or an inference?" Unresolved references Names that are referenced but defined nowhere in the project; deliberately not forced into an edge "What is missing?" β€” the most valuable list before a handover or a migration That third row is the part people skip. In a graph built from a real project, "tag3 is written by a coil in Auto_mode" and "tag3 is written by a coil in Auto_mode, per an operand parse, line 217" are different claims with different trust levels. If your agent cannot separate them, you are back to prose.

Target: PLC_SCADA_HMI, an MIT-licensed open-source project (rassulz/multirobot_painting_control_system), opened in TIA Portal V19 on an S7-1500 / ET200MP station, CPU 1513F-1 PN (6ES7 513-1FL02-0AB0, firmware V2.9). Read-only the whole way: no imports, no block edits, no save, project dirty flag unchanged. Metric Measured Build time, from scratch 1.2 s (one sentence to the agent; no further project access after that) Nodes 227 Edges 517 β€” containment 220, calls 2, instantiation 2, reads 87, writes 52, wires 154 Unresolved references 8 (7 "pending", 1 "external") Project size OB1 (4 networks), 2 FBs, 2 instance DBs, 2 tag tables (51 tags), 0 UDTs, 0 duplicate names 1.2 seconds is not the interesting number. The interesting number is 517 edges against 227 nodes β€” the relationship layer is twice the size of the object layer.

That ratio is the reason a context window is the wrong container: you don't have a document of 227 things, you have a network of 517 claims about them. Any graph-derived answer is only as good as its freshness, and the failure here is silent. Before asking your first real question, get three facts: Which project is the graph attached to? (should be the one you're analyzing β€” here, PLC_SCADA_HMI) When was it built?

If you edited a block after the build, the graph is stale and nothing will tell you. Was it built from the currently open project, or is it a leftover graph from last week? A stale graph looks completely healthy. It just describes a project that no longer exists.

If any of the three is unclear: rebuild, then ask. Two related traps: an empty graph gets rejected outright (the previous graph is kept, so you may be looking at yesterday's), and a graph query that hits its result cap says so β€” "there's more" β€” which means you have not seen the project, you have seen a page of it. The single most repeated mistake in this kind of analysis is batching six questions into one prompt, getting a long answer, and not noticing that section four was truncated. Six probes, six turns, each verifiable on its own: # Probe Question What a good answer looks like 1 Inventory What's in this project: blocks (type/number/language/network count), instance DBs, tag tables, UDTs, CPU and order number?

List duplicates separately, write 0 where empty A table that reconciles against the TIA project tree, with explicit zeros 2 Find Find Auto_mode (or CV-101, or Conveyor), widening exact β†’ prefix β†’ comment word β†’ contains; tell me which tier matched and whether results were truncated The match tier named, plus "there is more" if there is 3 Dissect Open OB1::Main once: interface, each network, what it calls, read/write list, unresolved refs inside the block; say which part was truncated One block, one page, complete 4 Relations Who reads and who writes additional_variables.tag3? Reads and writes separately, each with block + network + line + provenance Rows, not sentences 5 Impact If I change or delete FB::Auto_mode, what is affected? By depth, and through which relation? Depth-numbered hits 6 Gaps List every unresolved reference with file and line, mark pending/external, classify truly-missing / external-library / to-be-added; do not guess A table you can act on Probe 2 is worth its own paragraph.

On this project, Auto_mode matched four things β€” the FB, a call site Main::N1::I1, its file, and its network β€” and the answer came back with "there is more". Without the "was it truncated" question you would have concluded "four references" and been wrong. Ask about truncation every single time. It costs one sentence and it is the difference between an inventory and a sample.

Probe 4, verbatim result for additional_variables.tag3: Relation Source Line Provenance member of tag table additional_variables β€” tag-table containment read Auto_mode::N1::L13 (contact) 152 operand parse write Auto_mode::N1::L26 (coil) 217 operand parse read Auto_mode::N1::L27 (contact) 222 operand parse write Auto_mode::N1::L35 (coil) 262 operand parse Two things to notice. First, reads and writes are separate columns of truth: 2 reads + 2 writes means a change to this tag touches four ladder positions, all between lines 152 and 262 β€” that is the entire software blast radius of the change, stated as a list of places. "This tag is used in the auto mode block" is not that; it cannot be checked and it cannot be assigned. Second, the line numbers make the claim auditable.

Not "trust me, the graph says so" β€” open the XML, go to line 217, look at the coil. If an audit finds the graph wrong at line 217, you now know something about the graph for every subsequent answer. This is the whole reason to prefer a graph over a summary: it gives reviewers a way to be lazy safely. For FB::Auto_mode, the default relation set (call / instantiate / read / write) gives: Depth Through Hit 1 call call site Main::N1::I1 1 instantiation instance DB InstanceDB::Block_1_DB Which reads as one honest sentence: Auto_mode is called from network 1 of the main program and its instance DB is Block_1_DB; change the interface and both need a look.

But "which block does it live inside?" is not in that answer, because containment is not part of the default set. Add containment and walk up: Depth Through Hit 1 containment its source file 2 containment the network it sits in (Main::N1) 3 containment OB1::Main 4 containment the main program's source file These are two different questions, and conflating them is the fastest way to a wrong edit plan: the first says who is affected (callers, instance DBs, tag readers), the second says where it is nested (file β†’ network β†’ block β†’ file). Keep them as two probes. Eight references in this project point at objects the project does not define.

The graph does not invent edges for them; it puts them in a table: Status Reference (written) File Line pending IEC_Timer_0_DB_3 blocks/Auto_mode.xml 332 pending IEC_Timer_0_DB_4 blocks/Auto_mode.xml 337 pending IEC_Timer_0_DB_1 blocks/Auto_mode.xml 483 pending IEC_Timer_0_DB_2 blocks/Auto_mode.xml 488 external IEC_Timer_0_DB blocks/Auto_mode.xml 493 pending IEC_Timer_0_DB_3 blocks/Manu_mode.xml 232 pending IEC_Timer_0_DB_4 blocks/Manu_mode.xml 237 pending IEC_Timer_0_DB_2 blocks/Manu_mode.xml 323 Cross-check: all eight are timer coils writing an instance β€” and both of these FBs have empty interfaces, and no global DB with those names exists. So this is not a parser miss. The blocks reference timer instance DB names that were never created in this project. A search finds the name; only a project-wide graph can tell you the name resolves to nothing.

Then the compile: No block was compiled. All blocks are up-to-date. Read that sentence carefully before you put it in a report. It does not mean "the program is fine." It means "nothing needed compiling." A green compile is evidence about the compiler's queue, not about the program β€” and it is exactly how a dangling timer instance survives to commissioning.

The last step of the probe is the one only a human can do: classify each row as truly missing (add the object), external library (this project cannot define it; import something), or to be added later. The agent assembles the evidence; the judgment stays with the engineer who owns the machine. It reports what is, never what should be. An empty FB interface and two empty OB networks are facts; whether that is intentional design or a half-finished migration is a conversation with whoever built it.

It knows nothing about wiring, channels, or hardware. A tag addressed to %I29.4 is a string in a table until someone checks the terminal strip. It does not replace a compile, a simulation, or a safety review. Those prove different things, and one never substitutes for another.

Unresolved β‰  broken code. "Pending" often just means the object never entered this copy of the project. Pick one project you inherited, not one you wrote. The point is to be surprised.

Build the graph, then immediately confirm the three checks (project name, build time, current-project origin). Write the build time into your notes β€” you will need it to know when the graph went stale. Inventory probe. Reconcile against the TIA project tree by hand, once.

If they disagree, stop; the graph is not trustworthy yet. Pick one tag that matters β€” an output enable, an interlock, a handshake bit β€” and ask probe 4. Read the four (or forty) rows. Sample two or three line numbers back into the XML or the TIA editor. Verify. This is the step everyone skips and the only one that calibrates trust. Ask probe 5 twice: default relations, then with containment. Ask probe 6, then classify every row yourself.

Put the classification in the report, not in your head. Expect the surprises to come from steps 4 and 6. Impact analysis on a block you "know" is where the containment layers show you the file you forgot, and the tag probes routinely find a writer in a block nobody wanted to touch. The industry conversation about AI and PLC programming is mostly about generation β€” better SCL, better ladder, better conversions.

That is the part that got solved first, because it only needs the spec. The part that did not get solved is context, and prompt engineering cannot fix it, for a structural reason: a graph does not fit in a sequence. You can always paste more text, but pasting more text makes the coupling you need less visible, not more, because the relationships are spread across the very boundaries you are introducing. Three things are worth taking from this even if you never build a graph: Ask for provenance on every claim.

"Parsed from an operand, line 217" versus "the block seems to use this tag" β€” one is evidence, one is a sentence. AI-assisted engineering fails quietly when the two are formatted the same way. Proofread for truncation, not for tone. Long outputs from agents are persuasive at the paragraph level and unreliable at the inventory level.

The question "did you see all of it?" should be muscle memory. Treat absence as a finding. The eight dangling timer references are worth more than the 87 read edges, because a compile will never tell you about them and a human search will only confirm the name appears somewhere. Missing relations are only visible against a complete model of the project.

The lesson this came from is Lesson 6 of PLC AI in Practice β€” a free, text-based course (no video) that walks the same project through six probes with copy-paste prompts, ~30 minutes. The tool behind it is TiaLink, an MCP gateway that connects TIA Portal to an agent and runs locally, so project data stays in the plant. If you'd rather start from the human problem than the tooling: Taking over a legacy PLC program: a 4-step forensics routine (in Chinese) β€” what to do in the first 48 hours, before any analysis; AI PLC programming: 7 hands-on lessons, each with a verifiable gate (in Chinese) β€” how this lesson fits the series; Write protection in TIA Openness MCP tools (in Chinese) β€” why "read-only" has to be designed, not promised. Disclosure: every measured number above β€” 227 nodes, 517 edges (220/2/2/87/52/154), 8 unresolved references, 1.2 s build, lines 152–262, the IEC_Timer_0_DB rows β€” comes from the course's own published run on 2026-10-27 (TIA Portal V19, TiaLink 1.4.0.0).

I did not re-run the measurement independently for this post. Prompt examples were written in Chinese in the lesson and are translated here.*

Key Takeaways #

  • β€’An ungrounded answer is a paragraph that sounds right.
  • β€’This story was reported by Dev.to , covering developments in thedev space.
  • β€’AI advancements continue to reshape industries β€” read the full article on Dev.to for complete coverage.

πŸ“– Continue reading the full article:

Read Full Article on Dev.to β†’

── more in #artificial-intelligence 4 stories Β· sorted by recency
── more on @plc_scada_hmi 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/ai-plc-programming-t…] indexed:0 read:12min 2026-11-04 Β· β€”