Variables and the Type System in SIMATIC AX

May 11, 2026 · siemenssimatic-axtia-portalplc-programmingindustrial-automationstructured-textiec-61131-3apax

In TIA Portal, variables are spread across several familiar places.

You have PLC tags. You have DB variables. You have UDTs. You have enum types. You have function block inputs, outputs, in-outs, static variables, temp variables, and constants. If you are working close to hardware, you also have direct I/O addresses.

SIMATIC AX does not remove these PLC concepts. It writes them down as Structured Text.

That is the main mental shift for this article. The question is not “where is the tag table?” The better question is:

Where does this variable live, who owns it, and how strongly should its type be defined?

I am still studying AX through demo projects, not claiming customer deployment experience with it. But the TIA Portal side of this mapping is familiar territory. Once I stopped looking for a graphical project tree, the AX variable model started to feel much less foreign.

Correction note. After publication, a reader pointed out that several patterns described below are not AX-specific — TIA Portal SCL uses the same VAR_INPUT / VAR_OUTPUT / VAR text syntax for FB interfaces, accepts inline STRUCT declarations inside VAR sections, and supports typed literals like REAL#1.0 and INT#5. The article has been updated to make that clearer, and to keep the focus on what AX actually adds: namespaces, CONFIGURATION blocks, VAR_EXTERNAL contracts, and namespace-scoped types.

Start With A Function Block Interface

The cleanest place to start is the TempController function block from my demo project.

In TIA Portal, this would be a normal FB interface: measured value in, setpoint in, hysteresis in, enable in, heater output out, fault output out.

In AX, the same interface is text:

NAMESPACE Otomakeit.Demo.TempControl

    FUNCTION_BLOCK TempController

        VAR_INPUT
            i_rActualTemp  : REAL;    // Measured temperature
            i_rSetpoint    : REAL;    // Desired temperature setpoint
            i_rHysteresis  : REAL;    // Hysteresis band
            i_xEnable      : BOOL;    // Enable controller
        END_VAR

        VAR_OUTPUT
            q_xHeating     : BOOL;    // Heating output
            q_xFault       : BOOL;    // Fault flag
            q_rError       : REAL;    // Setpoint - actual
        END_VAR

        VAR
            _xHeatingState : BOOL;    // Internal latched state
        END_VAR

        VAR CONSTANT
            TEMP_MIN_VALID : REAL := REAL#-50.0;
            TEMP_MAX_VALID : REAL := REAL#500.0;
        END_VAR

        // Logic body runs when the FB instance is called.

    END_FUNCTION_BLOCK

END_NAMESPACE

If you write FBs in TIA Portal SCL, this will look familiar. The section keywords — VAR_INPUT, VAR_OUTPUT, VAR, VAR_TEMP, VAR CONSTANT — are the same in TIA SCL and have been since S7-SCL in SIMATIC Manager. What changes in AX is not the FB-interface syntax itself, but what surrounds it: namespaces above the FB, and an explicit CONFIGURATION / PROGRAM block higher up.

For a TIA engineer, the sections are the important part:

  • VAR_INPUT is the FB input interface.
  • VAR_OUTPUT is the FB output interface.
  • VAR is internal static data. It persists as part of the FB instance.
  • VAR CONSTANT is for local constants.

The syntax is different, but the PLC idea is not. The FB still has an interface. It still owns internal memory. Its outputs still belong to the FB instance.

The constants are worth noticing. I do not write TEMP_MIN_VALID := -50.0. I write:

TEMP_MIN_VALID : REAL := REAL#-50.0;
TEMP_MAX_VALID : REAL := REAL#500.0;

That REAL# prefix is a typed literal. It tells the compiler the literal is a REAL, not just “some number that looks like a decimal.” In the unit testing article, the same pattern appeared in test calls:

gTempController(
    i_rActualTemp := REAL#94.0,
    i_rSetpoint   := REAL#100.0,
    i_rHysteresis := REAL#5.0,
    i_xEnable     := TRUE
);

This is not just cosmetic. It makes the code say exactly what the type contract is. When the formal parameter is REAL, the literal is visibly REAL. When a constant expression uses an integer, you can write INT#5. Siemens’ ST documentation also uses examples like REAL#1.0 and INT#5 in constant expressions.

For PLC code, I like that. It reduces the quiet guessing that can creep into numeric logic.

Typed literals are not AX-specific either. They are IEC 61131-3 syntax, and TIA Portal SCL accepts the same REAL#, INT#, TIME#, and related prefixes. What changes in AX is how visibly the codebase leans on them — library APIs tend to require them where TIA SCL would tolerate implicit conversion.

The TIA Portal Mapping

Here is the practical map I use when moving from TIA Portal thinking to AX text:

TIA Portal ideaAX location or section
FB inputsVAR_INPUT inside FUNCTION_BLOCK
FB outputsVAR_OUTPUT inside FUNCTION_BLOCK
FB in-out parametersVAR_IN_OUT
FB static/internal variablesVAR inside the FB
Temporary calculation variablesVAR_TEMP
FB constantsVAR CONSTANT
Global DB variablesVAR_GLOBAL inside CONFIGURATION
Reading global variables from a programVAR_EXTERNAL inside PROGRAM
Read-only global constantsVAR_GLOBAL CONSTANT plus VAR_EXTERNAL CONSTANT
UDTTYPE ... STRUCT ... END_STRUCT; END_TYPE
PLC enum typeTYPE Name : (Value1, Value2, ...) := Default; END_TYPE
Direct I/O addressvariable declared with AT %I..., AT %Q..., or similar address area

This is not the full language reference. It is the first map I would want as a TIA Portal engineer opening an AX project.

Globals Are Not A Free-For-All

In TIA Portal, a global DB is easy to reach from anywhere. That convenience is useful, but it also makes ownership blurry.

AX makes the relationship more explicit. In the TempController demo, configuration.st declares global variables:

USING Otomakeit.Demo.TempControl;
USING Otomakeit.Demo.Safety;

{OpcUa.NodeGenerator.IdType = SymbolName}
CONFIGURATION MyConfiguration
    TASK Main(Priority := 1);
    PROGRAM P1 WITH Main: MainProgram;

    VAR_GLOBAL
        gTempController    : TempController;
        gOverheatProtect   : OverheatProtection;
    END_VAR

    {S7.extern = ReadWrite}
    {OpcUa.AccessLevel = ReadWrite}
    VAR_GLOBAL
        g_rActualTemp  : REAL;
        g_rSetpoint    : REAL;
        g_xHeating     : BOOL;
        g_xFault       : BOOL;
    END_VAR
END_CONFIGURATION

The global variables exist at configuration level. But MainProgram still redeclares the ones it uses:

PROGRAM MainProgram

    VAR_EXTERNAL
        gTempController    : TempController;
        gOverheatProtect   : OverheatProtection;
        g_rActualTemp      : REAL;
        g_rSetpoint        : REAL;
        g_xHeating         : BOOL;
        g_xFault           : BOOL;
    END_VAR

END_PROGRAM

This is a useful discipline. The global owner is the configuration. The program explicitly says, “I use these external variables.” That is closer to an import contract than to casually reaching into a global DB.

I do not want to turn this article into the full CONFIGURATION, TASK, and PROGRAM article. That deserves its own treatment. For variables, the key point is simple: VAR_GLOBAL declares global data, and VAR_EXTERNAL makes that data visible inside the program organization unit that uses it.

STRUCT Is The AX Version Of The UDT Mental Model

TIA Portal engineers already understand UDTs. You define one structured type, then reuse it everywhere.

In AX, the demo project does this with TempControlStatus:

NAMESPACE Otomakeit.Demo.Types

    TYPE TempControlStatus : STRUCT
        xHeating    : BOOL;
        xFault      : BOOL;
        rActualTemp : REAL;
        rError      : REAL;
    END_STRUCT;
    END_TYPE

END_NAMESPACE

That is the closest mental equivalent to a TIA UDT.

The type lives in a namespace: Otomakeit.Demo.Types. Then MainProgram imports that namespace and declares a local variable of that type:

USING Otomakeit.Demo.Types;

PROGRAM MainProgram
    VAR
        _stStatus : TempControlStatus;
    END_VAR
END_PROGRAM

The program fills the structure from the controller outputs:

_stStatus.xHeating    := gTempController.q_xHeating;
_stStatus.xFault      := gTempController.q_xFault;
_stStatus.rActualTemp := g_rActualTemp;
_stStatus.rError      := gTempController.q_rError;

This is normal PLC engineering. Package related values into a structured contract, pass that contract between subsystems, and avoid a pile of separate loose variables.

One thing worth being precise about: an inline STRUCT declared in the VAR section of an FB, or a named UDT-equivalent declared via TYPE ... STRUCT ... END_TYPE, also works in TIA Portal SCL. The bare type machinery is shared. The TIA-side limitation is that the type does not live in a namespace, which is what AX changes next.

The namespace piece is the part that differs from TIA Portal. In TIA, a PLC data type sits in the project tree. In AX, the type sits in source code and belongs to a namespace. That means two libraries can both have a type called Status without colliding, because their full names are different.

A STRUCT Can Be A Contract Between Namespaces

The namespace demo project has an even smaller example:

NAMESPACE Otomakeit.Demo.WaterTreatment.Types

    TYPE AnalogReading :
        STRUCT
            Value    : REAL;    // Engineering value
            RawValue : INT;     // Raw analog count
            InRange  : BOOL;    // Range validity flag
        END_STRUCT;
    END_TYPE

END_NAMESPACE

This is exactly the kind of type I would normally make as a TIA UDT: engineering value, raw value, diagnostic bit. In AX, I like putting this in a Types namespace because it makes the ownership obvious. The dosing controller, diagnostics logic, and any HMI-facing mapping can agree on the same shape without owning each other.

That is the practical value of STRUCT in AX: not just a group of fields, but a named contract.

ENUM Is For Named States

The same namespace demo has a pump status type:

NAMESPACE Otomakeit.Demo.WaterTreatment.Types

    TYPE PumpStatus : (
        Stopped,
        Running,
        Fault
    ) := Stopped;
    END_TYPE

END_NAMESPACE

From the TIA side, think of this as a PLC enum type. Instead of passing 0, 1, and 2 around the project, the code can say Stopped, Running, and Fault.

That matters more than it looks.

An INT called _pumpStatus tells me almost nothing unless I go find the comment or the HMI text list. A PumpStatus variable tells me the allowed states right at the declaration. It also prevents the accidental fourth state that nobody designed.

For state machines, alarms, modes, and statuses, I would rather read this:

_pumpState : PumpStatus;

than this:

_pumpState : INT;

The second one may be normal in older PLC projects. The first one is a better contract.

Direct Addresses Still Exist

The TempController demo currently uses HMI-visible globals and OPC UA. It does not show a real input card mapped to a sensor.

But AX does support direct represented variables with the AT keyword. Siemens’ hardware documentation shows generated I/O address variables like this:

The snippet below is an excerpt of the generated I/O-address configuration. It shows the relevant VAR_GLOBAL declarations, but omits the surrounding task/program lines from the full Siemens example so it should not be read as a complete standalone CONFIGURATION.

CONFIGURATION IoAddresses
    VAR_GLOBAL
        MyIODevice_MyDigitalInputModule_Input AT %IB100
            : MyIODevice_MyDigitalInputModule_Input_Layout;

        MyIODevice_MyAnalogOutputModule_Output AT %QB100
            : MyIODevice_MyAnalogOutputModule_Output_Layout;
    END_VAR
END_CONFIGURATION

The generated layout type can then expose channels inside the input or output image:

TYPE
    MyIODevice_MyDigitalInputModule_Input_Layout : STRUCT
        Channel_0 AT %X0.0 : BOOL;
        Channel_1 AT %X0.1 : BOOL;
        Channel_2 AT %X0.2 : BOOL;
        Channel_3 AT %X0.3 : BOOL;
    END_STRUCT;
END_TYPE

Those Channel_0 AT %X0.0 fields are relative bit offsets inside the generated layout type that starts at %IB100 in the example above. They are not separate global %I0.0 declarations.

That maps cleanly to the TIA Portal idea of PLC tags bound to physical addresses. The difference is that the address binding is visible as text, and in hardware-configuration workflows it may be generated into system constants rather than typed by hand.

I would not mix this topic too deeply into the variable article, because I/O abstraction deserves its own article. The important point here is that variables can still be tied to %I, %Q, and related PLC memory areas. AX did not remove the PLC memory model.

The Practical Rule

When I add a variable in AX, I try to ask three questions before writing code:

  1. Does this variable belong to an FB instance, a program, the configuration, or a shared type?
  2. Is this a simple scalar, or should it be a STRUCT or ENUM?
  3. Should the variable be local, external, global, constant, temporary, or physically addressed?

That order helps.

If it belongs to a function block interface, it is probably VAR_INPUT, VAR_OUTPUT, or VAR_IN_OUT.

If it is internal FB memory, it is VAR.

If it is just scratch calculation data, it is VAR_TEMP.

If it is a shared machine-wide value, it may be VAR_GLOBAL in the configuration and VAR_EXTERNAL where it is used.

If it represents a repeated shape of data, make a STRUCT.

If it represents a finite set of states, make an ENUM.

If it is tied to a physical address, use AT at the right level and keep the hardware boundary obvious.

The thing I like about AX is that these decisions become reviewable. In TIA Portal, you often have to click through block interfaces, DBs, tag tables, and PLC data types to understand the same thing. In AX, it is text. A pull request can show that someone changed a setpoint from REAL to LREAL, added a new field to a status STRUCT, or exposed a global via OPC UA.

That does not make the engineering automatically good. Bad variable ownership is still bad variable ownership. But it makes the design visible.

What Is Next

This article stayed on the foundation: variables, sections, scalar types, STRUCT, ENUM, typed literals, and direct address binding.

Next I want to go into the apax CLI in more detail. The earlier articles showed that the CLI works; the next one should map the commands by lifecycle: create, install, build, test, package, clean, and diagnose.

If you have done AX on a real project and organize variables differently, I want to hear it. I am especially interested in where teams draw the line between globals, structured status types, and hardware abstraction. I would rather learn the better field pattern than defend my first demo-project pattern.

Planning a new project? Message us to see how we can help.