# 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:

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

Write lines are object form only:

```jsonl
{"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.

```powershell
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:

```powershell
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:

```json
{
  "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](#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:

```powershell
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`](../../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:

```powershell
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[]`:

```jsonc
{
  "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 + `Advise` → `User_Name = <galaxy user>`, `User_Account = <galaxy domain>\<user>`.
- Anonymous + `AdviseSupervisory` → `User_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`](#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:

```text
[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 `[ ... ]`):

```jsonl
{"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 3–8 s LMX bind cost across the whole batch. Line shape is
defined in [Batch input format](#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:

```powershell
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):

```json
{
  "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](#batch-input-format). Auth flags follow
the same semantics as `mxa write` — see [Authentication](#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:

```powershell
# Set up Hi/HiHi/Lo/LoLo limits and enable alarms for a tag in one shot:
mxa write-batch limits.jsonl --llm-json
```

```jsonl
{"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):

```json
{
  "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](#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 3–8 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:

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

Output stream (each event as one JSON line):

```jsonl
{"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 3–8 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 versus `N × (resolve_latency +
write_ack)` for the sequential form, so the speedup grows with N: at ~10
writes the savings are typically 60–80 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/`](../../grdb/) to find a deployed instance whose `dynamic_attribute.mx_data_type` matches and whose `mx_attribute_category` is in `(2-11, 24)` ([Writeable_*](../../aot/dev-guide/appendix-f-attribute-categories.md) 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)

```powershell
mxa read 'MESReceiver_001.MoveInPartNumbers[]' --llm-json
```

Returns the full array as a single JSON value:

```jsonc
{
  "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]`

```powershell
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.

```powershell
# 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`](../../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

```powershell
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`](../../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`](../../grdb/connectioninfo.md).
2. Find a deployed instance with a writeable UDA — [`../../grdb/queries/attributes.sql`](../../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`](../../aot/dev-guide/appendix-e-security-classifications.md)).
3. The reference for MxAccess is `<InstanceName>.<AttributeName>`.
