WinCC V7 Alarm Logging to External SQL Database via C-Script GMsgFunction
Siemens WinCC V7 exposes a runtime-side hook named GMsgFunction that is invoked whenever a configured alarm changes state. When paired with a small C-Script that shells out to sqlcmd.exe, it becomes a reliable pipeline for writing alarm events, process values, and timestamps to an external SQL Server database. This article documents a complete, field-proven implementation for WinCC V7 SP2 and later, including the SQL schema, the C-Script, the ProgramExecute wrapper, the parameter encoding rules, and a verification procedure that eliminates the VBScript-on-save trigger problem that motivates most users to abandon the VBScript approach.
Problem: VBScript Actions Re-Fire When the Editor Saves
When a tag property action is bound directly to a configured WinCC alarm (for example through Alarm Logging → Message Line → Properties → Tag/Action → Function Name with a VBScript body), the action's body is evaluated not only at runtime on state changes, but also every time the project editor saves the configuration. Each save re-evaluates all attached actions, and any VBScript that issues an INSERT against a database will therefore push stale rows for every active alarm at the moment of the save.
The visible symptom is a flood of duplicate or zombie rows in the SQL table during development: the operator has not triggered any new alarm, yet the row count keeps climbing while the engineer is editing. In production this side effect can also occur on redundancy failover, project reactivation, and HMI restart, all of which re-enter the same save/load path. The cleanest way to remove the noise is to stop using VBScript actions bound to tag events and move the logic to a C-Script registered as the project's user-defined action for messages — the function that WinCC calls only at runtime, only on real state transitions.
| Mechanism | Fires on Editor Save | Fires on Project Activation | Fires on Real State Change | Recommended Use |
|---|---|---|---|---|
| VBScript tag action | Yes | Yes (often) | Yes | Display logic only |
| VBScript alarm action | Yes | Yes (often) | Yes | Display logic only |
| C-Script GMsgFunction | No | No (state change only) | Yes | External logging, side effects |
| Global Script C action scheduled | No | No | No (timer driven) | Polling, batched writes |
Architectural Shift: From VBScript to C-Script GMsgFunction
WinCC V7 reserves the function name GMsgFunction as the standard entry point for a user-defined action that runs whenever a message configured in Alarm Logging fires, acknowledges, or clears. WinCC compiles the C-Script at project compile time, links it into the runtime, and dispatches into it with a fixed parameter list. Because the dispatcher is reached only via the runtime alarm task — never through the editor's action validation path — save cycles do not reach the function body.
For the SQL write path, two implementation strategies are available from C-Script in WinCC:
- Call an external program (
sqlcmd.exe) with the row payload on the command line. The OS process is short-lived, has no state, and uses the standard SQL Server client. - Use
MSDBLLIB/ODBC32via a custom DLL. This is faster, but ships a C++ dependency into the project and complicates deployment.
This article covers strategy (1) because it is the only approach that does not require redistributing native libraries, fits inside the WinCC script sandbox, and is officially acknowledged in Siemens support note 23370769 for SQL access from WinCC C-Script.
Prerequisites
- WinCC V7 SP2 or later (SP3, SP4, V7.2, V7.3, V7.4, V7.5 are all valid — the function signature changed slightly between major service packs; see the parameter table below).
- Microsoft SQL Server 2008 R2 or later (Express, Standard, or Enterprise). The procedure works identically against any edition that accepts
sqlcmdconnections. - SQL Server Client Tools installed on the WinCC station so that
sqlcmd.exeis on%PATH%(or the script is updated to use the full path). - A 32-bit ODBC System DSN named, for example,
WinCC_AlarmLog, pointing at the target database. The DSN is used bysqlcmdthrough the-Sand-dswitches or via a DSN alias. - WinCC administrator rights to compile scripts and to write to the project directory.
- The Windows account running the WinCC Runtime must have permission to
INSERTinto the target table, and to spawn child processes. On Windows Server, theWinCCservice account or the localSYSTEMaccount (default) is typical.
sqlcmd is running. Use Windows Authentication (-E) and grant the WinCC service account db_datawriter on the target database. If SQL authentication is unavoidable, prefer a contained database user with a strong password, and limit the account to INSERT on the alarm table only.GMsgFunction: Signature, Parameters, and Trigger Semantics
The exact parameter list of GMsgFunction depends on the WinCC version. Two common forms are listed below; if your installation compiles neither, check the project's C-Script action template in the WinCC Information System under Alarm Logging → Working with Alarms → User-defined Actions.
WinCC V7 SP2 / SP3 form
void GMsgFunction(DWORD dwMsgNr, DWORD dwState, DWORD dwTime,
LPCTSTR lpszPictureName, LPCTSTR lpszObjectName,
LPCTSTR lpszMsgText, LPCTSTR lpszReserved);
WinCC V7.2 / V7.3 / V7.4 form (extended time)
void GMsgFunction(DWORD dwMsgNr, DWORD dwState,
MSG_TIME_STRUCT* pstTime,
LPCTSTR lpszPictureName, LPCTSTR lpszObjectName,
LPCTSTR lpszMsgText, LPCTSTR lpszReserved);
| Parameter | Type | Meaning | Field Notes |
|---|---|---|---|
dwMsgNr |
DWORD | Internal message number assigned by Alarm Logging | Stable for the project's lifetime; do not display this number to operators. |
dwState |
DWORD | Bitmask of message state | Decode with MSG_STATE_CAME (0x0001), MSG_STATE_WENT (0x0002), MSG_STATE_ACK (0x0004), MSG_STATE_RESET (0x0008). |
dwTime / pstTime
|
DWORD / struct | Event time in WinCC time base (100 ns ticks) or struct | Convert to SQL datetime2 with WinCCDateTimeToIso. |
lpszPictureName |
LPCTSTR | Picture in scope when the alarm fired | Empty string when alarm was not triggered from a picture. |
lpszObjectName |
LPCTSTR | Object that owns the alarm (tag or class) | Often a tag name or the alarm class name. |
lpszMsgText |
LPCTSTR | Resolved message text including any process value fields | Already substituted; no further tag reads required for static fields. |
lpszReserved |
LPCTSTR | Reserved for future use | Treat as read-only; do not write to it. |
The state bits are the only reliable way to tell came in from went out from acknowledged. Filter the dwState value before issuing the SQL write to keep one row per logical event.
Step-by-Step: SQL Database Schema
Run the following script on the target SQL Server to create the destination database and the alarm table. The schema separates the structural columns from the dynamic process value blob so that analytical queries can index on time, message number, and state without scanning the text.
-- 1. Database
IF DB_ID('WinCC_AlarmLog') IS NULL
BEGIN
CREATE DATABASE WinCC_AlarmLog;
END;
GO
USE WinCC_AlarmLog;
GO
-- 2. Table
IF OBJECT_ID('dbo.AlarmEvent', 'U') IS NULL
BEGIN
CREATE TABLE dbo.AlarmEvent
(
EventID BIGINT IDENTITY(1,1) NOT NULL,
EventTime DATETIME2(3) NOT NULL,
EventTimeUTC DATETIME2(3) NOT NULL,
MsgNr INT NOT NULL,
StateCode INT NOT NULL,
StateText NVARCHAR(32) NOT NULL,
PictureName NVARCHAR(255) NULL,
ObjectName NVARCHAR(255) NULL,
MsgText NVARCHAR(1024) NULL,
ProcessValue NVARCHAR(255) NULL,
HostName NVARCHAR(64) NOT NULL
CONSTRAINT DF_AlarmEvent_HostName DEFAULT HOST_NAME(),
CONSTRAINT PK_AlarmEvent PRIMARY KEY CLUSTERED (EventID)
);
CREATE NONCLUSTERED INDEX IX_AlarmEvent_Time
ON dbo.AlarmEvent (EventTime);
CREATE NONCLUSTERED INDEX IX_AlarmEvent_MsgNr
ON dbo.AlarmEvent (MsgNr) INCLUDE (EventTime, StateCode);
END;
GO
-- 3. Permission for the WinCC service account (replace DOMAIN\svc-wincc)
CREATE USER [DOMAIN\svc-wincc] FOR LOGIN [DOMAIN\svc-wincc];
ALTER ROLE db_datawriter ADD MEMBER [DOMAIN\svc-wincc];
GRANT EXECUTE ON SCHEMA::dbo TO [DOMAIN\svc-wincc];
GO
ProcessValue is stored as NVARCHAR(255) because WinCC substitutes it as a textual field inside the message text. If your project relies on numeric analytics, consider populating a sibling ProcessValueNum FLOAT NULL column from a parsed value in the C-Script.Step-by-Step: Wiring the C-Script Action
- Open the WinCC Explorer and double-click Alarm Logging.
- In the message tree, select either a single message (for one-off cases) or a message class (recommended for the bulk of your alarms).
- Open Properties (right-click → Properties, or use the menu).
- Navigate to the Tag/Action tab.
- Set Function Name to exactly
GMsgFunction. The name is case sensitive. - Confirm the action type is C-Script. If the project editor prompts for a function body, leave it empty for now; the body is supplied in the next section.
- Compile the project. Any C-Script compile error will block the project from going online, so fix syntax issues here before activating Runtime.
If you have many message classes, repeat step (2) for each class. Sharing one GMsgFunction across all classes is the recommended pattern — the function dispatches based on the parameters.
Step-by-Step: Implementing the C-Script
Open the global C-Script editor (Global Scripts → C-Editor) and create a new action. Paste the body below, then compile. The function escapes single quotes by doubling them (the standard SQL Server convention) and writes the row by spawning sqlcmd.exe with a single -Q command.
#include "apdefap.h"
#include "msdbllib.h"
// Build a SQL-safe literal from a C string. Doubles single quotes and
// truncates to the maximum length supported by the table.
static void SqlEscape(char *dst, const char *src, size_t dst_size)
{
size_t j = 0;
if (src == NULL) { dst[0] = '\0'; return; }
for (size_t i = 0; src[i] != '\0' && j + 1 < dst_size; ++i)
{
char c = src[i];
if (c == '\'') {
if (j + 2 >= dst_size) break;
dst[j++] = '\'';
dst[j++] = '\'';
} else if (c == '\n' || c == '\r') {
// Replace newlines with spaces; the SQL column is NVARCHAR.
dst[j++] = ' ';
} else {
dst[j++] = c;
}
}
dst[j] = '\0';
}
// Convert WinCC time (100 ns ticks since 1601) to ISO 8601 UTC.
static void WinCCDateTimeToIso(DWORD ft_time, char *buf, size_t buf_size)
{
// Use MSDBLLIB helper: SysTimeToUTC then format as "YYYY-MM-DDTHH:MM:SS.fffZ"
SYSTEMTIME st_utc;
FileTimeToSystemTime((FILETIME*)&ft_time, &st_utc);
// Optionally convert to UTC if local was intended:
// SystemTimeToTzSpecificLocalTime(NULL, &st_utc, &st_utc) is the inverse.
_snprintf_s(buf, buf_size, _TRUNCATE,
"%04u-%02u-%02uT%02u:%02u:%02u.%03uZ",
st_utc.wYear, st_utc.wMonth, st_utc.wDay,
st_utc.wHour, st_utc.wMinute, st_utc.wSecond,
st_utc.wMilliseconds);
}
void GMsgFunction(DWORD dwMsgNr, DWORD dwState, DWORD dwTime,
LPCTSTR lpszPictureName, LPCTSTR lpszObjectName,
LPCTSTR lpszMsgText, LPCTSTR lpszReserved)
{
// 1. Filter to "came in" and "went out" only. Drop acknowledgement
// transitions; they do not represent new alarm state changes.
const DWORD wanted_mask = 0x0001 /* CAME */ | 0x0002 /* WENT */;
if ((dwState & wanted_mask) == 0) return;
// 2. Format state text.
const char *state_text =
(dwState & 0x0001) ? "CAME_IN" : "WENT_OUT";
// 3. Build SQL-safe literals.
char esc_text[1024];
char esc_pic[256];
char esc_obj[256];
char esc_iso[40];
SqlEscape(esc_text, lpszMsgText, sizeof(esc_text));
SqlEscape(esc_pic, lpszPictureName, sizeof(esc_pic));
SqlEscape(esc_obj, lpszObjectName, sizeof(esc_obj));
WinCCDateTimeToIso(dwTime, esc_iso, sizeof(esc_iso));
// 4. Compose the SQL command. Use -E for Windows Authentication;
// -S server\instance, -d database, -Q for one-shot query.
char cmd[2048];
_snprintf_s(cmd, sizeof(cmd), _TRUNCATE,
"sqlcmd.exe -S .\\SQLEXPRESS -E -d WinCC_AlarmLog -Q "
"\"INSERT INTO dbo.AlarmEvent "
"(EventTime, EventTimeUTC, MsgNr, StateCode, StateText, "
" PictureName, ObjectName, MsgText) "
"VALUES (SYSDATETIME(), '%s', %lu, %lu, '%s', '%s', '%s', '%s')\"",
esc_iso, dwMsgNr, dwState, state_text, esc_pic, esc_obj, esc_text);
// 5. Execute asynchronously to avoid blocking the alarm task.
ProgramExecute(cmd);
}
ProgramExecute is asynchronous: it returns as soon as the child process is launched. This is what you want, because synchronous execution would queue up sqlcmd processes during an alarm storm and starve the alarm task. The trade-off is that errors are lost. For mission-critical logging, add a small WriteFile to a local log file inside the function body so failures are visible even if the SQL write silently fails.Parameter Encoding and Quoting
Three encoding decisions drive the entire C-Script — get them wrong and you will get either SQL syntax errors or worse, successful injection of arbitrary text into the table.
| Field | Source | Encoding | Failure Mode |
|---|---|---|---|
Single quote '
|
lpszMsgText |
Double to ''
|
Truncation, syntax error |
Newline \n / \r
|
Any string | Replace with space | Command-line parser splits the argument |
Backslash \
|
Paths in lpszPictureName
|
Escape to \\
|
Triggers SQL LIKE wildcard interpretation if column is later searched with LIKE |
Ampersand &
|
lpszMsgText |
Quote argument or escape ^&
|
cmd.exe splits on &&
|
The most common silent failure is the un-escaped ampersand: cmd.exe interprets it as a command separator and the second half of your SQL command becomes a stray executable name. Wrap the entire -Q argument in outer double quotes (as in the example) and also escape any internal double quotes if your project ever includes " inside alarm text.
Configuration in Alarm Logging
The two configuration paths for message actions are not interchangeable:
| Path | Scope | When to Use |
|---|---|---|
| Message → Properties → Tag/Action → Function Name | Single message | Special cases, debug |
| Message Class → Properties → Tag/Action → Function Name | All messages in the class | Production use; one C-Script for all alarms |
Setting the function name on the message class is preferred because it survives re-numbering, message copy operations, and class merges. If the class is configured for "single acknowledgment" or "multi acknowledgment" the state bits are the same; only the visibility of the ACK transition changes in the Alarm Control.
Verification Procedure
- Activate WinCC Runtime in test mode.
- Trigger a test alarm by setting the configured trigger tag in the WinCC Tag Simulator (or by simulating a real process value change).
- After one second, open SQL Server Management Studio and run:
SELECT TOP 10 * FROM WinCC_AlarmLog.dbo.AlarmEvent ORDER BY EventID DESC; - Confirm that a row appeared with
StateText = 'CAME_IN'and a non-nullEventTimeUTC. - Reset the alarm (clear the trigger condition). Verify that a second row appears with
StateText = 'WENT_OUT'. - Acknowledge the alarm from the Alarm Control. Verify that no new row appears — acknowledgements are intentionally filtered out.
- Save the project from the WinCC Explorer (Ctrl+S). Verify that no new rows appear in the SQL table. This is the regression test that proves the VBScript-on-save bug is gone.
- Restart the WinCC service. Verify that no spurious
CAME_INrows are written for alarms that were already active at the moment of restart.
If step (7) still produces rows, the C-Script was not actually registered — check the project compile log for the GMsgFunction action and confirm the editor did not fall back to VBScript because of a missing function name match.
Troubleshooting Matrix
| Symptom | Likely Cause | Fix |
|---|---|---|
| No rows in SQL at all |
sqlcmd.exe not on PATH of the WinCC service account |
Use the full path C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\sqlcmd.exe, or extend the service account PATH. |
| Rows on editor save | VBScript action still attached somewhere | Search the project for Function Name in Tag/Action properties; remove any VBScript references. |
| Truncated message text | Buffer overflow in SqlEscape
|
Increase esc_text buffer to 4096 and the table column to NVARCHAR(2048). |
| SQL syntax error: "Unclosed quotation mark" | Single quote in lpszMsgText not doubled |
Confirm SqlEscape is doubling; the issue can also come from a literal apostrophe in a configured message text. |
| No rows on acknowledgement | Filter wanted_mask excludes ACK by design |
Add 0x0004 to wanted_mask only if business logic needs ACK events. |
| High CPU during alarm storm | Each ProgramExecute spawns a process |
Throttle by writing to a local queue file and dispatching with a scheduler-driven C action, or move to direct ODBC. |
| "Sqlcmd: ... Login failed for user" | SQL authentication used, mixed mode not enabled, or password has special characters | Switch to -E (Windows Authentication) and grant the WinCC service account explicit database rights. |
| Time column off by one hour | DST transition not handled in WinCCDateTimeToIso
|
Use GetSystemTime and SystemTimeToFileTime explicitly; never assume FILETIME is local. |
| Duplicate rows under redundancy | Both WinCC servers write to the same table | Add HostName to the primary key as a logical partition, or only enable GMsgFunction on the preferred server. |
Field-Proven Caveats
Alarm storm throughput
Spawning one sqlcmd.exe per alarm event is acceptable up to roughly 10 events per second on a typical Windows Server. Above that, the cost of process creation, ODBC handshake, and authentication begins to dominate and the WinCC alarm task may begin to throttle. For higher rates, either: (a) buffer rows in a memory-mapped file and flush with a scheduler-driven C action, or (b) embed a native ODBC client in a custom DLL and skip the process boundary.
Redundancy
In a WinCC Redundancy pair, both servers can call GMsgFunction on every state change because the alarm task runs independently. If the destination table is shared, add a HostName column and use a primary key that includes it, or restrict the function to the preferred server via an internal tag. The simpler choice is to use two schemas (dbo.AlarmEvent_Srv1, dbo.AlarmEvent_Srv2) and a view that unions them.
Time zone handling
WinCC stores time internally as FILETIME in UTC. FileTimeToSystemTime returns UTC components, so the YYYY-MM-DDTHH:MM:SS.fffZ string is always UTC. Do not convert to local time inside GMsgFunction; if the operations team needs local time, do it at query time in SQL with EventTimeUTC AT TIME ZONE 'Central European Standard Time'.
Process value extraction
The lpszMsgText parameter arrives fully substituted. If the message text contains a token like @1%s@ or @2%f@, the resolved value is already inside the text and you do not need an extra GetTagXxx call. Avoid calling GetTagXxx from inside GMsgFunction; it can cause re-entrancy in the alarm task and trigger watchdog timeouts.
Licensing
WinCC Runtime licenses are counted per alarm tag, not per side effect. Logging the same alarm to SQL does not require an additional license, but ensure that the SQL write does not run on the configuration station, where it would consume an RT-only seat.
Permissions on the WinCC service account
The default account for the WinCC Runtime service is the local SYSTEM account, which has no network credentials and therefore cannot use -E against a remote SQL Server. Either run the SQL Server on the same host, or change the WinCC service to run as a domain user with the right granted on the SQL Server (the ALTER ROLE db_datawriter line in the schema script above).
Related Siemens Documentation
The official WinCC V7 documentation covers GMsgFunction and the alarm logging action model. Refer to:
- Siemens Support entry 25678075 — "Determining message texts and process values at runtime for GMsgFunction"
- Siemens Support entry 23370769 — "SQL access from WinCC C-Script via ProgramExecute"
- WinCC V7.4 System Manual — section on Alarm Logging and user-defined actions
FAQ
Why does my VBScript action on a WinCC alarm fire when I save the project?
WinCC V7 re-evaluates tag and alarm actions during the project save path, not only at runtime. Any VBScript body that has a side effect (database write, file write, network call) will run during the save. Moving the body to a C-Script registered as GMsgFunction removes the save-time execution because the runtime alarm task is the only path that calls that function.
How do I write to SQL Server from a WinCC C-Script without using ADO?
Use ProgramExecute to launch sqlcmd.exe with a -Q query, or with -i pointing to a .sql file. This is the approach documented in Siemens Support entry 23370769 and is the simplest portable method, because it requires no custom DLLs and no changes to the WinCC install.
How do I get the alarm text, time, and process value inside GMsgFunction?
WinCC passes them as function parameters: lpszMsgText is the fully substituted text, dwTime (or pstTime in newer WinCC) is the event time, and the process value is already substituted into lpszMsgText through the @n%f@ token in the message configuration. No additional GetTagXxx call is needed inside the function.
Can I filter the alarm types that get written to SQL?
Yes. Inspect dwMsgNr against a list of message numbers you want to log, or inspect lpszObjectName to filter by class. The state bitmask dwState lets you keep only came in (0x0001) and went out (0x0002) events, ignoring acknowledgements.
What is the performance limit of this approach?
On a typical Windows Server, the process-per-event model sustains roughly 10 events per second before ProgramExecute overhead becomes visible. For higher throughput, buffer events to a local file inside GMsgFunction and dispatch them in batches with a scheduler-driven global C action, or replace ProgramExecute with a direct ODBC client in a custom DLL.