Files
wwtools/mxaccesscli/docs/usage.md
Joseph Doherty 2e937228a0 mxaccesscli: add read-batch / write-batch / subscribe-batch (JSONL input)
Three new subcommands that take a JSONL file (or '-' for stdin) and reuse a
single MxSession across all entries. The big win is in write-batch: a
two-phase pipeline (Advise all -> drain DataChange to resolve types; Write
all -> drain WriteComplete) reduces wall time from N x (resolve + write_ack)
to ~max(resolve) + ~max(write_ack). Measured 38.2s -> 10.3s (~3.7x) for
four writes against the ZB dev galaxy; the saving grows with N.

Per-item continue-on-error: parse errors are collected line-by-line and
abort with exit 2 before any LMX session opens; runtime failures (resolve
timeout, bad references, coerce errors, write timeouts) get their own
results[] row with a typed `error` string and exit 1. Auth flags mirror
`mxa write` and are resolved once before Phase A.

Shared infra:
  - Mx/JsonlInputReader.cs: lazy line reader (skips blank / '#' lines),
    bare-string or {"tag":"..."} for read/sub, {"tag","value","type"?} for
    write, with array-suffix consistency check at parse time.
  - Mx/ValueCoercion.cs: new CoerceJToken(...) wrapper preserves the
    single source of truth for type vocabulary.

Docs:
  - README run examples extended for each new command.
  - docs/usage.md: new "Batch input format" subsection (shared contract),
    one section per command with envelope examples and a full
    failure-mode table for write-batch, plus a "Batch commands -
    verified live" section capturing the 2026-05-10 ZB-galaxy run and
    pipelining-timing numbers.
  - test-fixtures/ holds the exact JSONL files used in the verified-live
    run so the doc numbers are reproducible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:34:46 -04:00

31 KiB
Raw Permalink Blame History

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). For Galaxy: references, follow the convention used in InTouch / Object Viewer.
  • --client <name> sets the client name passed to MxAccess Register(). Defaults to mxa. Most install logs key on this string.
  • Timeouts are per-call. They control how long the CLI waits for a OnDataChange (read) or OnWriteComplete (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 OnDataChange arrives 3-8 seconds after Advise(). Set timeouts and subscribe --seconds accordingly: a 3-second read may 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: 0 on success, 1 if any operation timed out or returned a non-Ok / non-Pending MxStatusCategory, 2 on argument-validation errors.

Batch input format

The three -batch commands (read-batch, write-batch, subscribe-batch) all take a single positional <input> argument which is either a path to a JSONL file or - to read JSONL from stdin. The line conventions are shared:

  • Blank lines and lines starting with # are skipped. Line numbers still count them, so error messages stay accurate against the source file.
  • Per-line auth override is not supported in v1. All entries in a write-batch use the same --username / --password / --user-id / --secured / --verifier-* flags supplied on the command line.
  • Duplicate tags are allowed. Each input line is an independent entry with its own item handle and its own row in results[].

Read / subscribe lines accept either a bare JSON string or an object form:

"TestMachine_001.Speed"
{"tag": "Reactor1.Level"}

Write lines are object form only:

{"tag": "TM.Setpoint",  "value": 42.5,                 "type": "double"}
{"tag": "TM.RunFlag",   "value": true}
{"tag": "TM.Parts[]",   "value": ["", "11111", ""],    "type": "string"}

value may be a JSON bool, number, string, or array. A JSON array requires the tag to end in [] (whole-array reference, matches the mxa write rule). type is optional; when omitted the JSON token's native type drives the default. When set, type accepts the same vocabulary as mxa write --type (bool, byte, short, int, long, float, double, string, datetime).

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: AuthenticateUser returned userId=1 for both the correct password and intentionally bad credentials (incl. an unknown username). This is consistent with a galaxy configured in Free Access mode 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 with galaxyAuthenticationMode enabled and attribute-level security classifications above Free 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 + AdviseUser_Name = <galaxy user>, User_Account = <galaxy domain>\<user>.
  • Anonymous + AdviseSupervisoryUser_Name typically 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 write invocation creates a fresh MxSession, 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 start daemon, or a calling script that wraps multiple commands in one MxSession), AuthenticateUser is called once and the same userId can flow into many subsequent Write calls.
  • The --user-id <N> flag is only useful when you're operating inside a session you already authenticated against — typically not from a one-shot mxa 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.

mxa read-batch <input>

Reads many tags from a JSONL file (or - for stdin) in one MxSession, amortizing the 38 s LMX bind cost across the whole batch. Line shape is defined in Batch input format. Result rows arrive in input-line order regardless of which OnDataChange fires first.

Option Default Notes
-t, --timeout <seconds> 5 Wall budget for the whole batch (not per-tag). Tags that haven't delivered a DataChange by the deadline are reported with error: "timeout".
--client <name> mxa Passed to Register().
--llm-json off Emit the JSON envelope.

Example:

mxa read-batch tags.jsonl --llm-json
echo '"TM.Speed"' | mxa read-batch - --llm-json

LLM-JSON envelope (one results row per non-blank input line, carrying the 1-based line for traceability):

{
  "query":   { "command": "read-batch", "input": "tags.jsonl", "timeout_s": 5.0, "client": "mxa" },
  "ok":      true,
  "results": [
    { "tag": "TM.Speed",      "line": 1, "ok": true, "value": 1234.5, "quality": 192, "timestamp": "...", "statuses": [...] },
    { "tag": "Reactor1.Level","line": 2, "ok": true, "value": 78.1,   "quality": 192, "timestamp": "...", "statuses": [...] }
  ]
}

Exit code: 0 if every entry resolved Ok; 1 if any timed out or returned a non-Ok status; 2 for parse errors, missing input, or empty input.

mxa write-batch <input>

Writes many tag/value pairs from a JSONL file (or -) in one MxSession, pipelining per-item resolve and write so wall time is roughly max(resolve_latency) + max(write_ack) instead of N × (resolve_latency + write_ack). Auth is resolved once before any item is touched.

Line shape: see Batch input format. Auth flags follow the same semantics as mxa write — see Authentication for how --username / --domain / --secured / --verifier-* interact. Per-line auth override is out of scope for v1 (planned follow-up).

Option Default Notes
-t, --timeout <seconds> 5 Per-phase wall budget. Phase A (resolve types) and Phase B (await OnWriteComplete) each get this many seconds.
--user-id <int> 0 Pre-resolved authenticated user id. See mxa write for caveats.
-u, --username <name> (none) Galaxy / OS username. Resolved to a userId via AuthenticateUser once before Phase A.
--domain <name> (none) Combined with --username as <domain>\<username>.
-p, --password <pwd> (none) Password for --username. Redacted in the LLM-JSON query echo.
--secured off Route writes through WriteSecured(currentUserId, verifierUserId, value). Required for Secured Write / Verified Write classifications.
--verifier-username / --verifier-domain / --verifier-password (none) Two-person verified write; implies --secured.
--client <name> mxa Passed to Register().
--llm-json off Emit the JSON envelope.

Example:

# Set up Hi/HiHi/Lo/LoLo limits and enable alarms for a tag in one shot:
mxa write-batch limits.jsonl --llm-json
{"tag":"BCD_T1.HighLimit",      "value": 80,  "type": "float"}
{"tag":"BCD_T1.HighHighLimit",  "value": 95,  "type": "float"}
{"tag":"BCD_T1.LowLimit",       "value": 20,  "type": "float"}
{"tag":"BCD_T1.LowLowLimit",    "value": 5,   "type": "float"}
{"tag":"BCD_T1.HighAlarm.Alarmed",     "value": true}
{"tag":"BCD_T1.HighHighAlarm.Alarmed", "value": true}
{"tag":"BCD_T1.LowAlarm.Alarmed",      "value": true}
{"tag":"BCD_T1.LowLowAlarm.Alarmed",   "value": true}

LLM-JSON envelope (per-entry row with auth attribution and line echo):

{
  "query":   { "command": "write-batch", "input": "limits.jsonl", "entries": 8,
               "timeout_s": 5.0, "user_id": 0, "verify_user": null, "secured": false, "client": "mxa" },
  "ok":      true,
  "results": [
    { "tag": "BCD_T1.HighLimit", "line": 1, "ok": true, "error": null,
      "authenticated": false, "auth_user_id": null, "secured": false,
      "verifier_user_id": null, "statuses": [...] }
  ]
}

Failure modes — write-batch

error string Cause Exit
(top-level) parse-error One or more lines failed JSONL / schema validation; results[] lists each. 2
(top-level) empty-input File / stdin contained no non-blank entries. 2
(top-level) authentication-failed AuthenticateUser returned 0 for the operator credentials. No items attempted. 1
(top-level) verifier-authentication-failed Same for the verifier in two-person Verified Write. 1
per-entry add-item-failed: … LMXProxyServer.AddItem threw (typically a malformed reference). 1
per-entry timeout-resolving-type No OnDataChange arrived for this item before --timeout elapsed. 1
per-entry type-resolution-failed First OnDataChange carried a non-Ok status — statuses filled. 1
per-entry value-coerce-failed: … JSON value couldn't be converted to the configured --type (or inferred type). The Write was not queued. 1
per-entry write-call-failed: … The Write / WriteSecured COM call itself threw before OnWriteComplete. 1
per-entry timeout No OnWriteComplete arrived before --timeout elapsed. 1
per-entry write-failed OnWriteComplete arrived with non-Ok statuses (e.g. read-only attr, security denied). 1

The process exits 1 if any per-entry row failed, 0 if every row was Ok. The envelope's top-level ok matches.

mxa subscribe-batch <input>

Subscribes to many tags from a JSONL file (or -) and streams OnDataChange events for a duration. The streaming output is identical to mxa subscribe — no envelope wrap; one event per line when --llm-json is set.

Line shape: see Batch input format.

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 event per line, no outer envelope.

First-event latency is still 38 s per tag, but Advise() is issued for every tag up front so the binds run in parallel — wall time matches a single-tag subscribe. For short --seconds windows (< 5 s) you may miss the initial values; budget accordingly.

Example:

mxa subscribe-batch tags.jsonl -s 30 --llm-json

Output stream (each event as one JSON line):

{"tag":"TM.Speed","ok":true,"value":1234.5,"quality":192,"timestamp":"...","statuses":[...]}
{"tag":"Reactor1.Level","ok":true,"value":78.1,"quality":192,"timestamp":"...","statuses":[...]}

Exit code: 0 on a clean run, 1 if every tag failed to subscribe (no item ever bound), 2 for parse errors / missing input / empty input.

Batch commands — verified live

Captured on 2026-05-10 against the dev ZB galaxy (System Platform 2017 Express, MxAccess 3.2.0.0). The galaxy is configured permissively (eNone / Free Access) so writes were issued anonymously.

Scenario Result
read-batch 3 entries (bare-string + object + duplicate, with blank/# lines) All three returned in input-line order; line field correctly reflected 1, 2, 5.
read-batch continue-on-error (NoSuchObject.NoSuchAttribute mixed with DevPlatform.CPULoad) Good entry returned value, bad entry returned Category=MxCategoryConfigurationError, Detail=6. Exit 1.
write-batch 5 entries (1 attribute with security_classification=5 failed SecurityError, Detail=1007; rest wrote anonymously) 4/5 wrote; per-item statuses preserved; exit 1. Round-trip read confirmed TuneValue=7.25, ProtectedValue=true, ProtectedValue1=false, MoveInPartNumbers[1]="PN-42".
write-batch failure-mode coverage (value-coerce-failed + type-resolution-failed + ok, same TuneValue tag duplicated) Each failure surfaced its distinct error string; duplicate tag entries are independent handles.
subscribe-batch two tags, 18-second window Both tags bound in parallel; CPULoad streamed ~26 OnDataChange events; SystemStartupTime delivered its initial value then stayed quiet (constant). Exit 0.
Parse error on stdin ({ malformed json }) results[] row with error: "parse-error", line + reason, no LMX session opened. Exit 2.
Missing input file Top-level error envelope, exit 2.

Pipelining timing

Same four writes (OtOpcUaParityTest_001.TuneValue, TestMachine_001.ProtectedValue, TestMachine_001.ProtectedValue1, MESReceiver_001.MoveInPartNumbers[1]) executed two ways, wall-clock:

Method Wall time Notes
4 × mxa write (sequential subprocess invocations) 38.2 s Each invocation pays the 38 s LMX bind once.
1 × mxa write-batch (4 writes in one MxSession, two-phase pipeline) 10.3 s Includes a 5th entry that failed SecurityError — still resolved in Phase A.

3.7× faster at N=4. The cost model is roughly `~max(resolve_latency)

  • ~max(write_ack)for the batched form versusN × (resolve_latency + write_ack)` for the sequential form, so the speedup grows with N: at ~10 writes the savings are typically 6080 seconds, which is the scale of the test-plan setup phases this command was added for.

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 via graccesscli 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. Use WriteSecured semantics (not yet exposed by this CLI) or target a Writeable_USC_* attribute.
  • Timeout — most likely the tag is genuinely silent (no value updates) or the reference is wrong. With --llm-json you'll see "error": "timeout" and an empty statuses.

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_dimension is 50 and you supply 25 values, after the write mxa 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 via mxa read '...[]' --llm-json first, or read it from array_dimension in ../../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:

  1. Read with [] and count the returned values (this is the runtime length, which may have been resized by a previous bulk write).
  2. Query the Galaxy Repository's ../../grdb/queries/attributes.sql — the array_dimension column reports the configured size from the template at deploy time.

Picking a tag for a smoke test

If the live galaxy is not familiar:

  1. Connect to the Galaxy Repository SQL — see ../../grdb/connectioninfo.md.
  2. Find a deployed instance with a writeable UDA — ../../grdb/queries/attributes.sql lists user-defined attributes with their data type. Filter on a Writeable_* security classification (see ../../aot/dev-guide/appendix-e-security-classifications.md).
  3. The reference for MxAccess is <InstanceName>.<AttributeName>.