TIATool

Self-extension: teaching TIATool what it cannot do yet

TIATool ships with a fixed set of tools, but TIA Portal Openness can do far more than any fixed set. Self-extension closes that gap: when no tool does what you need, the AI explores the live Openness object model, writes a small C# script, runs it read-only, runs it for real only after you approve it, verifies the result, saves the script as a reusable extension and writes down what it learned. The next session finds the extension and the lesson.

The aim is "if Openness can do it, TIATool can do it — safely". Every change still goes through the same rails as the built-in write tools: policy, licence, a person's approval, backup, transaction, journal and the verify gate.

Status. The built-in extensions are compiled on every build against our Openness reference stub, but they stay unverified until the live selftest (tiax selftest --live) has run on your machine: step 21 proves that a read script rolls back and a write script commits, step 22 compiles every built-in extension against your real Siemens.Engineering assemblies and runs the read-only ones on a scratch project. Treat their first result on your TIA version with healthy suspicion.

The loop

Step Tool What happens
1. Reuse extension_list, extension_show Search the library first: ~40 live extensions and 10 offline ones ship built in.
2. Explore openness_browse Walk the object model of the open project: attributes, compositions and actions with their exact names and types.
3. Script script_check Compile the C# script; errors come back with line numbers.
4. Read run script_run mode=read Runs inside an Openness transaction that is never committed and never saved.
5. Write run script_run mode=write Only after a person approves it (see below): backup, transaction, commit only on success, save, journal.
6. Verify export_project → refresh_scan → verify_change The post-write pipeline proves exactly the intended change happened.
7. Promote extension_save The working script becomes an extension with a description, parameters and provenance.
8. Learn knowledge_add Every pitfall met on the way is recorded with the verbatim error, cause and fix.

Questions that the scanned cache can answer never need TIA Portal at all: use cache_query (JavaScript over the .tiax cache) instead of a live script.

Clients that support MCP prompts can start the whole loop with the extend_tiatool prompt (arguments: goal, optional project). It tells the model the order above and the rules that go with it — reuse before writing, never guess a member name, read before write, confirm writes, verify, save, learn.

Safety model

  • Licence. Every tool needs an active licence. Listing and showing extensions, offline queries and knowledge_add only read; live scripts and extensions follow the rules of the live tools, and anything that changes a project needs the write feature.
  • Read mode is a promise. A read run happens inside an Openness exclusive access and transaction that is rolled back; the project is never saved. A script that tries to change something in read mode is stopped by ctx.RequireWrite(...).
  • Write mode is explicit and approved by a person. script_run mode=write (and an extension whose manifest declares "mode": "write") needs a person's yes, which the AI cannot give itself: an AI client that supports MCP elicitation shows you the plan as a form (the model never sees a token); otherwise the result names a request and you run tiax-agent approve <id> in a terminal, then let the assistant repeat the call. The old confirm_token round trip is used only when a policy sets "confirm_mode": "in_band". After the approval the run takes a new timestamped backup, commits the transaction only when the script returns without an error, saves, journals (with the approver and the code's SHA-256) and runs the verify gate.
  • Policy. An administrator can limit scripts with "allow_scripts": "off" | "read" | "write" in the policy file (security/policy.md); read keeps write runs out.
  • Guard rail, not a sandbox. Scripts are checked against a denylist: processes, registry, network, loading or emitting code (Assembly.Load*, Reflection.Emit), AppDomain (except AppDomain.CurrentDomain.GetAssemblies(), to find an Openness type by name), Environment.Exit, writing, copying, deleting or moving files and folders (scripts may read files; Openness exports still work), pipes and memory-mapped files, threads, timers and background tasks, dynamic, native interop (DllImport, Marshal), unsafe code, COM activation, and saving, closing or opening projects (TIATool does that). Every script run is also bracketed by a check of the approval and policy files: if one appears or changes during the run it is restored, the run is refused and the journal records script_tamper. Cache queries run in a JavaScript engine without .NET access and with time and memory limits. That keeps honest mistakes from doing damage; it is not a security boundary against a hostile script. Read a script before you approve a write run, as you would read a colleague's change.
  • Honest status. Everything self-made starts unverified. Nothing is called proven until it has run against a real project.

Exploring the object model

openness_browse lists what an Openness object offers. Paths follow the object model:

Devices["PLC_1"]/DeviceItems[1]/...   a device, then its first device item, and on
plc:PLC_1/Blocks["Main"]              shortcut: the block Main of the PLC named PLC_1

Each answer gives the attributes (name, type, read/write access), compositions and actions of the object, which is exactly what a script needs. See the tool description for the full syntax.

Writing a script

A script is one C# file (C# 12 syntax compiled for .NET Framework 4.8 — it runs inside the TIA engine process). The common System, Newtonsoft.Json.Linq, TiaTool.Scripting and Siemens.Engineering namespaces are imported for you, and a small Tia helper class is compiled in: Tia.Project(ctx), Tia.Devices(ctx), Tia.Plcs(ctx), Tia.Plc(ctx, name) (refuses an ambiguous name), Tia.Hmis(ctx), Tia.Blocks(plc), Tia.FindBlock(plc, name), Tia.TagTables(plc) — all of them walk device groups too.

using Siemens.Engineering;
using TiaTool.Scripting;

public static class Script
{
    public static object Run(ScriptContext ctx)
    {
        var project = (Project)ctx.Project;
        return new { name = project.Name, devices = project.Devices.Count };
    }
}

ScriptContext gives you:

Member Meaning
ctx.Project, ctx.Portal The open Siemens.Engineering.Project / TiaPortal (cast them).
ctx.Arg<T>(name, fallback) / ctx.Require<T>(name) Arguments; a missing required one is reported as a usage error.
ctx.RequireWrite("what") Call before the first change: refuses in read mode with a clear message.
ctx.Log(text) Lines returned with the result.
ctx.ThrowIfCancelled() Call in long loops so a timeout stops the script cleanly.
ctx.Mode, ctx.ReadOnly, ctx.TiaVersion, ctx.ProjectPath Run information.

The return value is serialized to JSON: anonymous objects, lists and dictionaries work. Return data, not prose.

Rules that keep scripts working across TIA versions:

  1. Use only members you saw in openness_browse or in the Openness documentation. Read optional scalar properties by name with GetAttribute("Name") inside try/catch, so a version without the property yields "not exposed" instead of a crash.
  2. Do every check (does it exist, is the name free, is the language valid) before ctx.RequireWrite and the first change.
  3. Walk devices in device groups too (project.DeviceGroups), not only project.Devices.
  4. Never read secrets (passwords, keys) — not even to display them.

Example 1 — read: watch and force tables per PLC

using System.Collections.Generic;
using Siemens.Engineering;
using Siemens.Engineering.HW;
using Siemens.Engineering.HW.Features;
using Siemens.Engineering.SW;
using Siemens.Engineering.SW.WatchAndForceTables;
using TiaTool.Scripting;

public static class Script
{
    public static object Run(ScriptContext ctx)
    {
        var project = (Project)ctx.Project;
        var result = new List<object>();
        foreach (var device in project.Devices)
            foreach (var item in device.DeviceItems)
            {
                var sc = item.GetService<SoftwareContainer>();
                var plc = sc != null ? sc.Software as PlcSoftware : null;
                if (plc == null) continue;
                var tables = new List<object>();
                Walk(plc.WatchAndForceTableGroup, "", tables);
                result.Add(new { plc = plc.Name, tables = tables });
            }
        return result;
    }

    private static void Walk(PlcWatchAndForceTableGroup g, string path, List<object> into)
    {
        foreach (var t in g.WatchTables) into.Add(new { kind = "watch", group = path, name = t.Name, entries = t.Entries.Count });
        foreach (var t in g.ForceTables) into.Add(new { kind = "force", group = path, name = t.Name, entries = t.Entries.Count });
        foreach (var s in g.Groups) Walk(s, path + s.Name + "/", into);
    }
}

(The built-in watch_tables_list is this script plus device groups and nested device items.)

Example 2 — write: create a PLC tag

using System;
using System.Linq;
using Siemens.Engineering;
using Siemens.Engineering.HW.Features;
using Siemens.Engineering.SW;
using TiaTool.Scripting;

public static class Script
{
    public static object Run(ScriptContext ctx)
    {
        var project = (Project)ctx.Project;
        var name = ctx.Require<string>("name");
        var plc = project.Devices.SelectMany(d => d.DeviceItems)
            .Select(i => i.GetService<SoftwareContainer>()).Where(s => s != null)
            .Select(s => s.Software).OfType<PlcSoftware>().Single();
        var table = plc.TagTableGroup.TagTables.First(t => t.IsDefault);
        if (table.Tags.Find(name) != null) throw new ScriptArgumentException("name", "already exists");

        ctx.RequireWrite("create PLC tag " + name);          // nothing has changed before this line
        var tag = table.Tags.Create(name, ctx.Require<string>("data_type"), ctx.Require<string>("address"));
        return new { table = table.Name, name = tag.Name, address = tag.LogicalAddress };
    }
}

Run it with mode=read first: it checks everything and stops at RequireWrite with a message saying what it would do. Then run it with mode=write and confirm.

Example 3 — an API that only some TIA versions have

Some Openness services (software units, user management, cross-references) do not exist in every version. Look them up by name at run time, so the script compiles everywhere and answers honestly (AppDomain.CurrentDomain.GetAssemblies() is the one AppDomain call the guard rail allows):

using System;
using System.Linq;
using Siemens.Engineering;
using TiaTool.Scripting;

public static class Script
{
    public static object Run(ScriptContext ctx)
    {
        var project = (Project)ctx.Project;
        var type = AppDomain.CurrentDomain.GetAssemblies()
            .Where(a => a.GetName().Name.StartsWith("Siemens.Engineering", StringComparison.Ordinal))
            .Select(a => a.GetType("Siemens.Engineering.Umac.UmacConfigurator", false))
            .FirstOrDefault(t => t != null);
        if (type == null) return new { available = false, reason = "no UMAC service in this Openness version" };
        var service = typeof(IEngineeringServiceProvider).GetMethod("GetService").MakeGenericMethod(type).Invoke(project, null);
        return new { available = service != null };
    }
}

Extensions

An extension is a folder:

extensions/<id>/
  extension.json     the manifest
  script.cs          kind "script": runs live in TIA Portal (extension_run)
  query.js           kind "cache":  runs offline on the .tiax cache (cache_query extension=<id>)

extension.json (tiatool.extension/1):

Field Meaning
id ^[a-z][a-z0-9_]{2,47}$, unique; the folder name.
title, description What the AI reads to choose it — say precisely what it returns or changes.
category, tags Grouping and search words (extension_list category= / query=).
kind script (live) or cache (offline, always read-only).
mode read or write. Anything that changes the project is write.
parameters JSON Schema (type object) of the arguments; arguments are checked against it.
min_tia Lowest TIA Portal version it is written for, e.g. V17.
status unverified until a live selftest ran it; then selftest_verified / proven.
provenance Set by extension_save: where the script came from (session, journal id, assembly hash).

Override chain (the same as equipment templates): built-in → %APPDATA%\TIATool\extensions → <workspace>/extensions. A folder with the same id further down the chain replaces the one before it, so you can fix or adapt a built-in for one project without touching the installation. A broken override is reported by extension_list and the previous definition stays in use.

Offline queries: cache_query

cache_query runs JavaScript (ES2023, strict mode) over the scanned cache in tiax-agent. Pass code, or extension=<id> with args to run a cache extension.

// every FB that writes more than 20 symbols
tia.edges({ type: 'FB' }).filter(b => b.writes.length > 20).map(b => ({ name: b.name, writes: b.writes.length }))

The result is the value of the last expression — or, when the code defines function run(args), what run returns (that is the shape of a cache extension's query.js). log(...) lines come back with the result.

Call Returns
tia.project() project.json of the scan: PLCs, HMI devices, counts, fidelity.
tia.manifest() Every manifest row (blocks, tag tables, HMI artifacts).
tia.blocks({type, lang, plc, name}) PLC manifest rows: id, name, type, lang, plc, number, networks, fidelity, ...
tia.block(name) / tia.record(id) The full record: interface tree, networks with scl, reads, writes, calls.
tia.edges({type, plc, name}) Compact per block: name, type, plc, lang, fidelity, networks, scl_chars, members, writes, reads, calls, uses_types. Use it for whole-project questions.
tia.tags({plc, table, name}) PLC tags {name, type, address, comment, table, plc}.
tia.constants({plc, table, name}) User constants {name, value, table, plc}.
tia.whoWrites(symbol, exact) / tia.whoReads(symbol, exact) Sites {block, network, title, direction, confidence, scl}.
tia.callers(block) / tia.calls(block) The call graph from CallInfo.
tia.unreachable() Code blocks no OB reaches.
tia.search(text, limit) Ranked rows across blocks and HMI.
tia.screens(device) / tia.screen(name) HMI screens.
tia.hmiTags(filter) HMI tags with table, connection and the screens using them.
tia.hmi(type, device) HMI manifest rows of a type, e.g. 'TextList', 'HmiTagTable'.
args The arguments object (frozen).

Limits: arrays above 5 million elements and strings above 16 million characters are refused before they are allocated; 5 s of script time and 64 MB of script allocations (the time and memory the host spends loading cache records do not count), 30 s wall clock, 10 million statements, recursion depth 256, a result of at most 1 million characters (return summaries, not whole records) and 200 log lines. The engine has no file, network or .NET access and no eval.

The answers are as good as the cache: STL and GRAPH bodies record no read/write edges, LAD/FBD renderings marked lossy are incomplete, and HMI-to-PLC matches are by name only (the export does not carry the link).

Learning: knowledge_add

When a script or tool fails and you find out why, record it:

knowledge_add title="Watch table entries need the quoted symbol"
              symptom="<the verbatim error text>"
              cause="<what was proven>" fix="<what worked>" evidence="<calls, journal id, TIA version>"

The entry is written to <workspace>/knowledge/learned/<slug>.md (without a workspace: %APPDATA%\TIATool\knowledge\learned) in the same format as the shipped knowledge base, with a Symptom block holding the verbatim error. From then on explain_error and the error answers of every tool match the same error text, and the entry is listed as a tiatool://knowledge/workspace/… resource marked learned, not reviewed. Entries that prove themselves belong in the shipped knowledge base: copy them into knowledge/ and review them like any other change.

The built-in pack

Category Extension Kind / mode What it does
Project project_properties script / read Author, comment, copyright, family, version, times, size.
Project project_languages script / read Editing, reference and active project languages.
Project project_inventory script / read Devices, subnets, and per PLC / Classic HMI the count of every kind of object.
Devices and catalog device_inventory script / read Devices and modules with position, type identifier, order number and firmware.
Devices and catalog plc_overview script / read Per PLC: CPU order number and firmware, block / tag / type counts, protected and inconsistent blocks.
Devices and catalog catalog_search script / read Hardware catalog search: order number, version, type identifier.
Network network_interfaces script / read Every interface node: subnet, IP address, mask, router, PROFINET name, device number.
Network network_subnets script / read Subnets with type, connected nodes and IO systems.
Network io_systems script / read IO systems with controller and IO devices.
Network network_set_ip script / write Set the IP address (and mask) of a device's Ethernet node.
Watch and force tables watch_tables_list script / read Watch and force tables per PLC with entry counts.
Watch and force tables watch_table_read script / read The entries of one watch or force table.
Watch and force tables watch_table_create script / write Create a watch table with entries (or append to one).
Text lists hmi_text_lists script / read Text and graphic lists of every Classic HMI.
Text lists hmi_text_list_entries cache / read Text-list entries per language and missing translations.
Technology objects technology_objects_list script / read Axes, PID controllers, cams, counters per PLC.
Technology objects technology_object_parameters script / read Parameters of one technology object.
Technology objects technology_object_set_parameter script / write Set one technology-object parameter.
Software units software_units_list script / read Software units of a PLC (where the TIA version has them).
Libraries library_types script / read Project or global library types with every version and state.
Libraries library_master_copies script / read Master copies with folder path.
Libraries global_libraries script / read Known and open global libraries.
Libraries library_type_instances script / read Type instances in the project and the ones not on the newest released version.
HMI hmi_connections script / read HMI connections (Classic and Unified), no secrets.
HMI hmi_screens_list script / read Screens, templates and pop-ups (Classic) and screens (Unified).
HMI hmi_alarm_classes script / read Alarm classes of Unified HMIs.
Security (UMAC) umac_users_roles script / read Users, groups and roles, read-only, no secrets.
PLC tags and constants tag_tables_list script / read Tag tables with group path and counts.
PLC tags and constants tag_table_read script / read One tag table with types, addresses, comments and External* flags.
PLC tags and constants tag_create script / write Create a PLC tag.
PLC tags and constants tag_update script / write Change a tag's name, type, address, comment or External* flags.
PLC tags and constants tag_delete script / write Delete a PLC tag (returns what it was).
PLC tags and constants tag_table_create script / write Create a tag table in a group path.
PLC tags and constants tags_by_address_range script / read Tags inside an address range, e.g. %I0.0..%I7.7.
PLC tags and constants tag_address_overlaps script / read Tags whose addresses overlap.
PLC tags and constants user_constants_list script / read User constants with type and value.
PLC data types udt_list script / read UDTs with consistency, protection and library version.
Blocks block_protection_status script / read Know-how protected blocks and counts per type.
Blocks block_language_census script / read Blocks by language, type and memory layout.
Blocks block_numbers script / read Used and next free block numbers per number space.
Blocks block_consistency script / read Inconsistent blocks and UDTs.
Blocks block_attributes script / read Every exposed property of one block.
Blocks block_groups script / read The program-block folder tree with counts.
Cross-reference cross_reference script / read Live TIA cross-reference of one tag or block (where the TIA version has it).
Offline analysis unused_tags cache / read PLC tags no block or HMI uses (by name).
Offline analysis blocks_by_language cache / read Blocks by language, type and cache fidelity.
Offline analysis largest_blocks cache / read The biggest code blocks.
Offline analysis write_conflicts cache / read Symbols written by more than one block.
Offline analysis alarm_text_coverage cache / read Alarm-like DB bits without a comment.
Offline analysis hmi_tags_without_plc cache / read HMI tags whose name matches nothing on the PLC side.
Offline analysis naming_violations cache / read Names that break your naming rules (regex per kind).
Offline analysis call_depth cache / read The deepest call chain below every OB.
Offline analysis udt_usages cache / read Blocks and tags that use a UDT.

extension_list always shows the live library, including your own extensions and overrides.

Limits and honesty

  • Compiling against our reference stub proves a script matches our description of the Openness API. Only the live selftest proves it matches yours.
  • A write extension is a write: it is approved by a person, backed up, journaled and verified like any other change. Read the script before approving it.
  • The script guard rail and the query limits catch mistakes, not attackers.
  • Learned knowledge is written by an AI and marked as such until a person reviews it.

Built from docs/self-extension.md in the product repository. Links to files that are not published here are shown as plain text.