tia-harness
CLAUDE.md12 starsChanged 28 days agoMITView on GitHub
You are driving Siemens TIA Portal V21 through the tia_* tools (an MCP over the TIA Openness HTTP serve). This brief is ALWAYS in effect. For depth, call the tia_guide tool (topics: gates-guards, gui-gates, gotchas, pipeline, recipes, spec-format) — read the relevant topic BEFORE authoring blocks, changing hardware, or preparing a deploy. You are an assistant up to the gate, never an autonomous commissioner. - Class R — read-only; safe anytime. - Class M-off — offline mutation (edits the project;…
In this repository10 other files it gives its agents.
- codex/AGENTS.mdAGENTS.md
- cpu-selectSkill
- spec-planSkill
- spec-validateSkill
- tia-backupSkill
- tia-handoffSkill
- tia-initSkill
- tia-reviewSkill
- tia-scaffoldSkill
- tia-verifySkill
The file
Show its rules and text
We split this CLAUDE.md into the rules it gives an agent
17 rules. About 0 are common, 1 is distinctive, and 16 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
16
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 53
Prerequisites: run the serve (http://localhost:5000.
- Very specific: line 66
Before the first build on a project: read tia_guide {"topic":"pipeline"} and tia_guide {"topic":"gotchas"}.
- Very specific: line 96
Fresh 1214C communication settings: PUT/GET, OPC UA license and HR interface need a human source configuration.
- Very specific: line 154
One serial worker: never concurrent tia_* calls.
- Very specific: line 179
A V17 program does not upload into V21 (2026-10-31): use the TIA version the program was made with.
- Very specific: line 234
Use the PlcSoftware name from tia_plcs, not the device name.
- Very specific: line 250
Confirmation dialog: When the client supports elicitation, the crossing calls (tia_download with approved:true.
- Very specific: line 266
Before edits, use target, kind, filter:"ObjectsWithReferences".
- Very specific: line 280
Read first (tia_subnet_get, tia_device_network, tia_io_systems), then run each write's dryRun (answered by the serve, needs an open …
- Very specific: line 347
Hashes: keep sha256, normalizedSha256 and contentHash distinct.
- Very specific: line 469
Keep the same MLFB, swap only the /V<fw> suffix.
- Very specific: line 470
After ChangeType the CPU/blocks can be inconsistent.
- Distinctive: line 523
Use the manual sequence for a fresh CPU, or clone an owner-prepared station as described below.
- Very specific: line 552
Only for a WinCC Unified HMI (tia_hmis → kind:"unified").
- Very specific: line 564
No tool creates an HMI connection: when tia_hmi_connections is empty, PLC-bound tags cannot be created — ask the human to create the …
- Very specific: line 621
(c) Verify an export: Every row of every page must be equal.
- Very specific: line 625
(d) What a row says: Only equal is a match. After a human edits a workspace file its row should show workspaceChanged.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
<!-- GENERATED from mcp/content/ (run: cd mcp && npm run sync) — do not edit by hand -->TIA Openness MCP — operating brief
You are driving Siemens TIA Portal V21 through the tia_* tools (an MCP over the TIA
Openness HTTP serve). This brief is ALWAYS in effect. For depth, call the tia_guide
tool (topics: gates-guards, gui-gates, gotchas, pipeline, recipes,
spec-format) — read the relevant topic BEFORE authoring blocks, changing hardware,
or preparing a deploy. You are an assistant up to the gate, never an autonomous
commissioner.
Vocabulary (used across all tool descriptions)
- Class R — read-only; safe anytime.
- Class M-off — offline mutation (edits the project; never touches the physical PLC).
- Class GATE — touches the physical world or a human-only decision; needs a human.
- G1..G7 — the GATES: G1 trust dialog, G2 S7-connection precondition (tool-enforced: both CPUs must compile clean), G3 download / CPU-STOP, G4 go-online (incl. station upload), G5 safety / F-logic (NEVER autonomous), G6 credentials — master secret, UMAC, upload / block / certificate passwords, password policy (from a human — never invented), G7 network scan. All except G2 are human-only — never crossed autonomously.
- Gd* — tool-enforced GUARDS (preconditions the tools check), e.g. Gd5
OrderNumber:<MLFB>/V<fw>format (slash before V, never:V), Gd8 only ERRORs block a compile (warnings don't), Gd9 clean compile before deploy, Gd11 dryRun-then-approve.
Non-negotiable rules
- Serial only. The serve owns ONE TIA Portal on ONE serial worker. Never fire
concurrent
tia_*calls; one hung Openness call can stall the whole serve. - Never cross a GATE autonomously. A
requires_human_actionresponse is a HANDOFF, not an error: relay itsyouMustDo/nextStepsto the human verbatim, wait for their explicit yes, then resume exactly as instructed. Never self-approve a download; never invent a secret (G6); never author, modify or deploy safety/F logic (G5). - Long operations are jobs. Long tools (connect, compile, download, upload,
project create/save/archive, exports, CAx…) may answer
status:"running"with ajobId— the operation is STILL running on serve. Calltia_job_status {jobId}until it returns the final result; never repeat the original call. Onkind:"busy", polltia_job_statusonly for a running job whosejobIdis known (e.g.details.jobId).tia_pingprobes/health: never busy, no job ID — use it for reachability only. Without a known running job ID, wait/recheck withtia_status; forconnect_incomplete, follow the host-inspection handoff (tia_guide {"topic":"gui-gates"}), never retry connect. Offline tools (tia_guide,tia_spec_validate,tia_spec_plan,tia_job_status) never answer busy. - Compile before deploy.
tia_compileaggregates hardware + software and is the offline verifier:errors:0is the Gd9 precondition for download (G3) and for S7 connections. - Follow the build pipeline order (details:
tia_guide {"topic":"pipeline"}): validate the spec (tia_spec_validate) → plan (tia_spec_plan) → scaffold (tia_spec_applyor the M-off tools) → verify (tia_compile) → human-gated deploy (tia_downloadwith dryRun → human approval →approved:true+ the sameplanHash). - Fresh CPUs may need GUI setup (PUT/GET, OPC UA license/interface); a prepared station can
inherit these via
tia_device_from_mastercopy. Readtia_guide {"topic":"gui-gates"}. Missing settings are human handoffs, not retryable failures. - Prerequisites: run the serve (
http://localhost:5000;tia_ping). First connect per process may need G1; running/timeout alone are not G1 evidence. - Confirmation dialogs. Where the client supports it, a gated call first asks the human in the client's dialog
(elicitation); only the human answers it, and it never replaces dryRun → approval →
planHash.declined_by_human/cancelled_by_human= refused, nothing sent: stop, no retry unless asked. - Size budget and paging. A result holding
{"omitted":true,…}had that value cut out WHOLE by the MCP's size budget: repeating the same call returns the same marker, so follow itshintinstead. Read large lists (tia_plc_blocks,tia_plc_tags,tia_device_tree) in pages: passlimit, then eachnextCursorascursoruntil it isnull. A page with a marker has NOnextCursor(it would skip the omitted data): retry the same request with a smallerlimit.cursor_stalemeans the project changed: restart from the first page (nocursor). Gate handoffs, plans and job envelopes are never cut. For XML too big for one result usetia_export_xml(it writes files on the serve host).
Session start: tia_ping → tia_status → (if needed) tia_start / tia_connect.
Before the first build on a project: read tia_guide {"topic":"pipeline"} and
tia_guide {"topic":"gotchas"}.
Tool names are written bare (tia_status) here and in the skills. Claude Code shows them qualified:
mcp__plugin_tia-harness_tia__tia_status (plugin; what a subagent tools: list needs) or
mcp__tia__tia_status (standalone). Other clients may display them differently; the tia_* suffix
is the tool.
Gates vs guards
GATES vs GUARDS
- GATES (human-only — never auto-bypass): trust dialog (G1), download / CPU-STOP
(G3), go-online (G4, which includes a station upload (
tia_upload): it goes online to read the CPU), network scan / accessible-devices (G7), master-secret/UMAC (G6, which includes a CPU read-access password for an upload, a block know-how password (tia_block_protect), a certificate file's password (tia_certificate_import) and the password-policy values (tia_password_policy_set)), safety / F-logic (G5) — never autonomous. - GUARDS (tool-enforced preconditions): compile-clean before deploy, consistency,
dry-run→approve, idempotency by name (advisory: re-runs do not converge),
OrderNumber:<MLFB>/V<fw>format, serial-only. - Back up a physical CPU with the
tia-backupskill (dedicated empty project; the upload token is bound to the project shown in the handoff). - Picking the G3 download path (no gate, no scan):
tia_pg_interfaceslists the connection modes/PG-PC interfaces TIA already sees for the CPU (plus the OS status of each adapter) — use it to fillconnectionMode/pcInterfaceName/targetInterfaceNamefortia_downloadinstead of reaching fortia_accessible_devices(a real network scan, GATE G7). - Fresh 1214C communication settings: PUT/GET, OPC UA license and HR interface need a
human source configuration;
tia_device_from_mastercopycan inherit it into a new station. Server activation is scriptable:tia_obj_set_attributes {handle, attributes:{OpcUaServer:true}}. Details and certificate recovery:tia_guide {"topic":"gui-gates"}. Never script the GUI.
Deploy-time approval gates (download / online / network-scan / secret / safety = G3/G4/G7/G6/G5)
are a different class — see the tia-handoff skill. Those are "a human approves an action",
not "a human clicks a GUI setting". Both are human interaction points; surface them as
requires_human_action with concrete guidance, never as a tool failure.
GUI gates — expected human interaction points
Human interaction points (GUI gates) — EXPECTED, not an error
Expected handoffs include trust, fresh-CPU setup and firmware-change recovery. On the validated 1214C V4.4, no direct setter for PUT/GET, license selection or HR mapping was verified; a prepared station master copy inherits all three. Do not blindly retry a missing setting: give the human the exact steps.
| # | Signal you'll see | Why it's expected | Tell the human (exact GUI steps) |
| G1 trust | EngineeringSecurityException or a failed start with details.cause:"trust" → requires_human_action / trust_dialog. Neither running nor the 20-minute client timeout (STARTING_UNCONFIRMED) proves G1 or success. | First attach may prompt per serve process. | Human on the serve host: click "Yes to all" only on a visible trust dialog. While a job runs, poll tia_job_status; do not queue calls. A timeout does not cancel it: inspect the host, wait for completion before status/retry. After explicit G1 handoff, retry with resumeToken. For TRUST_DIALOG_DENIED_OR_EXPIRED, inspect closedPids/stillRunningPids/unknownPids: close only verified remaining TIA started by serve with no open project, then retry with a human ready. |
| OPC UA license | tia_compile ERROR: "The selected OPC UA license is not sufficient…" | No verified direct setter on this 1214C; a fresh project or firmware change can leave it unset | CPU → Properties → General → Runtime licenses → OPC UA → required "SIMATIC OPC UA S7-1200 Basic"; purchased → the license the owner declares for this CPU. Recompile. |
| OPC UA interface | OPC UA client connects but the DB nodes aren't there (ns=4 empty / no ServerInterfaces) | The validated S7-1200 requires a configured server interface to expose DBs | CPU → OPC UA communication → Server interfaces → Add new server interface → type Interface → drop the HR array (ModbusData.HR), not the DB or individual elements. Recompile. |
| PUT/GET | S7 client connects on port 102 but reads return access-denied | No verified direct setter on this 1214C | CPU → Properties → Protection & Security → Connection mechanisms → check "Permit access with PUT/GET communication from remote partner". Compile + human-gated download (hardware:true). |
| FW-change reset | After tia_device_change_version: OPC UA server off, the license error on compile, blocks isConsistent:false | DeviceItem.ChangeType resets GUI-gated comm settings (server activation + license); the interface & PUT/GET survive | tia_project_archive before the FW change. After: re-set OpcUaServer:true via tia_obj_set_attributes (scriptable), re-select the license (row above), recompile. |
Bounded connect: connect_incomplete / CONNECT_UNCONFIRMED (serve_host_inspection)
names the last phase, not G1 or success. No cancellation or TIA kill: don't repeat or queue work.
Human: inspect the host and answer pending Siemens dialogs. If none is pending and unresponsive,
save stdout (CLI) or %APPDATA%\TIAOpenness\startup.log (tray, overwritten on launch),
then restart the process (tray: Exit/relaunch, not API Stop/Start).
TIA may remain running: inspect it and verify project state before reconnecting.
A finished MCP job carrying this handoff isn't connection proof.
Prepared station: tia_library_mastercopy_create {device}, then tia_device_from_mastercopy {masterCopy}
clones all three settings and HR mappings. Not an existing-CPU setter or license grant.
Certificates are not copied: on the NEW "OPC UA" item, OpcUaServer:false then true regenerated one; compile afterwards.
GUI type Interface maps to SimaticInterface (Export, no Import).
The separate ServerInterface.Import did not reproduce its HR mappings.
1214C/V21 bench, 2026-10-26: HR[0..2] IDs in ns=4: HR array → 13,14,15;
individual elements → 12,23,34; whole DB → 14,15,16.
Selection/order affects IDs; probe after construction/import.
Hard-won gotchas (runtime-validated)
Hard-won gotchas
- Always compile before deploy.
tia_compileaggregates hardware + software; a clean compile (errors:0) is the precondition for download and for S7 connections.tia_downloadcompiles too and leaves the project modified (projectModifiedin plan/result): save (tia_project_save) before switching projects; Openness does not say what changed. First download over another project's or TIA version's program: planallowUpgradeTargetDevice:true, or a refusal may leave the CPU stopped (the plan cannot predict it). - One serial worker: never concurrent
tia_*calls; a hung operation may need a serve restart. - FB edits: software-scope
tia_compile; export regenerated DBs (blockChanges). Still inconsistent:tia_block_delete+tia_instancedb_create, recompile. Global DB source put re-creates it:IsOnlyStoredInLoadMemoryresets (no source support; pragma ignored); nested starts needBEGIN. UDT put/tia_types_importdid not reset DB values. Save XML/text/flags. InspectchangeImpact.dbAttributeChanges/readGaps(unknown;trackChanges:falseopts out). Restore flag=true:tia_block_set_attribute, compile; starts: saved XML/text, not attributes. N warnings then 0: hardware compile produced OPC UA guest/unsecured + missing protection-level warnings; next skipped hardware. Checkmessages[]"Hardware configuration". - Firmware rule: project FW ≤ device FW. Bump the project FW in-place with
tia_device_change_version(DeviceItem.ChangeType), which preserves the program — never set the project higher than the physical CPU. ⚠️ It RESETS GUI-gated comm settings (the OPC UA server activation + runtime-license; the server interface and PUT/GET survive): archive first (tia_project_archive) on a CPU with hand-configured OPC UA, recompile after (it marks blocks inconsistent), and re-verify. Same-versionChangeTypeis rejected. - Use a NON-optimized DB for Modbus (
MB_HOLD_REG) and S7 (absolute addressing); OPC UA works with optimized or not. - Order numbers use
/V<fw>, not:V<fw>(guard Gd5). - A station upload (
tia_upload) ADDS a new station to the open project — back up into a dedicated EMPTY project. Bench (S7-1200 1214C, 2026-09-26): 0 → 1 device in ~75 s, twice, with the CPU in RUN throughout. Protect the working project FIRST:tia_project_createcloses it and is refused (409 unsaved_changes) while it has unsaved changes, sotia_project_save+tia_project_archivefirst (neverdiscardUnsavedChangesto get past the refusal), with the archivenameWITH the.zap21extension (absent or extension-less fails). A V17 program does not upload into V21 (2026-10-31): use the TIA version the program was made with.upload_incompatible_versionis definite: check versions andstationAddedindetails; a partial station is not a backup. Approval stays single-use. - One wrong know-how password can lock a block for the session. Observed on the bench
(2026-10-12): after one rejected unprotect (
tia_block_protect {protect:false}), TIA refused the CORRECT password of that block for the rest of the session (cause not confirmed — possibly brute-force protection). Never retry with a guess — ask the human for the password (G6); if a password the human confirms is refused, stop and hand off instead of retrying. unsaved_changesandbinding_conflictare refusals, not faults: nothing was changed.tia_connect,tia_project_close,tia_disconnectandtia_project_createanswer409 unsaved_changeswhen the call would close a project with unsaved changes (the error lists each one). ASK the human; the recommended answer is to repeat the same call withsaveUnsavedChanges:true. PassdiscardUnsavedChanges:true(data loss) ONLY when the human explicitly decides to throw the work away, never to make the error go away (the client then asks the human to confirm in its own dialog;declined_by_human= nothing was sent, stop).409 binding_conflictmeans the project this session is bound to is no longer the one open in the bound TIA (closed, replaced or Save As done by hand;tia_statusshowsboundProjectPathandbindingDrift): ask the human which project is intended, thentia_connectwith the bound path (back to it) or the open one (adopt it) — do not repeat the refused call first.503 audit_unavailable: the serve could not write its audit log, so it refused the call BEFORE running (nothing reached the PLC). Have the human fix the disk or permissions, then re-plan. AnauditWarningin a response = the call ran but was not fully recorded: report it.
Prerequisites (environment)
- The
tiaserve must be running (defaulthttp://localhost:5000, e.g.TIAOpenness.exe serve 5000). The MCP is a pure HTTP client; point it at the serve host viaTIA_API_BASEif it runs elsewhere. - The Windows user must belong to the "Siemens TIA Openness" local group.
- A first connection per serve process may show a trust dialog; a human confirms it on the serve host. A slow start or timeout alone is not G1 evidence.
The build pipeline (spec → verified project)
TIA version preflight: tia_guide {"topic":"recipes"}, 'TIA version evidence and SimaticML', before authoring.
| # | Stage | Tool(s) | Class | What it does |
| 0 | Validate the spec | tia_spec_validate | R (pure) | Partial shape checks + the cross-field rules the tool implements: unique names, IP inside the subnet CIDR, Gd5 order-number format, no STL/GRAPH on S7-1200, the G5 safety rule, at most 8 SMs on an S7-1200. It does NOT run the JSON Schema or per-model module limits; the spec-validate skill covers both. Needs no serve. |
| 1 | Plan | tia_spec_plan | R (pure) | Turns a VALID spec into the ordered list of M-off operations. Nothing mutates. |
| 2 | Scaffold | tia_spec_apply (or the M-off tools manually) | M-off | Creates part of the project content in dependency order (scope below). Stops at every gate. |
| 3 | Verify | tia_compile | M-off | THE offline verifier: aggregates hardware + software. Gd8: only ERRORs block. errors:0 is the Gd9 precondition for any deploy. |
| 4 | Handoff / deploy | tia_download | GATE G3 | dryRun:true verifies compile, connection and software fingerprint → planHash → HUMAN approval → approved:true + SAME single-use planHash. Serve re-checks software (plan_stale if changed). Approval binds connection/options and covered software, NOT hardware: humans must confirm hardware has not changed since that dryRun; changed hardware needs a NEW plan and approval, never the old token. Fingerprint covers user blocks, UDTs and tag tables only; exclusions and source:"dates" fallback: tia_guide {"topic":"recipes"}, 'Reference — download plan fingerprint limits'. Never self-approve; resolve connection via tia_pg_interfaces (no scan). |
For the first download over another project's or another TIA version's program, plan with
allowUpgradeTargetDevice:true; without it, a refusal may leave the CPU stopped.
The offline plan cannot predict whether TIA will request that confirmation.
For the first download over another project's or another TIA version's program, plan with
allowUpgradeTargetDevice:true; without it, a refusal may leave the CPU stopped.
The offline plan cannot predict whether TIA will request that confirmation.
Scaffold order (UDTs BEFORE the blocks that use them): by hand, tia_guide {"topic":"recipes"}, 'Pipeline order'. tia_spec_apply automates only subnets, devices, modules, network, tag tables, tags, FB shells, control-module instance DBs and the compile; protection, constants, UDTs and all logic stay manual. The FB shells it creates are empty: add their logic with tia_source_put (SCL) or tia_import_xml (LAD/FBD).
Use the PlcSoftware name from tia_plcs, not the device name.
The approval envelope (what a gate handoff looks like)
Gated tools return status:"requires_human_action" with:
gate— which gate (e.g.download_approval,station_upload,go_online,trust_dialog);youMustDo— what the human must confirm, in plain words;plan+planHash— exactly what would be executed (the hash binds the approval to this exact plan; a changed plan needs a new approval); the download's plan also carriesfingerprint(the software content the plan was verified against — show it too);nextSteps— how to resume after the human's yes.
Relay the envelope to the human, wait for an explicit yes, resume exactly as instructed. The AI never approves, never invents a secret, never deploys F-logic.
Confirmation dialog. When the client supports elicitation, the crossing calls (tia_download with
approved:true; tia_upload, tia_online, tia_accessible_devices with confirm:true) first ask the
human in a dialog; clients without it behave as before. The dialog only checks the human's answer: the
planHash and serve's approval token stay the authority.
Passwords (G6) are never asked there. declined_by_human / cancelled_by_human = the human refused:
nothing was sent, no retry unless asked. elicitation_failed (isError) = the dialog could not be
shown or answered, nothing was sent (fails closed).
After scaffolding
- Re-run
tia_compileafter ANY authoring change. tia_project_saveregularly;tia_project_archiveBEFORE risky operations (firmware change — seetia_guide {"topic":"gotchas"}).- Auditing an existing project is class R (
tia_plc_*reads); baseline:tia_guide {"topic":"recipes"}, 'Reference — network, audit and CAx how-to'. tia_crossrefs(R, Long; polltia_job_statusforjobId):UnusedObjectsreturns CANDIDATES, not proof (OBs, external-only uses and library TYPEs can look unused). Before edits, usetarget,kind,filter:"ObjectsWithReferences"; the human interprets TIA'sreferenceType(Uses/UsedBy) andaccess. Page containers/whole PLCs withlimit+ unchangednextCursoruntil null. App 1.8+ caches whole-PLC walks until mutation/close/cursor expiry; older serves re-read pages. Read coverage on every page, not only the first: a later page can report less than an earlier one. Oncursor_stale, restart from the first page; differing counts restart the walk once from the first page. On omission retry the same request with smallerlimit, never its cursor. Claim complete/no uses ONLY after a finished walk with identical counts, no omissions andcoverage.complete:trueon EVERY page; else NOT complete (inspectcoverage.unavailableOwners).- FAT/SAT I/O list (
tia-verifystep 6):tia_io_map; recipe:tia_guide {"topic":"recipes"}, 'Recipe — generate the FAT/SAT I/O list'. - Prove the project matches what's versioned (a raw hash is NOT proof):
tia_guide {"topic":"recipes"}, 'Recipe — prove the project matches the versioned XML'.
Network lifecycle (subnets, PROFINET names, IO systems)
Read first (tia_subnet_get, tia_device_network, tia_io_systems), then run each write's dryRun (answered by
the serve, needs an open project). Order, PROFINET names, disconnectNodes: tia_guide {"topic":"recipes"}, 'Reference — network, audit and CAx how-to'.
- Refused, nothing changed:
tia_subnet_deleteof a non-empty subnet (409 subnet_not_empty);tia_io_system_connectacross subnets (409 cross_subnet_assignment:tia_network_connectthe device to that subnet first). 409 postcheck_failed: the write did not take effect as asked: re-read, never repeat blindly.
Libraries, HMI, CAx and PLC security
Reads are class R; every change is class M-off (offline, with dryRun) and reaches the CPU
only through a later tia_compile + human-gated tia_download (G3). Every file path is a
path ON THE SERVE HOST.
- Guarded PLC writes (
tia_tag_update,tia_constant_update,tia_constant_delete,tia_tagtable_delete,tia_block_group_delete,tia_ob_create,tia_block_compile): the serve answers theirdryRun(it needs an open project) with the guard verdict (name collision, default tag table, non-empty group, singleton OB). - Project library:
tia_library_mastercopies/tia_library_typesread it;tia_library_mastercopy_createturns a DEVICE into a master copy (a block cannot be a source);tia_device_from_mastercopyinherits PUT/GET, OPC UA license selection and HR mappings into a NEW station; regenerate its certificate;tia_block_from_mastercopyneeds one that holds PLC blocks. - Global libraries (in the TIA Portal session, not the project):
tia_global_libraries,tia_global_library_create/_open/_save/_close; save/close take the name fromtia_global_libraries(exact name first, then ignoring case). REFUSED, nothing saved or closed (404 not_found/409 conflict): a name that picks no single open USER library (unknown, read-only system/corporate, several matches ignoring case) and a save of areadWrite:falselibrary. Up to app 1.6.3 an unknown name can report success without saving or closing anything: checktia_global_libraries. - HMI:
tia_hmisandtia_hmi_screensread any HMI; screen files (tia_hmi_screen_export,tia_hmi_screens_import) work for WinCC Classic only, and the import OVERWRITES same-named screens. - CAx (AutomationML):
tia_cax_export/tia_cax_import(scope,optionvalues: recipes). Archive the project before an overwrite. A failed transfer is an error after app 1.6.3; on an older serve check the devices withtia_devices. - Error contracts by app version:
tia_guide {"topic":"recipes"}, 'Reference — global library and CAx error contracts (by app version)'. - PLC security:
tia_certificates/tia_certificate_template/tia_password_policyread;tia_certificate_import/_export/_delete,tia_password_policy_setandtia_block_protectchange the project. G6: a block or certificate password and the password-policy values come from a HUMAN — never invent, guess or reuse one; dryRun plans show a password as[redacted]. A CPU without the certificate manager or a policy service answers 409. - Multiuser:
tia_project_servers,tia_server_projectsread Project Servers;tia_local_session_openneeds NO open project (tia_project_close,tia_start). Nothing is ever checked in.
TIA version evidence, verified recipes & scaffold order
TIA version evidence and SimaticML
Delivery boundary: App 1.8.0 retains the V21 baseline and ships no legacy layers. The following minVersion notes are the lowest API major observed in the inspected V17–V21 references, not a legacy runtime-support promise or a universal introduction release. V16 has no measured gap matrix. Host version, PublicAPI version and CPU firmware are different values.
Before authoring, read selected host/API diagnostics when available. An installed compatibility DLL is not another installed host; if the target is unknown, ask rather than assume V21. Detection is not compatibility. Never bypass a version refusal through generic object calls, substitute delete/recreate or silently upgrade/downgrade.
| Operation or field | Observed API minVersion | Evidence limit |
Free-text tia_catalog, tia_crossrefs, tia_device_change_version, password-policy services; tia_library_types' LibraryType.Namespace field | 18 | V17 gap; V18–V20 uses bind. CPU/project service availability and legacy runtime behavior unverified. The library minimum is field-level, not a minimum for every library read. |
| Unified tags/alarms/screens/connections/validation | 18 | V17 gap; V18 lacks alarm TriggerBitAddress and TriggerAddress; those fields bind from V19. This is not a reason to reject all V18 Unified reads. |
tia_accessible_devices, current certificate-export overload and CAx structured-result API | 19 | V17/V18 gap or signature adaptation; legacy execution unverified. Scan still requires human G7. |
| Current VCI mapped-object/status/export model; multiuser groups and marking mutations | 20 | V17–V19 gaps; two-argument VCI workspace creation already binds from V18. Older read surfaces are not equivalent to current mutations. |
| S7 connection authoring; generic self-description type/support fields | 21 | Gaps across inspected V17–V20; V21 is the reference baseline. |
| Other tools and SimaticML import compatibility | null | No cross-version minimum/import matrix established; a V21 recipe does not establish legacy support. |
These notes derive from the V17 and V18–V20 spikes (ADR 0041), which did not test cross-version LAD/FBD/SCL/UDT imports. An API that binds can still refuse a CPU/project; no silent fallback or invented data.
LAD/FBD/GRAPH authoring: start from a real export made for the selected target Portal/API and preserve its <Engineering version="...">, interface, instruction and FlgNet/StructuredText namespaces. Do not copy a V21 template to a legacy target by changing version strings or namespaces. Import UDTs before dependent blocks; compile, export and compare on that same target. A matching marker is not proof of import compatibility. tia_block_render is review-only, never a version converter.
Import diagnostics: V21 keeps its current import behavior, including Openness diagnostics; no new V21 version rejection. Legacy-only unsupported-version refusals must come from the authoritative target boundary before importing, for both inline XML and server-local path. Relay 409 feature_unsupported_in_version and its operation/host/API/minimum/reason details when returned; do not infer a compatibility matrix from a namespace or claim all older XML is importable.
Hashes: keep sha256, normalizedSha256 and contentHash distinct. Normalization does not erase Engineering versions or schema namespaces and is not down-conversion. Keep a fresh contentHash from the same target/object for guarded writes; never reuse a V21 hash as a legacy baseline.
Live check (#213): collect real V17 and V20 LAD/FBD/SCL/UDT exports at version acceptance, recording the host, loaded API, installed update and export options, then retain them as fixtures beside the V21 originals. Exercise import, compile, re-export and normalized comparison on each accepted target; record refusals and unknown combinations explicitly. V16 and V18/V19 also need their accepted evidence; V18/V19 acceptance is scoped to their APIs on the V20 host. Current fixtures prove V21 behavior only; synthetic marker edits are not real legacy exports.
Pipeline order (dependency-correct)
tia_spec_apply automates only steps 1-4, the tag tables and tags of 6, the FB shells and control-module instance DBs of 8, and the compile of 9. Protection (5), constants (6), UDTs (7), OB/FC/DB blocks and all block logic stay manual: declaring dataTypes in the spec imports nothing. The FB shells of 8 are empty: their logic is step 8 by hand.
- Subnets (
tia_subnet_create) — before connecting devices to them. - Devices (
tia_device_create,OrderNumber:<MLFB>/V<fw>) — CPUs first. - Modules (
tia_device_plug) — into existing devices, by slot. - Network (
tia_network_connect, IP,createIoSystem) — CPU + its IO-devices on the same subnet; exactly onecreateIoSystemper IO-system. - Protection recipe (below) — so the CPU can compile clean.
- Tag tables (
tia_tagtable_create) → tags (tia_tag_create) → constants. - UDTs first (
tia_types_import), then blocks that use them. - Blocks — the FB comes with its logic:
tia_source_put(SCL) ortia_import_xml(LAD/FBD/GRAPH), Gd6 format routing (tia_fb_createonly makes an empty FB shell); then its instance DB (tia_instancedb_create). - Compile (
tia_compile) — the verifier; onlyErrorblocks. It compiles the software scope (not just the CPU device item), so it makes freshly-generated blocks consistent and, in most cases, regenerates any instance DB left inconsistent by an FB interface change. Recompile first; only if the DB still reports inconsistent after a compile, delete it (tia_block_delete), recreate it (tia_instancedb_create) and recompile.
The PlcSoftware name. TIA names the PlcSoftware of a CPU created through tia_device_create or tia_spec_apply after the CPU device item (default PLC_1), not the device, so plc in steps 6-9 is the name from tia_plcs. The spec plan carries {plc}; tia_spec_apply resolves it after the device step (plc.resolve in applied) and stops with kind plc_unresolved rather than guess. devices[].plcName names the CPU item.
Recipe — edit an existing block or type without overwriting a newer change (stale-write guard)
Read it with tia_export_block (block) or tia_export_type / tia_type_source (type) and keep its contentHash: contentHash, normalizedSha256 and sha256 are different hash contracts over different normalizations, so never substitute one for another and pass only the contentHash of a fresh read as expectedContentHash. Edit, then write it back with tia_import_xml, tia_source_put or tia_types_import passing expectedContentHash:<that contentHash> (one object per write); on state_changed NOTHING was written — re-read, merge your edit onto the current content and retry with the new contentHash; a guarded write never compiles, so a block that is not consistent is refused with 409 inconsistent (NOTHING written) — tia_compile, re-read with tia_export_block for the current contentHash, retry; an object whose current export is not well-formed XML has no hash (its read answers contentHashError instead of contentHash) and a guarded write over it is refused with 409 unverifiable (NOTHING written) — repair or re-export it in TIA Portal, or retry WITHOUT expectedContentHash (an unguarded write); a verified write returns contentGuard.
Recipe — unlock a freshly-created CPU so it compiles clean (VERIFIED V21, firmware-dependent)
A bare CPU added via Openness may fail compile with protection errors: the
confidential-PLC-configuration-data password is unset, the communication certificate
can't be generated without it, and (on newer firmware) the access control defaults to a
protected level needing a password. The recipe differs by CPU family/firmware — there is
no single universal form. Fix, in order:
Tell the human first. The recipe lowers the CPU's protection in the project: it sets the confidential-configuration password (typed by the human) and then either sets the access level to Full access (S7-1500) or turns access control off (S7-1200, FW V4.5+). The CPU runs with that protection after the next download. Name these changes when you ask for the password and list them in the report; the human may pick a stricter level in the TIA GUI before deploying.
(1) Confidential-data password → also auto-generates the certificate (common to both families):
# find the CPU device-item handle:
tia_obj_roots -> the device handle (role:"device")
tia_obj_items { handle:<device>, composition:"DeviceItems" } -> the CPU item handle
# acquire the service ON THE CPU MODULE (not the rack) and invoke Protect:
tia_obj_service { handle:<cpu>, type:"Siemens.Engineering.HW.Features.PlcMasterSecretConfigurator" } -> <svc>
tia_obj_invoke { handle:<svc>, name:"Protect", args:["<password>"] }(2) Clear the access-control error — pick the variant for the CPU's family/firmware:
S7-1500 V2.9 (verified 2026 on real hardware — a 1516-3 V2.9, 3 errors → 0): set
PlcProtectionAccessLevel = "FullAccess"on the CPU device item:tia_obj_set_attributes { handle:<cpu>, attributes:{ "PlcProtectionAccessLevel":"FullAccess" } }S7-1200 V4.7 (new security model, FW ≳ V4.5; per
docs/HARNESS-KNOWLEDGE.md§2, runtime-observed): setPlcAccessControlConfiguration = "Disabled"on thePlcAccessControlConfigurationProviderSERVICE — not a direct attribute on the CPU item:tia_obj_service { handle:<cpu>, type:"Siemens.Engineering.HW.Features.PlcAccessControlConfigurationProvider" } -> <svc2> tia_obj_set_attributes { handle:<svc2>, attributes:{ "PlcAccessControlConfiguration":"Disabled" } }⚠️ The two variants are mutually exclusive — the wrong one returns HTTP 500 on the other family:
PlcAccessControlConfigurationisn't on a 1500's CPU item, andPlcProtectionAccessLevelisn't the right model on a 1200 V4.7 item. Detect the family/FW — or the compile-error signature — before picking a variant.- S7-1200 V4.0 (old security model): nothing to do here — the same bare-CPU condition
is only a WARNING, not an error; a plain
tia_compilealready returnserrors: 0.
Then tia_compile → state: Warning, errors: 0 (any remaining warnings — e.g. CPU-display
password — are benign and do not block; Gd8).
Requires the
tia_obj_servicetool (acquireGetService<T>→ handle) for step 1 and for the S7-1200 V4.7 variant of step 2. Without it the MCP cannot run either.
Policy — who supplies the confidential password
Protect() sets an offline project password (it is not the online master secret / G6), but
it protects confidential configuration data, so the human supplies it, every time (the
recommended policy, pending the product owner's confirmation): the project-spec has no field
for it, and a skill never invents, defaults, stores or logs it. The online master secret (tia_mastersecret_set, G6) is always
human-supplied too.
S7 connection precondition (G2)
tia_connection_create needs ≥ 2 CONSISTENT CPUs (both compile clean). Two bare CPUs
never form a valid S7 connection — apply the protection recipe + tia_compile to make each
consistent first, then create the connection. Use tia_connections_diagnose on failure.
Recipe — bump the project firmware in-place (keeps the program) (VERIFIED, V21 / S7-1200 1214C)
Bumped a project V4.0 → V4.4 on a real S7-1200 1214C without losing blocks or tags
(verified 2026-06-07). Use this for auto-FW-match: align the project to the firmware
actually read from the CPU. DeviceItem.ChangeType(typeIdentifier) changes the item type
in place and preserves the software — it does not require delete/recreate.
# archive first (see the warning below), then change the CPU's type to the SAME MLFB with the target /V<fw>:
tia_device_change_version { plc:"<plc>", typeIdentifier:"OrderNumber:6ES7 214-1HG40-0XB0/V4.4" }
# then recompile — a regenerated FB may need a fresh compile before download:
tia_compile { ... }(tia_device_change_version runs ChangeType on the CPU item for you; the raw form, tia_obj_invoke
with name:"ChangeType" on the CPU item handle from tia_obj_roots / tia_obj_items, does the same.)
- Rule:
project FW ≤ device FW— never set the project firmware higher than the physical device's (download would be rejected). Bumping the project up to the device FW is the safe move. - Keep the same MLFB, swap only the
/V<fw>suffix. The program (blocks/tags) survives. - After
ChangeTypethe CPU/blocks can be inconsistent; always recompile before any handoff. - ⚠️
ChangeTypeRESETS GUI-gated comm settings (runtime-validated on the 1214C): it disabled the OPC UA server (OpcUaServer→false) and cleared the OPC UA runtime-license selection (the custom server interface and PUT/GET survived). Sotia_project_archivefirst on a CPU with hand-configured OPC UA/PUT-GET, and re-verify after. Recovery:tia_obj_set_attributes {handle, attributes:{OpcUaServer:true}}on the "OPC UA" device-item (it is writable; re-select the license in the GUI).ChangeTypeto the same version is rejected ("Cannot change the type of the device item").
Recipe — enable a Modbus TCP server (MB_SERVER) on the S7-1200 (VERIFIED, FW V4.0)
Lets an external client (e.g. a Node backend) read/write the CPU over Modbus TCP, port 502
(verified 2026-06-07; full backend/app detail in docs/COMM-SCENARIOS.md).
- Non-optimized DB with an
Array of Wordfor the holding-register block (MB_HOLD_REG). Modbus needs a byte-addressable (non-optimized) DB — optimized access won't map. - On FW V4.0 use
MB_SERVERV1.x, which takesCONNECT_ID+IP_PORTparameters (notCONNECT/TCON, which the newer instruction versions use). Pin the instruction version when generating the FB. - Instantiate
MB_SERVERas a multi-instance inside an FB (its instance lives in the FB's instance DB), call it cyclically. - Map a holding register → a command tag (e.g.
MB_HOLD_REG[0]→LedCmd), so writing the register drives the output.
Instance-DB gotcha (the big one): when you regenerate the FB via
tia_source_put, the existing instance DB can go inconsistent (its interface no longer matches the FB). Recompile first — a software-scopetia_compileregenerates the instance DB in most cases. Only if it still reports inconsistent after a compile (typical when the interface was edited via source), delete + recreate it, then recompile:tia_compile { ... } # try first tia_block_delete { ... the stale instance DB ... } # only if still inconsistent tia_instancedb_create { ... fresh instance DB for the FB ... } tia_compile { ... }(Matches the §Pipeline note: recompile first, delete+recreate only as the fallback.)
Recipe — enable S7 / PUT-GET on the S7-1200 (port 102)
Lets an external S7 client (e.g. nodes7) read/write the CPU over S7comm, port 102.
- For a fresh CPU, ask the human to enable PUT/GET: Properties → Protection & Security →
Connection mechanisms → "Permit access with PUT/GET communication". No direct setter was
verified on the 1214C V4.4. Alternatively,
tia_device_from_mastercopyinherits this permission from an owner-prepared station; do not script the GUI. - Non-optimized DB for the data the client touches (PUT/GET addresses absolute offsets, so the DB must be byte-addressable / non-optimized).
- Compile + download as usual; the client then reads/writes the DB by area+offset.
Recipe — enable OPC UA on the S7-1200 (port 4840)
Lets an external OPC UA client read/write the CPU over OPC UA, port 4840. Use the manual sequence for a fresh CPU, or clone an owner-prepared station as described below.
Prerequisite — firmware ≥ V4.4. OPC UA server isn't available on V4.0. If the project is on
an older FW, bump it in-place first (see bump the project firmware in-place above —
ChangeType to .../V4.4).
For a fresh CPU, step 1 is scriptable; steps 2–3 require human configuration:
- Activate the OPC UA server — Properties → OPC UA → Server (enable). (This step is
scriptable via Openness:
tia_obj_set_attributes {handle, attributes:{OpcUaServer:true}}on the "OPC UA" device-item — it auto-generates the server certificate.) - Select the runtime license — "SIMATIC OPC UA S7-1200 Basic" (Runtime licenses).
- Create a server interface (type Interface) and drop the
ModbusData.HRarray, not the DB or separate elements. The S7-1200 does not auto-expose DBs. Browse the resulting node IDs.
Finding the node IDs (client side): browse ns=3;s=ServerInterfaces with any OPC UA client;
the actual data nodes show up under ns=4. Use those node IDs from the backend.
Prepared-station path (V21 / 1214C V4.4): capture the configured source with
tia_library_mastercopy_create, then tia_device_from_mastercopy {masterCopy}. This creates a new
station, preserving PUT/GET, selected runtime license and the HR interface; it is not an existing-CPU
setter or a license grant. TIA may rename the new PLC. Certificates are not copied: inspect its
"OPC UA" item; OpcUaServer:false then true regenerated the certificate in the measured clone.
Compile, save/reopen and check the exported HR mappings before any human-gated download.
The GUI type Interface is exposed as SimaticInterface (Export/Delete, no Import);
ServerInterface.Import did not reproduce its HR mapping. See tia_guide {"topic":"gui-gates"}.
Recipe — WinCC Unified HMI: read, then guarded writes
Only for a WinCC Unified HMI (tia_hmis → kind:"unified"). A Classic HMI answers 409 hmi_not_unified; use the screen export/import there. Everything below is offline (M-off): it edits the project, never the panel or the PLC.
- Read first.
tia_hmisfor the HMI name, thentia_hmi_tag_tables,tia_hmi_tags(table),tia_hmi_alarm_classes,tia_hmi_alarms(kind:discrete|analog) andtia_hmi_connections. Every read pages (limit1..1000,cursor): follownextCursorUNCHANGED, and oncursor_stalerestart from the first page. ReadinventoryGapstoo: a non-empty list means a value readsnullbecause Openness refused it, so do not write on the strength of that value. - Baseline.
tia_hmi_validate(scopeall, ortags/alarms/connections/screens) aggregates the per-objectValidate()results intoerrors,warningsandfindings. Unified has NO compile: a clean validate is not a clean compile, and it can be slow (Long). dryRun:trueon EVERY write first (tia_hmi_tag_table_create|delete,tia_hmi_tag_create|update|delete,tia_hmi_alarm_create|update|delete). The plan runs the same guards as the real call and changes nothing; repeat withoutdryRunonly when it answersplan_ok. The real answer carriespostcheck(verified:true,requestedequal toactual).409 postcheck_failedis NOT a success: read the object again before going on (rollbacksays what was undone; a delete is never rolled back).- What the guards enforce — fix the cause, do not retry the same call. Names: letters, digits and
_, starting with a letter or_(tag tables may also hold spaces and-inside), at most 128 characters (invalid_name), unique ignoring case (name_collision).dataTypeis an allow-list: Bool, Byte, Char, DInt, DWord, Int, LInt, LReal, LWord, Real, SInt, String, UDInt, UInt, ULInt, USInt, Word, WChar, WString, DateTime, Time (invalid_data_type). A tag withoutconnectionis internal and needsdataType; withconnectionit is external and takes exactly one ofplcTagoraddress. A PLC-bound tag needs aplcTagthat EXISTS in the connected PLC (plcNamewhen the connection's partner does not resolve to one PLC):plc_unresolved/reference_not_foundmean fix the link, never invent a tag. A PLC tag whose own name contains a dot or a bracket is sent as ONE quoted token ("A.B","Valve[1]"): unquoted,A.Bis memberBof data blockAandValve[1]is an indexed name, so the call is refused. A discrete alarm needs an existingalarmClassandtriggerTagand atriggerBitinside the width of the tag's type (Bool: 0, Word: 0..15;trigger_bit_out_of_range); an analog alarm has ONEconditionand ONEconditionValueon a numeric tag (no limit ordering exists), and the value must fit the type (condition_value_out_of_range). - Alarm values, event text and what a read shows. TIA stores the condition value of an alarm as a double: on an
LInt,ULIntorLWordtrigger tag a wholeconditionValueabove 2^53 in absolute value is refused409 condition_value_out_of_range(adryRuntoo, nothing written); 2^53 itself is accepted, sent as a string, andRealandLRealvalues are always JSON numbers.eventTextis PLAIN single-paragraph text per active project language: the serve wraps it as the rich text TIA stores (<body><p>…</p></body>) and escapes&,<and>, so send no markup; a line break, an XML-invalid character (a tab is allowed), an unpaired surrogate, a blank text or more than 1024 characters is400 bad_request, and a text cannot be cleared by a write. A read answers the plain text (an event-text item without text is"", a language without an itemnull; any other markup comes back as raw rich text, which cannot be written back). TIA's placeholders read asnull: an internal tag hasconnection,plcName,plcTag,address,accessMode,acquisitionModeandacquisitionCycleallnull(TIA reads its connection back as a placeholder such as<Internal tag>), and an alarm class without a common class hascommonAlarmClassnull(<No alarm class>). The attribute list of an alarm itself is never read:hmi_attribute_list_unavailableandhmi_attribute_not_writableapply to tags and to the event text item of each culture (its list unreadable,Textnot writable, or no item for that language), never to another alarm attribute. - Deleting. A tag, or a tag table that holds one, is refused with
tag_in_usewhile an alarm references it; a non-empty table needsrecursive:true(tag_table_not_empty); the default table is refused. References from screens, scripts and logging are NOT scanned, so runtia_hmi_validateagain after a delete. - Finish with
tia_hmi_validateand compareerrorsandfindingswith the baseline.
Limits to remember: a create's dryRun cannot check writability (a plan_ok can still end in hmi_attribute_not_writable, after which the new object is removed again); an update checks only what it changes (a tag change against the stored bit or value, a connection change against its link), never a reference it leaves alone, and never switches a tag between plcTag and address — tia_hmi_validate finds a dangling carried reference; send a whole conditionValue as a JSON integer or a string (5.0 and 1e3 are refused, and so is a whole value above 2^53 on an LInt, ULInt or LWord tag: step 5); a system tag may be an alarm trigger tag; tia_hmi_validate is Long, so it may answer status:"running" with a jobId: follow tia_job_status and never repeat the call.
No tool creates an HMI connection: when tia_hmi_connections is empty, PLC-bound tags cannot be created — ask the human to create the connection in TIA Portal.
Recipe — diff a project against a VCI workspace, or round-trip a block through a new one
A VCI workspace is a folder on the serve host that mirrors project objects as files, so a diff tool or git can version
them. VCI takes leaf objects only (PLC blocks, PLC data types, tag tables): a device, a PLC, a group or the library is
refused with 409 vci_not_leaf before VCI is called. Every write takes dryRun:true first (Gd11), a call holds at most 50
objects, and a batch is NOT a transaction: it stops at the first failure.
(1) A workspace that already has mapped objects is a baseline: its diff is READ-ONLY.
tia_vci_workspaces -> `mappedObjects` above 0: a baseline exists
tia_vci_status { workspace:"WS_1", limit:100 } -> follow nextCursor until it is nullClassify every row. equal is the only verified match. projectChanged (the project object changed), workspaceChanged
(the file changed), conflict (both), workspaceFileMissing (the file is gone) and unequal (a difference of unknown
side) are differences: report each by name, they are the result, not something to repair. unknown, unsupported and
statusFailed prove nothing, neither equality nor a diff: report them by name, and the diff is NOT complete. Never
export or import just to make a comparison equal. tia_vci_export over a mapped object REPLACES its workspace file
with the project content (the plan says action:"synchronize") and tia_vci_import replaces the project object with
the file, so either one destroys the difference you were asked to see. Refreshing the baseline is an explicit decision
of the human: name the objects, ask, and only then export with dryRun:true first and read which files the plan
replaces.
(2) A NEW workspace, or new objects: export, then prove it.
(a) tia_vci_workspaces -> reuse a workspace that is listed
# none yet: ask the HUMAN for the root folder on the serve host. Never choose or invent a rootPath.
tia_vci_workspace_create { name:"WS_1", rootPath:"<the folder the human gave>", dryRun:true }
tia_vci_workspace_create { name:"WS_1", rootPath:"<the same folder>" } -> created | reused
(b) tia_vci_export { workspace:"WS_1", objects:[{ kind:"block", name:"FB_Motor" }], dryRun:true }
tia_vci_export { workspace:"WS_1", objects:[{ kind:"block", name:"FB_Motor" }] } -> the file(s) written
(c) tia_vci_status { workspace:"WS_1", limit:100 } -> follow nextCursor until it is null
(d) tia_export_block { plc:"PLC_1", block:"FB_Motor" } -> contentHash
tia_block_set_attribute { plc:"PLC_1", block:"FB_Motor", name:"HeaderAuthor", value:"<a new text>" } -> an edit IN PLACE: the mapping survives
tia_block_compile { plc:"PLC_1", block:"FB_Motor" }
tia_export_block { plc:"PLC_1", block:"FB_Motor" } -> a DIFFERENT contentHash
tia_vci_status { workspace:"WS_1", limit:100 } -> that row is now projectChanged
(e) tia_vci_import { workspace:"WS_1", objects:[{ kind:"block", name:"FB_Motor" }], dryRun:true }
tia_export_block { plc:"PLC_1", block:"FB_Motor" } -> a fresh contentHash
tia_vci_import { workspace:"WS_1", objects:[{ kind:"block", name:"FB_Motor", expectedContentHash:"<fresh>" }] }
tia_export_block { plc:"PLC_1", block:"FB_Motor" } -> the import REPLACED the project object: read it again by name
tia_vci_status { workspace:"WS_1", limit:100 } -> that row is equal again- (b) Read the plan.
action:"export"creates a new file;willCreateDirectory:trueon an object means the serve will create its target folder (the first export into a fresh workspace has none yet), and the dry run creates nothing. An object the plan lists assynchronizeis already mapped and its file would be REPLACED: stop and go back to (1).formatchooses the file format of an UNMAPPED object only (leave it out unless the serve answers400 bad_requestwithdetails.supportedFormats); a mapped object keeps its mapping's format and the selector is neither applied nor validated for it. - (b) A
skipis not a write. An object the plan lists asskip(reason:"already_equal") is already mapped and its status isequal: TIA refuses to synchronize an equal mapping, so the serve writes nothing for it and the answer lists it asskip. - (c) Verify an export. Every row of every page must be
equal. Readcoverageon EVERY page:summaryandcoveragedescribe only the rows of that response (the serve compares only the rows of the page), so fold them over all pages before saying the workspace is in sync.409 cursor_stale(a mapping appeared or went) restarts the walk once. - (d) Edit the project IN PLACE, never by regenerating. A block attribute (
tia_block_set_attribute, a header attribute such asHeaderAuthor) keeps the project object, so its mapping stays and the row becomesprojectChanged(measured on V21). Iftia_export_blockshows the samecontentHashafterwards, that attribute is not part of the exported XML: try another header attribute (HeaderFamily,HeaderName).tia_source_put(regenerating the block from SCL) REPLACES the project object, and V21 DROPS its VCI mapping (measured): the row vanishes fromtia_vci_statusandtia_vci_importanswers409 not_mapped, which is the correct answer, not a fault;tia_import_xmlover an existing block replaces it the same way (not measured separately). Export the new object again to map it:tia_vci_exportis refused409 vci_path_collision(reason:"file_exists") while the old workspace file is still in the folder, so ask the human to remove or move that file first; on a baseline you were asked to diff, report the dropped mapping instead of exporting over it. - (d) What a row says. Only
equalis a match. After a human edits a workspace file its row should showworkspaceChanged; a row that still saysequalafter a known edit means the status is not live (V21 has no refresh call): say so and treat the check as NOT verified. - (e) Read the plan before importing, and read the hash per kind. The dry run shows per object
statusBeforeandoverwritesProjectChanges(trueforprojectChangedandconflict: the import discards an edit that exists only in the project, there is no undo, so ask the human). An object that is alreadyequalis skipped. The hash reader depends on the kind: a block usestia_export_block, a type usestia_export_type { plc:"PLC_1", type:"UDT_Motor" }, and a tag table has NO hash (the serve and the MCP refuseexpectedContentHashfor it), so rely on the dry run and the postcheck. A wrongexpectedContentHashis409 state_changedand nothing was written. - (e) A refused file, a changed attribute. A workspace file that TIA refuses (an SD parser error, a corrupted resource
file) is
400 bad_requestwith TIA's text and nothing changed: fix the file, never retry it unchanged. A postcheck whoseattributeChangeslists theSecondaryTypeorNumberof an OB, orIsOnlyStoredInLoadMemory,IsWriteProtectedInASorDBAccessibleFromOPCUAof a data block, means TIA changed the object while importing it (a SIMATIC SD import does): the object WAS written, so report the before and after and do not call the import done. - An import REPLACES the project object. After a real import the old object is disposed: a handle of it (
tia_obj_*) and every value read before are stale, so re-read the block by name (tia_export_block,tia_plc_blocks) and thentia_vci_status. The serve does the same for its postcheck: an object it cannot find again by name, or whose mapping the replacement dropped, is409 import_unverified(the object WAS written). In a batch, an object that can no longer be found or mapped when its turn comes stops the batch there:409 vci_batch_incompletenaming it, or409 conflictwhen nothing was imported yet; nothing more is written. - Verified only with a postcheck. An import is done only when every imported object has
postcheck.verified:true. Every other answer means it is NOT verified:409 import_unverified(the object WAS written, review it withtia_export_blockortia_export_type),409 vci_batch_incomplete(donelists the objects written and the skipped ones,action: skip, nothing written for them), a status rowunsupported,statusFailedorunknown,coverage.complete:false, an omission marker that survives smallerlimitretries, or a page walk that did not finish. - Unknown outcomes.
504 worker_restarted, atimeoutor a lost session during an export or an import leaves the outcome UNKNOWN or partial: walktia_vci_statusand re-read the object before repeating anything. - Refusals before any write (nothing changed):
vci_path_collision(two objects, another mapping or an existing file share a target;reason:"alias_unverifiable"means the short 8.3 name of an existing file in the target folder could not be read, so a collision with its alias cannot be excluded),vci_path_too_long,vci_mapping_outside_workspacefor an EXISTING mapping rejected in the plan,not_mapped(export it first),workspace_file_missing,workspace_exists,workspace_root_in_use,workspace_ambiguous(ask the human which workspace is meant) andvci_inventory_incomplete(the workspace inventory could not be read completely: report it and stop, never guess a workspace). vci_mapping_outside_workspacehas TWO outcomes. An existing mapping rejected in the plan changed nothing. After a NEW export the same kind means the file WAS written outside the workspace root: read its message anddetails(object,directory,root,done), report it, hand the cleanup to the human and never retry blindly.
Reference — global library and CAx error contracts (by app version)
Project library (tia_block_from_mastercopy). A device copy is refused (409 mastercopy_not_blocks after app 1.6.3; up to 1.6.3 a 500).
Global libraries (tia_global_library_save / _close). After app 1.6.3 a name that picks no single open USER library is refused, nothing saved or closed: 404 not_found (details.userLibraries lists the names that work), or 409 conflict for a read-only system/corporate library or a name several user libraries match only ignoring case; up to 1.6.3 the serve reports success even for an unknown name. A library opened read-only (readWrite:false) cannot be saved: 409 conflict (details.readOnly) after app 1.6.3, a 500 up to 1.6.3 — close it and open it again with readWrite:true to change and save it.
CAx (AutomationML) (tia_cax_export / tia_cax_import). A failed transfer is an error (500 cax_transfer_failed) after app 1.6.3; up to 1.6.3 the serve returns an opaque result even on failure — check the devices with tia_devices.
Reference — download plan fingerprint limits
Fingerprint limits (tia_download): user program blocks, UDTs and tag tables only — no hardware configuration, system block groups, S7-1500 software units or technology objects; per-object fallback to modification dates (source:"dates", warned in the plan — dates can false-positive plan_stale).
Recipe — generate the FAT/SAT I/O list (tia_io_map)
Generate the FAT/SAT I/O list (tia-verify step 6). tia_io_map (jobId → tia_job_status), one
list per PLC, limit:100, cursor = previous nextCursor unchanged until null. Read coverage on every
page; cursor_stale or differing coverage restarts once; omission marker: same call and cursor, half the
limit, max 3 times, never follow its cursor. Rows: one per tag on a channel, or on a range with
status matched; per unmatched tag entry; and (empty tag) per unmatched channel or channelless
range; none for a range with status channels; a tag spanning channels: ONE row (table+name)
listing them; statuses matched|unmatched|conflict only. Unresolved channels/ranges and unparsed
tags: "Could not be mapped". Print coverage numbers, coverage.limits, DERIVED addresses (spot-check in
TIA). Complete ONLY after a finished walk with no marker left on ANY page and coverage.complete: true,
same counts, on every page; else start "NOT complete", say why. An unmatched tag is never
"unused"/"orphan"; never invent, round or fix an address. Read-only; the human signs.
Lengths: a range's start is a byte number and its length is in BYTES (Openness reports an address length in bits and the map converts it, rounding up to whole bytes); from and to are its first and last bit (2 bytes from %I0.0 end at %I1.7). tia_device_tree shows the raw Openness value, in BITS: convert before comparing the two.
Recipe — prove the project matches the versioned XML (tia_block_compare)
The project matches what's versioned — prove it, don't assume it. A raw hash of an
export is NOT proof (two exports of the same block differ in <Created>/UIds). Use
tia_export_block to pull a block inline and tia_block_compare to diff it against the
committed XML (normalized — ignores formatting/BOM/<Created>; UIds are canonically renumbered,
preserving wiring); verdict:"equal" is
the actual round-trip proof. After tia_compile/tia_source_put, read blockChanges and
export/commit any regenerated instance DBs before calling the project verified.
Comparison hashes (normalizedSha256) from older MCP builds that stripped UIds may change:
recompute stored comparison baselines.
Recipe — SIMATIC SD text: read, edit, create (tia_export_sd, tia_import_sd)
tia_export_sd returns one block or type as .s7dcl code text and .s7res texts, with the same contentHash
as tia_export_block/tia_export_type; tia_import_sd creates or replaces ONE object from them (the only SD route
for an object that does not exist yet; VCI updates mapped objects only). Run dryRun:true first and pass the fresh
contentHash as expectedContentHash. Rules: an existing OB of any event class is refused (sd_existing_ob_unsupported: TIA
gives every OB imported from SD a new number, a ProgramCycle OB 123 came back as OB 1): change it with tia_export_block +
tia_import_xml, which keep its number and event class (a NEW OB can be created from SD); a global DB loses IsOnlyStoredInLoadMemory,
IsWriteProtectedInAS and DBAccessibleFromOPCUA: read changeImpact.dbAttributeChanges, nothing is restored for
you (IsOnlyStoredInLoadMemory and IsWriteProtectedInAS can be restored with tia_block_set_attribute; a person sets
DBAccessibleFromOPCUA again in TIA Portal, the block's properties, when that route refuses it: on V21 2100.0.121.1 its
400 details.writable omits it on an optimized global DB; then compile); comments only through .s7res (// inside a
LAD/FBD network is refused; quote YAML values with : or #);
FBD: a wire# consumer rung before its producer, every box output pin listed (an empty et => is fine); TIA
re-quotes some identifiers on export, so compare ignoring quotes. The object comes back inconsistent: compile it.
Reference — network, audit and CAx how-to
Network lifecycle. tia_subnet_delete on a non-empty subnet is refused (409 subnet_not_empty); disconnectNodes:true disconnects its nodes and removes its IO systems. A subnet rename (tia_subnet_update name) takes at most 24 characters: a longer name is 400 invalid_subnet_name (a dryRun too, nothing changed) although tia_subnet_create accepts longer names; a name equal to the current one is not a rename, and the serve counts UTF-16 code units, spaces included. PROFINET name (tia_pn_name_set): lower-case DNS labels (a-z0-9-) split by ., not an IP look-alike. A node in its default state (automatic name generation on, PnDeviceName read-only) takes a name: the serve switches auto-generation off first and writes the name second, and its dryRun cannot prove that the name becomes writable, so a plan_ok can still be followed by 409 pn_name_unsupported from the real call; the serve then writes the previous auto-generation back and verifies it, the error ends with "Nothing was changed." only when that read-back confirmed it, and otherwise 409 conflict names autoGeneration as NOT restored: re-read with tia_device_network. IO system first: tia_io_system_create on the controller, then tia_io_system_connect per device; 409 cross_subnet_assignment: connect the device to that subnet (tia_network_connect) first.
Audit (class R). Read blocks/types/tags via tia_plc_* and check the engineering baseline: optimized access, 100% symbolic, UDT-typed interfaces, FB + instance-DB, uniform naming. Scope tia_crossrefs with target/kind on a big project: a whole-PLC read can take minutes.
CAx (AutomationML). tia_cax_export exports one device or the whole project; tia_cax_import takes option: RetainTiaDevice (default) / OverwriteTiaDevice / MoveToParkingLot.
Where to stop
Everything above is offline (M-off). tia_download / tia_online (G3/G4) are not
part of scaffold — they belong to tia-handoff and require explicit human approval.
The project-spec (input model)
A project-spec is ONE JSON document describing the whole project. Always run it
through the pipeline front door: tia_spec_validate (rung 0, pure) → tia_spec_plan
(dry) → tia_spec_apply (M-off, stops at gates).
tia_spec_validate checks the shape only in part (it does not run the JSON Schema; the spec-validate
skill adds it) and enforces: unique device/subnet/UDT/block/per-table tag names; every network node IP
inside the subnet CIDR; OrderNumber:<MLFB>/V<fw> with /V — never :V (Gd5); no STL/GRAPH on an
S7-1200; at most 8 SMs on an S7-1200 (its only module limit); and G5 — a spec with safety content
marked autonomous is REJECTED (F-logic is never autonomous).
Minimal example:
{
"project": { "name": "Line2", "directory": "C:\\TIA\\Line2" },
"conventions": {
"namingScheme": "plcopen",
"optimizedAccess": true,
"symbolicOnly": true,
"defaultLanguages": { "interlocks": "LAD", "algorithms": "SCL", "sequencers": "GRAPH" }
},
"devices": [
{
"name": "Line2_CPU",
"typeIdentifier": "OrderNumber:6ES7 511-1AK02-0AB0/V2.9",
"role": "cpu",
"family": "S7-1500"
}
],
"network": {
"subnet": { "type": "System:Subnet.Ethernet", "name": "PN_Line2", "cidr": "192.168.0.0/24" },
"nodes": [{ "device": "Line2_CPU", "ip": "192.168.0.1", "createIoSystem": true }]
},
"dataTypes": [
{
"name": "UDT_Motor",
"members": [
{ "name": "xStart", "dataType": "Bool", "comment": "Start command" },
{ "name": "rSpeed", "dataType": "Real", "comment": "Speed [%]" }
]
}
],
"tagTables": [{ "name": "IO", "purpose": "IO" }],
"tags": [{ "table": "IO", "name": "Conv01_Run", "dataType": "Bool", "address": "%Q0.0" }]
}The plan and tia_spec_apply neither import dataTypes nor author block logic: UDTs go in with
tia_types_import, logic with tia_source_put / tia_import_xml.
Resolve every concrete MLFB + firmware via tia_catalog before building — never guess
an order number. Project firmware must be ≤ the physical device's firmware.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

