Replaces the "Reusing a userId" subsection with a clearer "userId is session-scoped" note that captures the actual GRAccess/MxAccess contract: - AuthenticateUser returns an int that is bound to the LMX session hServer it was issued against, exactly like tag handles from AddItem. When the session unregisters (i.e. the MxSession is disposed, which happens when the process exits) the userId is no longer valid. - Each mxa write invocation creates a fresh MxSession, so an authenticated write must include --username/--password (or a userId resolved within the current process). - --user-id <N> is a hand-off for in-process callers, not a persistable credential. This matches the user's observation that userIds behave like tag/item handles in scope, and explains why batch flows benefit from a future session-mode mxa daemon (modeled on graccesscli's session) that holds one MxSession across many commands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
20 KiB
mxa — usage
Read, write, and subscribe to AVEVA System Platform tags via MxAccess. The CLI runs in-process: each invocation registers an LMXProxyServer, executes, and unregisters cleanly. Errors carry the underlying MxStatusCategory so an agent can decide whether the failure is transient (Pending), configurational, or operational.
Common notes
- Tag references are full attribute paths:
<ObjectName>.<AttributeName>(e.g.TestMachine_001.Speed). ForGalaxy:references, follow the convention used in InTouch / Object Viewer. --client <name>sets the client name passed to MxAccessRegister(). Defaults tomxa. Most install logs key on this string.- Timeouts are per-call. They control how long the CLI waits for a
OnDataChange(read) orOnWriteComplete(write). The default is 5 seconds. - First-event latency. LMX has to resolve the reference and bind to the hosting engine on each fresh client connection. Empirically the first
OnDataChangearrives 3-8 seconds afterAdvise(). Set timeouts andsubscribe --secondsaccordingly: a 3-secondreadmay legitimately time out on first contact, then succeed on the next try because LMX has cached the binding. - Subsequent events are fast. Once a tag is bound, value-change updates propagate within ~100 ms.
- Exit codes:
0on success,1if any operation timed out or returned a non-Ok / non-PendingMxStatusCategory,2on argument-validation errors.
mxa info
Print the loaded ArchestrA.MxAccess assembly identity, supported --type values, and the full MxStatusCategory enum. No tag access.
mxa info
mxa read <tag> [<tag>...]
Reads one or more tags by briefly subscribing and capturing the first OnDataChange per tag.
| Option | Default | Notes |
|---|---|---|
-t, --timeout <seconds> |
5 |
Per-tag timeout. Tags that don't deliver a DataChange within the window are reported with error: timeout. |
--client <name> |
mxa |
Passed to Register(). |
--llm-json |
off | Emit the JSON envelope. |
Examples:
mxa read TestMachine_001.Speed
mxa read TestMachine_001.Speed Reactor1.Level -t 3
mxa read TestMachine_001.Speed Reactor1.Level --llm-json
LLM-JSON envelope:
{
"query": { "command": "read", "tags": ["TestMachine_001.Speed"], "timeout_s": 5.0, "client": "mxa" },
"ok": true,
"results": [
{
"tag": "TestMachine_001.Speed",
"ok": true,
"value": 1234.5,
"quality": 192,
"timestamp": "2026-05-03T19:42:18.001",
"statuses": [
{ "Success": 0, "Category": "MxCategoryOk", "DetectedBy": "MxSourceRespondingAutomationObject", "Detail": 0 }
]
}
]
}
mxa write <tag> <value>
Writes one value to one tag and waits for OnWriteComplete.
| Option | Default | Notes |
|---|---|---|
--type <kind> |
inferred | Force the .NET type used for the boxed value. One of bool, byte, short, int, long, float, double, string, datetime. |
-t, --timeout <seconds> |
5 |
How long to wait for OnWriteComplete. |
--user-id <int> |
0 |
Pre-resolved authenticated user id passed straight to Write(). Use only when you already have a userId. 0 = unauthenticated. |
-u, --username <name> |
(none) | Galaxy / OS username. Combined with --domain (if set) into <domain>\<username> and resolved to a userId via AuthenticateUser before Write(). See Authentication. |
--domain <name> |
(none) | Domain or hostname for OS-authenticated galaxies. Combined with --username as <domain>\<username>. Omit for galaxy-authenticated logins. |
-p, --password <pwd> |
(none) | Password for --username. Redacted (***) in the LLM-JSON query echo. |
--client <name> |
mxa |
Passed to Register(). |
--llm-json |
off | Emit the JSON envelope. Includes authenticated and auth_user_id fields when --username was supplied. |
Type inference rules (when --type is not set): true/false/yes/no/on/off/1/0 → bool; pure integer → int (then long); decimals → double; everything else → string.
Examples:
mxa write TestMachine_001.Setpoint 42.5 --type double
mxa write TestMachine_001.RunFlag true
mxa write TestMachine_001.Label "Hello world"
mxa write Reactor1.Setpoint 100 --type int -t 10 --llm-json
The same JSON envelope shape as read, with results[0] containing { tag, ok, error?, statuses }. No value/quality/timestamp on the write result — consult a follow-up mxa read to confirm.
Authentication
Most galaxies require an authenticated userId to write attributes whose security classification is anything stricter than Free Access (i.e. Operate, Tune, Configure, Secured Write, Verified Write — see ../../aot/dev-guide/appendix-e-security-classifications.md).
The write command resolves credentials to a userId by calling LMXProxyServer.AuthenticateUser(verifyUser, password). The verifyUser string is composed from --username and --domain:
| Galaxy mode | Pass | Composes to |
|---|---|---|
osAuthenticationMode (Windows / domain users) |
--username dohertj --domain DESKTOP-6JL3KKO |
DESKTOP-6JL3KKO\dohertj |
galaxyAuthenticationMode (galaxy-internal users) |
--username dohertj (no --domain) |
dohertj |
| Mixed / AAD UPN | --username dohertj@example.com |
dohertj@example.com |
Example:
mxa write TestMachine_001.Setpoint 75.5 --type double `
--username dohertj --domain DESKTOP-6JL3KKO --password Sonamu89 `
--llm-json
A successful authentication populates two new fields in the JSON envelope's results[]:
{
"tag": "TestMachine_001.Setpoint",
"ok": true,
"authenticated": true,
"auth_user_id": 17, // returned by AuthenticateUser; 0 means failure
"statuses": [{"Category":"MxCategoryOk", ...}]
}
The human output appends (as <verify-user>, userId=N) to the success line so the right credentials are visible in interactive use.
Password handling
--password is redacted to *** in the LLM-JSON query echo and never logged in cleartext. It travels in-process from CliFx's argument parser straight into AuthenticateUser and is not persisted anywhere by the CLI.
Failure modes
| What you sent | What you get |
|---|---|
| Correct credentials, strict-mode galaxy | authenticated=true, auth_user_id > 0, write proceeds. |
| Bad password, strict-mode galaxy | auth_user_id == 0 → CLI exits 1 with "error": "authentication-failed". No write attempted. |
| Bad password, permissive-mode galaxy | The proxy returns a non-zero auth_user_id regardless. The CLI cannot tell this apart from a successful auth — it's the galaxy admin's responsibility to configure security strictly enough to reject. |
--username without --password |
Sends an empty password. Some galaxies allow this; most don't. |
⚠️ Verified behavior on the test galaxy used during development:
AuthenticateUserreturneduserId=1for both the correct password and intentionally bad credentials (incl. an unknown username). This is consistent with a galaxy configured inFree Accessmode where security checks are effectively disabled — the CLI's auth path is wired correctly, the galaxy just isn't strict. To exercise real authentication, target a galaxy withgalaxyAuthenticationModeenabled and attribute-level security classifications aboveFree Access.
Advise variant — operator vs supervisory
write picks how it subscribes to the destination attribute (the briefly-active subscription used for type resolution before the Write call) based on whether you supplied credentials:
--username supplied? |
Advise variant used | Audit-trail intent |
|---|---|---|
| Yes | LMXProxyServer.Advise |
Operator action — attribute the Write to the authenticated Galaxy user. |
| No (anonymous) | LMXProxyServer.AdviseSupervisory |
Supervisory action — attribute the Write to the hosting client (no Galaxy user claimed). |
This affects how System Platform records the action in the alarm/event subsystem and the Historian's Events table. On a strict galaxy with galaxyAuthenticationMode and real user records:
- Authenticated +
Advise→User_Name = <galaxy user>,User_Account = <galaxy domain>\<user>. - Anonymous +
AdviseSupervisory→User_Nametypically NULL or the supervisory client identity.
On a permissive galaxy (the development config used here), every action maps to DefaultUser regardless of advise variant — the mechanism is wired correctly but can't be differentiated until galaxy security is configured with real users. See Authentication above.
userId is session-scoped
The integer userId returned by AuthenticateUser(hServer, ...) is bound to the LMX session identified by hServer — the same way tag handles from AddItem are. Once the session unregisters (i.e. the MxSession is disposed, which happens when the process exits) the userId is no longer valid. There is no portable, long-lived "user id" you can persist across mxa invocations.
Practical implications for the CLI:
- Each
mxa writeinvocation creates a freshMxSession, so an authenticated write must include--username/--password(or pass a userId that was already resolved within the current process). - Within a single process (e.g. a future
mxa session startdaemon, or a calling script that wraps multiple commands in one MxSession),AuthenticateUseris called once and the sameuserIdcan flow into many subsequentWritecalls. - The
--user-id <N>flag is only useful when you're operating inside a session you already authenticated against — typically not from a one-shotmxa write. Treat it as a hand-off for in-process callers, not as a persistable credential.
AuthenticateUser itself is moderately expensive (SQL Server lookup + OS credential check). The CLI's per-invocation cost is dominated by the LMX bind / type-resolution stage, not by the auth round-trip — so re-authenticating on each mxa write is acceptable for interactive use. A future session-mode mxa would amortize it.
mxa subscribe <tag> [<tag>...]
Streams OnDataChange events for a duration.
| Option | Default | Notes |
|---|---|---|
-s, --seconds <seconds> |
10 |
Wall-clock duration of the subscription. |
--max <int> |
1000 |
Hard cap on emitted events. |
--client <name> |
mxa |
Passed to Register(). |
--llm-json |
off | JSON Lines mode — one JSON object per line, no outer envelope. |
Human output:
[INFO] Subscribed to 1 tag(s). Streaming for 30.0s. Ctrl-C to stop early.
[19:42:18.001] [OK ] TestMachine_001.Speed = 1234.5 (q=192)
[19:42:19.002] [OK ] TestMachine_001.Speed = 1245.7 (q=192)
...
[INFO] 30 event(s) emitted; subscription closed.
LLM-JSON output (one event per line, no surrounding [ ... ]):
{"tag":"TestMachine_001.Speed","ok":true,"value":1234.5,"quality":192,"timestamp":"2026-05-03T19:42:18.001","statuses":[{...}]}
{"tag":"TestMachine_001.Speed","ok":true,"value":1245.7,"quality":192,"timestamp":"2026-05-03T19:42:19.002","statuses":[{...}]}
JSON Lines lets a downstream consumer parse events incrementally rather than buffering the whole stream — the right shape for indefinite or long-running subscriptions.
Type support matrix
Verified end-to-end against the live ZB galaxy (System Platform 2017 Express, MxAccess 3.2.0.0). Each row records what the wire shape looks like in the JSON envelope.
MxDataType |
Read | Write | JSON shape | Notes |
|---|---|---|---|---|
MxBoolean |
✅ | ✅ | JSON true / false |
--type bool accepts true/false/1/0/yes/no/on/off. |
MxInteger (Int32) |
✅ | ✅ | JSON number | --type int. Up-cast to long if it overflows int.MaxValue. |
MxFloat (single) |
✅ | ⚠️ | JSON number | Read verified on DevPlatform.CPULoad family. Write requires a writeable Float UDA — none in the test galaxy, but --type float is wired. |
MxDouble |
❓ | ❓ | JSON number | No accessible Double instance in the test galaxy. Wiring is identical to Float; expected to work. |
MxString |
✅ | ✅ | JSON string | Default inferred type for non-numeric values. |
MxTime (DateTime) |
✅ | ⚠️ | JSON string "YYYY-MM-DDTHH:mm:ss" |
Read verified on DevPlatform.SystemStartupTime. Write via --type datetime accepts ISO-8601. |
MxElapsedTime |
❓ | ❓ | JSON number (seconds) | No accessible instance in the test galaxy. |
MxReferenceType |
✅ | – | JSON string (target object's Tagname) |
E.g. TestChildObject.Container → "DevTestObject". Writing references is not exposed by the CLI. |
MxQualifiedEnum (13) |
❓ | – | (likely string) | No accessible instance. |
MxQualifiedStruct (14) |
– | – | – | Access via dotted member names: <obj>.<struct>.<field>. |
MxInternationalizedString (15) |
❓ | ❓ | (likely string) | No accessible instance. |
MxBigString (16) |
❓ | ❓ | JSON string | No accessible instance. |
Array (any type), bulk read/write via [] |
✅ | ✅ | JSON array of element type | Reference syntax <obj>.<arrayAttr>[] — empty square brackets. Read returns the entire array as a single value. Write takes one positional value per element (mxa write '<obj>.<arr>[]' v1 v2 v3 ...). A bulk write resizes the array to the count provided (verified: 50 → 25 → 50 round-trip on MoveInPartNumbers). |
| Array (bare reference) | ❌ | ❌ | — | The plain <obj>.<arrayAttr> (no brackets) returns MxCategoryCommunicationError, Detail=1003. Always use [] for bulk operations. |
| Array element by index | ✅ | ✅ | scalar of element type | Reference syntax <obj>.<arrayAttr>[<n>]. 1-based, runs from [1] to [NumElements]. [0] is invalid. |
Legend: ✅ verified live, ⚠️ wiring present but no live instance to write, ❓ wiring present but no live instance found, ❌ not supported by MxAccess at this layer, – not applicable.
To test write support for a type, use ../../grdb/ to find a deployed instance whose dynamic_attribute.mx_data_type matches and whose mx_attribute_category is in (2-11, 24) (Writeable_* family).
Errors and statuses
Every result carries a statuses array — the elements of the COM MXSTATUS_PROXY[] MxAccess passes back. Field names match the C# struct exactly:
| Field | Type | Meaning |
|---|---|---|
Success |
int16 | 0 = Ok, non-zero = error code |
Category |
enum | MxCategoryOk, MxCategoryPending, MxCategoryWarning, MxCategoryCommunicationError, MxCategoryConfigurationError, MxCategoryOperationalError, MxCategorySecurityError, MxCategorySoftwareError, MxCategoryOtherError, MxStatusCategoryUnknown |
DetectedBy |
enum | MxSourceRequestingLmx, MxSourceRespondingLmx, MxSourceRequestingNmx, MxSourceRespondingNmx, MxSourceRequestingAutomationObject, MxSourceRespondingAutomationObject, MxSourceUnknown |
Detail |
int16 | Additional error-code detail |
A result is considered ok only if every statuses element has Category in (MxCategoryOk, MxCategoryPending).
Common failure shapes:
Category: MxCategoryConfigurationError— usually a typo'd reference or the attribute doesn't exist on the deployed instance. Sanity-check viagraccesscli object snapshot.Category: MxCategoryCommunicationError— engine isn't running, object is OffScan, or LMX can't reach the platform hosting the object.Category: MxCategorySecurityError— secured attribute,--user-id 0. UseWriteSecuredsemantics (not yet exposed by this CLI) or target aWriteable_USC_*attribute.- Timeout — most likely the tag is genuinely silent (no value updates) or the reference is wrong. With
--llm-jsonyou'll see"error": "timeout"and an emptystatuses.
Reading arrays
MxAccess accepts two reference forms for arrays — pick by what you need:
Whole array — <obj>.<arrayAttr>[] (empty brackets)
mxa read 'MESReceiver_001.MoveInPartNumbers[]' --llm-json
Returns the full array as a single JSON value:
{
"tag": "MESReceiver_001.MoveInPartNumbers[]",
"ok": true,
"value": ["", "11111", "", "", /* ... 50 elements total ... */],
"quality": 192,
"statuses": [{"Success":-1,"Category":"MxCategoryOk", ...}]
}
The array is fixed-length (sized at deploy time per the template's array_dimension). Empty string elements are unset slots, not gaps.
Single element — <obj>.<arrayAttr>[N]
mxa read 'MESReceiver_001.MoveInPartNumbers[2]' --llm-json
Indices are 1-based: [1] is the first element, [NumElements] is the last. [0] is invalid. Single-element reads are also writeable: mxa write '<obj>.<attr>[N]' <value>.
Whole array write — also via []
Pass one positional value per element after the tag. The CLI bundles them into a strongly-typed array (string[], int[], bool[], …) before writing.
# Write a 50-element string array
mxa write 'MESReceiver_001.MoveInPartNumbers[]' \
"" "11111" "" "" "" "" "" "" "" "" \
"" "" "" "" "" "15" "" "" "" "" \
"" "" "" "" "" "" "" "" "" "" \
"" "" "" "" "" "" "" "" "" "" \
"" "" "" "" "" "" "" "" "" ""
# Write a typed array
mxa write 'SomeObj.SomeFloats[]' 1.0 2.5 3.14 --type float
⚠️ A bulk write resizes the array to the count provided. If the configured
array_dimensionis 50 and you supply 25 values, after the writemxa read '...[]'returns 25 elements, not 50. The trailing slots are deallocated, not zero-filled. Always supply the full element count when you want to preserve the array's logical size — fetch the current count viamxa read '...[]' --llm-jsonfirst, or read it fromarray_dimensionin../../grdb/queries/attributes.sql.Mixing scalar / array forms is guarded: passing multiple values without
[]exits 2 with a clear error message.
What does not work
mxa read 'MESReceiver_001.MoveInPartNumbers' # bare ref, no brackets
# → MxCategoryCommunicationError, Detail=1003
The plain reference (no [], no [N]) is rejected by the proxy on both read and write. Always include the brackets — empty for whole-array, indexed for element.
Discovering array length
The CLI doesn't (yet) auto-discover element count. Two ways to find it:
- Read with
[]and count the returned values (this is the runtime length, which may have been resized by a previous bulk write). - Query the Galaxy Repository's
../../grdb/queries/attributes.sql— thearray_dimensioncolumn reports the configured size from the template at deploy time.
Picking a tag for a smoke test
If the live galaxy is not familiar:
- Connect to the Galaxy Repository SQL — see
../../grdb/connectioninfo.md. - Find a deployed instance with a writeable UDA —
../../grdb/queries/attributes.sqllists user-defined attributes with their data type. Filter on aWriteable_*security classification (see../../aot/dev-guide/appendix-e-security-classifications.md). - The reference for MxAccess is
<InstanceName>.<AttributeName>.