Working reference for reading and generating LabVIEW code through the lvai.LVAI
gRPC interface. Everything here was derived empirically from a corpus of 13 exports of
shipping LabVIEW example VIs (LabVIEW 2026), plus round-trip validation of each. NI
publishes no schema — there is no XSD anywhere in the addon — so treat this as observed
behaviour, not documented contract, and re-derive it after a LabVIEW upgrade.
Names of concrete projects, libraries and products are deliberately excluded; all examples below use neutral placeholders.
| Topology (nodes, wires, nesting) | yes — this is the whole point |
| Terminal-level wiring | yes, by uid.terminal reference |
| Control/indicator types, defaults, connector pane | yes |
| Comments | yes, FreeLabel |
| Positions, sizes, layout | no — no coordinate attribute exists at all |
| Colours, fonts, decorations | no |
| Terminal display mode (View As Icon) | no — see the measurement below |
Generated VIs show front-panel terminals as large icons. That is a LabVIEW option (Tools → Options → Block Diagram → "Place front panel terminals as icons"), on by default, applied when the generator creates the terminals — not something AIXML carries.
Measured, both directions:
- A generated VI's terminals were switched to the small representation by hand in LabVIEW.
The diagram visibly changed — large framed icons became
DBL/abc/[DBL]stubs. - Re-exporting that VI produced an AIXML file attribute-identical to the authored input.
No new attribute, no
styleaddition, nothing.
So the property is real in the VI and absent from the format. Two consequences: you cannot request the small representation in AIXML, and a manual fix is lost the moment the VI is regenerated. Turn the LabVIEW option off if you want it to stick.
This is the same shape as the empty-export trap in §11 — the export not changing does not mean
the VI did not change. Render the diagram (--diagram) when the question is what a VI looks
like.
Since there are no coordinates, LabVIEW places FreeLabels itself. It does not stack
them — it spreads them out — but it also does not avoid collisions: long labels end up
lying across wires and hiding constants. The only levers you have are how many and
how long.
Measured on one generated VI: six labels of 40–70 characters produced a diagram where a
string constant was fully covered and the stop terminal was obscured. Rewriting the same
VI with three labels of 12–20 characters, and moving the prose into the VI description
(where it becomes Context Help), halved the diagram area and left every object visible.
So: keep diagram comments to a few words, and put explanations in description.
Verify by exporting the rendered diagram — see --diagram in the README.
An XML comment is not a place to put anything — it breaks validation. <!-- … --> anywhere
inside the document makes ValidateAIXML answer
Error 42 occurred at LV AI Core.lvlibp:VI generator.vi
LabVIEW: (Hex 0x2A) Generic error.
and that is the whole message: no line, no column, no element, nothing pointing at a comment.
Measured 2026-08-13 by removing the comment and nothing else — the same file then validated at
errorCode 0. This is the harshest failure in the format so far, because the well-formedness
errors it does diagnose are precise to the column (Line 5, Column 308, missing required attribute 'outputs'), so a bare generic error reads as a deep structural fault and sends you
rewriting the diagram. It cost one such detour in the run that found it.
The three places a note can legitimately go: a FreeLabel on the diagram, the comment
attribute, or the description of the VI or a terminal. Nowhere else. §11 carries the same
finding under the error message, since that is where somebody holding an Error 42 will look.
The attribute vocabulary, measured over every shipping example rather than over the original
13 (LabVIEWMCP --corpus, then attributes.tsv from the report):
_id _name adapt aggregate async comment concat cond conIdx connection convertEol
count description dimensions element elements fields ignoreAttributes includeHigh
includeLow inputs instance inversions items label link maxin maxout mode
operation outputs readLines selectin selectout selector strict style target
text type uid uid_parent utf8 value values
The two newest entries above were not in this list before 2026-08-22, and they are the whole
of what an add-on upgrade changed about the format. Measured that day, after NI's AI add-on was
reinstalled (the 26.3 tree's binaries redated to 2026-08-13, which moved the fingerprint
LvaiVersion keys the export cache to and dropped 2 382 cached exports):
async="true" |
on <Node>, on VISA Write / VISA Read. VISA's synchronous-vs-asynchronous I/O setting, previously not carried at all |
utf8="true" |
on <VI> and on <Node> (Invoke and Property nodes). Marks UTF-8 string content |
Why this is an addition rather than a wider net: the inventory above is generated by sweeping
every shipping example — 1 677 VIs — and aixml_corpus_report.py tallies attributes from every
successful export, not only from VIs that survive a round trip. So GPIB with VISA functions.vi
and Multilingual Temperature Monitor.vi were both in the pre-update sweep and neither attribute
was found. Re-exporting those same VIs after the update produces both. Nothing else moved: the
element vocabulary is unchanged, and five vi.lib VIs exported before and after the update came
back byte-identical.
The lesson for the next upgrade is that LvaiVersion's cache drop is necessary but not
sufficient — it protects stale exports, it does not tell you the vocabulary grew. Re-run
LabVIEWMCP --corpus and diff attributes.tsv against this list, because two attributes appearing
in NI's own exports are two attributes a generated VI is silently not carrying.
Eighteen of those were missing from the list this document carried before the sweep, among
them elements (how Array To Cluster fixes its output size), dimensions, operation,
aggregate, link, strict and text. Several — adapt, instance, concat, convertEol,
readLines, items, values — are described elsewhere in this file yet were absent here, so the
list was never a reliable place to check whether an attribute exists. It is now generated rather
than remembered; regenerate it after a LabVIEW upgrade. Conversely scope appears in the old list
and in no export, so treat it as unconfirmed.
No x, y, left, top, bounds. A comment can be added but never placed, and a
diagram cannot be tidied through this format. Beware a false positive when grepping for
layout: conIdx= contains the characters x=.
<VI _name="Example.vi" description="One line, then details.">
<FreeLabel comment="Free-standing comment" uid="9001" uid_parent="root"/>
<Control _name="a" conIdx="0" connection="required"
outputs="value:43.value" type="double" uid="43" uid_parent="root" value="0"/>
<Node _name="Add" inputs="x:43.value,y:57.value"
outputs="x+y:71.x+y" uid="71" uid_parent="root"/>
<Indicator _name="sum" conIdx="4" connection="recommended"
inputs="value:71.x+y" type="double" uid="88" uid_parent="root" value="0"/>
</VI>- Root element is
VI. No XML declaration, no namespace. descriptiononVIis mandatory, even when it is the only attribute besides_name. Omitting it fails validation, not generation:lvai_validate_aixmlanswerserrorCode 1withError -2628 … missing required attribute 'description'. It may be any non-empty string. So the smallest legal document — and the way to generate an empty VI — is a single self-closing element with no children:That was validated and generated a real, openable 4.9 kB VI. Note the asymmetry with export: a read that returns only a bare<VI _name="Empty.vi" description="Empty VI."/>
<VI …/>means the diagram could not be parsed (§11), but a bare<VI …/>as input legitimately means "no diagram".- LabVIEW writes CRLF line endings and two-space indentation. Neither appears to be required, but matching them keeps diffs against fresh exports readable.
- Document order carries no meaning and is not preserved. On export LabVIEW groups by
kind —
FreeLabels first (in reverse of the authored order), then controls, constants and indicators, then nodes. Since there are no coordinates either, position on the diagram is decided entirely by LabVIEW. connectionwithout aconIdxis dropped on export: a terminal only counts as connector-pane-assigned when it has an index.conIdxIS a position, and the map is knowable — see "The connector pane" below. An earlier revision of this line claimed there was "no fixed map to memorise" and told the reader to copy a set of numbers from some other VI. That was wrong, and it produced badly styled VIs: numbering depends on the pattern, but within a pattern each index is a fixed rectangle, and the geometry is readable through VI Server._nameonVIshould match the target file name. LabVIEW overwrites it with the real file name on export, so a mismatch is at best ignored.valueis required on everyControlandIndicator, including an error cluster, where the literal is[false,0,]— the trailing empty string is written as nothing at all, not as"". Counted: 5 occurrences in the corpus and 2 in a freshly generated VI's re-export, and no instance of[false,0,""]anywhere. (An earlier revision of this line claimed the""form; it was written from memory rather than from an export, and""would give asourcecontaining two literal quote characters — §6 takes a string element in avalueliterally.) Omittingvaluefails validation, which is cheap; the expensive part is the case of what you put in it. A boolean literal must be exactly lowercasetrue/false:TRUEgenerates without complaint and runs as false (§11).inputsis required on anIndicatortoo, even an unwired one, where it readsinputs="value:"— the empty-net form used for any unwired terminal (§8).
A generated VI can be functionally perfect and still be wrong, because conIdx decides where on
the connector pane a terminal sits — and a reviewer sees that before anything else. This section
exists because several generated VIs put their inputs on the right-hand edge and their error
terminals at the top, which is exactly what
NI's style guide
tells you not to do.
The rules, from NI: inputs on the left, outputs on the right, error in at the
bottom left and error out at the bottom right, and terminals arranged so that wires do
not have to cross to reach them.
The pattern is a STATION SETTING. It is not yours to choose per VI, and it is not the generator's
choice either — LabVIEW gives every new VI the default pane from LabVIEW.ini:
[LabVIEW]
DefaultConPane="4833"
That key, read from the LabVIEW.ini next to LabVIEW.exe, overrides everything — never assume
a pattern when that file can be read, and it is why everything generated on this station comes out
16-terminal. If the key is absent, LabVIEW's factory default applies: 4815, the 12-terminal
4x2x2x4. So the pattern of a new VI is knowable before you generate — just not from
anything inside AIXML or the VI Server API. lvai_connector_pane with no argument reads it for you.
The file is read-only to us: read it, quote it, never write a key to it.
Take the whole style-guide block that call prints — not four numbers. This paragraph said "prints
the four conIdx values to write", and that phrasing is itself the cause of a bug that shipped three
times: the tool prints six entries — first input, more inputs, error in, first output,
more outputs, error out — and the two more rows are the ones a reader drops. A VI with two
data inputs then gets its first on conIdx 0, correctly, and its second on conIdx 1, because 1
follows 0. On 4833 the left edge is 0, 5, 7, 9; 1 is the top of the second column, a middle
slot. lvai_connector_pane with viPath catches it and forces a regeneration, so the cost is about
30 s per occurrence rather than a defect — but never derive the next index by adding one. On 4815
the left edge counts down: 11, 10, 9.
Three revisions of this section were wrong before that was known, and the shape of the error is
worth keeping. The first said a generated VI "essentially always gets 4815" and called that map "a
constant"; the second said the pattern was "chosen by the highest conIdx you use"; the third said
it could not be predicted at all and had to be measured every time. All three were attempts to find
a rule inside LabVIEW for something that was sitting in a text file:
| VI | highest conIdx |
terminals assigned | pattern |
|---|---|---|---|
| three early probes | 3 / 7 / 11 | up to 12 | 4815 — the factory default, before the key was set |
| a fourth probe | 15 | 12 | 4833 |
DaqReadAndTDMS.vi, 2026-08-13 |
11 | 5 | 4833 |
| the same VI, re-indexed | 15 | 5 | 4833 |
The index set never mattered. What it cost: DaqReadAndTDMS.vi was generated with the set the first
revision prescribed — first input 11, error in 8, first output 3, error out 0 — and landed
on 4833, where those four indices mean right edge, right edge, middle column and top-left
corner. Two of its inputs sat on the output edge and error out sat top left. Validation was clean,
the VI ran, and the person who asked for it rejected it on sight.
Read the setting for a new VI; measure the pane for an existing one. The two questions are not
the same. A VI you are about to create gets the station default. A VI that already exists carries
whatever pane it was given — on another machine, under another setting, possibly rotated or flipped —
so there viPath is the only honest answer.
Measured through {LV.VI} → read+Connector Pane\3AReference → {LV.ConnectorPane} →
read+Terminal Bounds[], one rectangle per index on a 32×32 pane:
pattern 4815 — 12 terminals, 4x2x2x4 |
conIdx, top → bottom |
|---|---|
| left edge | 11, 10, 9, 8 |
| second column | 7 (upper), 6 (lower) |
| third column | 5 (upper), 4 (lower) |
| right edge | 3, 2, 1, 0 |
pattern 4833 — 16 terminals, 5x2x2x2x5 |
conIdx, top → bottom |
|---|---|
| left edge | 0, 5, 7, 9, 11 |
| middle columns | 1, 2, 3 (upper row), 12, 13, 14 (lower row) |
| right edge | 4, 6, 8, 10, 15 |
So the same number means opposite things in the two patterns: 0 is bottom-right in 4815 and
top-left in 4833; 8 is bottom-left in 4815 and third down the right edge in 4833. 4815
numbers right-to-left and bottom-to-top, which is the rule the LabVIEW Wiki gives for the default
pane. 4833 does not follow it: it takes the four corners first — 0 top left, 4 top right, 11
bottom left, 15 bottom right — and then zig-zags down the edges, left, right, left, right.
There are two families, and they are interleaved across the id range — which is the measurement
that kills the idea of deriving a third pattern from a rule. Of the 31 measured, 19 number
right-to-left/bottom-to-top like 4815 (its first input is the highest index), 10 take the corners
first like 4833 (first input 0), and 2 have no edges at all. The families do not split by id or by
size: 4820–4824 are corner-first while 4825–4829 are not, and 4809 with six terminals is
corner-first while 4812 with eight is not. Neither the id nor the terminal count predicts which
scheme a pattern uses.
Per pattern, then, NI's style guide comes out as:
| 4815 | 4833 | |
|---|---|---|
| first input | 11 |
0 |
| second input | 10 |
5 |
error in (bottom left) |
8 |
11 |
| first output | 3 |
4 |
error out (bottom right) |
0 |
15 |
So do not carry these numbers in your head, and do not read them out of this document either —
ask lvai_connector_pane. That is what it exists for, and it is the reason the two tables above
are examples rather than instructions:
- with no argument it reads this station's
DefaultConPaneand prints the fourconIdxvalues a newly generated VI needs. This is the call to make before authoring anyconIdx. - with
viPathit measures that VI's pane, joins it with the VI's own export, and answers with the slot map, every breach of the style guide, and theconIdxeach terminal should have — ready to paste. Read-only, about 1 s. - with
patternit serves one pattern's measured map without touching LabVIEW. - with neither it lists all 36 patterns and which of them have been measured.
The procedure for a new VI is therefore: call the tool with no argument, write the conIdx values
it gives you, generate — then call it with viPath to confirm. The confirmation is not ceremony:
the station setting tells you the pattern, not that you transcribed it correctly, and it says nothing
about a VI that already existed.
Where the tool's table comes from, and why it has holes. 32 of the 36 patterns have measured
geometry, harvested by sweeping 1 449 VIs of this installation with scripts/lvpane_sweep.xml and
building the table with LabVIEWMCP --panes. It cannot be completed by brute force, because
{LV.ConnectorPane} → Pattern is read-only — there is no setter anywhere in the 3 078-entry
VI Server catalogue — so a pattern is only observable on a VI that already uses one. 4816, 4818, 4819
and 4830 had no such VI in the sweep; the tool says so rather than guessing, and sweeping more VIs is
what closes the gap — 1 683 VIs across vi.lib, LVAddons, examples, user.lib, instr.lib, project,
resource and this machine's own code did not, so those four are simply unused here.
Do not reach for DefaultConPane to close the gap. Changing it would make new VIs use a chosen
pattern, so it looks like the missing measurement — and LabVIEW.ini is read-only for this
server and everything built on it, by the station owner's rule. Read the key, quote it, never write
it. A pattern nothing in 1 683 VIs uses does not need a map badly enough to touch a machine's
configuration.
A pattern id does not pin the orientation, and that is measured too. A pane can be rotated or
flipped (Rot90, FlipHoriz, FlipVert on {LV.ConnectorPane}), and then the same id numbers its
slots along other edges. Over 1 449 VIs, 8 of the 32 measured patterns turned up in two
orientations — 4815 appears 1 022 times upright and 4 times on its side, in VIs such as
XML Script - CompoundArithmetic.vi. The table therefore stores the majority orientation and
carries the variant count, and the tool says "CAUTION: n distinct ORIENTATIONS" on those rows.
This is also why the harvest counts instead of keeping the first VI it sees, which is what it used to
do: harvesting the same six sweeps in a different file order flipped 4829 to its minority orientation
and quietly changed four published conIdx values. A measurement of the VI in hand — viPath —
never has this problem, because it reads that VI's own bounds, rotation and all.
Two smaller measurements worth keeping, both from that sweep. A slot can span two columns —
4817 and 4820 each have one — and it still belongs to the edge it touches, which is why the
classification tests Left == 0 and Right == width rather than counting columns. And the
LabVIEW Wiki's shape strings are not column profiles: measured, 4817 is 2x3x2 where the wiki
writes 3x2x2, and 4820 is 3x2x3x2 against 3x2x2x3. Same sums, different order, so its
notation counts something else; 4833's row does not even sum to its own terminal count. The tool
reports the measured profile and keeps the wiki's string beside it as a name only.
One further caveat, and it is the same trap from the other side: hand-written NI VIs use other
patterns still (4800–4835 exist), so a set of indices copied from one of them means
something else in yours: Close File+.vi is pattern 4812 (8 terminals, left edge 4, 0, right
edge 7, 3), where its error in = 0 and error out = 3 are the same bottom-left/bottom-right
convention, not a different one. That is why NI's numbers look inconsistent across VIs and are not.
Terminal Bounds[] is indexed by exactly the AIXML conIdx — proven rather than assumed. A
probe VI with indicators on conIdx 0–5 and controls on 6–11 was read back through
{LV.ConnectorPane} → read+Controls[] and {LV.Control} → read+Indicator, one per index, and
returned TTTTTTFFFFFF. Reading an unassigned slot gives Error 1055 (invalid reference), so a
reader has to tolerate holes.
To check a finished VI, print it: Print.VI To HTML (see scripts/lvdoc_print.xml) renders the
pane with each terminal labelled name [conIdx]. Beware the one thing that render does not
show: it always draws inputs on the left and outputs on the right regardless of where they
actually sit, so a badly placed terminal looks fine there — the wire routing into the icon is the
only visible tell. The bounds are the reliable check.
LabVIEW's own Context Help window has the same blind spot, which is worth knowing because it is
where a user looks first. On DaqReadAndTDMS.vi it drew TDMS File Path [10] and error in [8]
down the left with error out [0] on the right — the roles, not the slots, all five of which were
somewhere else entirely. What it does print is the pattern id, in parentheses after the VI path
(C:\Temp\DaqReadAndTDMS.vi (4833)), and that number is the fastest way to spot that a generated
pane is not the 12-terminal one you assumed.
Every element carries a uid (unique within the document) and a uid_parent
naming its container — root for the top-level diagram, otherwise the uid of the
enclosing Structure or CaseFrame.
Wires are not separate elements. Instead:
inputs = "myTerminal:netName, myOtherTerminal:netName, ..."
outputs = "myTerminal:netName, ..."
A uid.terminal string is the name of a net (a wire), not a pointer to an element.
Each element lists, for every one of its terminals, which net that terminal hangs on —
input terminals in inputs, output terminals in outputs. Two terminals are wired
together precisely when they name the same net.
This is the single most important rule, and the obvious guess ("inputs points at the
source element") is wrong. Worked example — a shift register feeding two consumers:
<Left inputs="value:" outputs="value:154.x" uid="100" uid_parent="104"/>
<Node _name="Increment" inputs="x:154.x" outputs="x+1:154.x+1" uid="154" uid_parent="85"/>
<Node _name="Index Array" inputs="array:91.value,index:154.x" outputs="element:142.element" uid="142" uid_parent="85"/>Net 154.x has three terminals on it: the shift register's left output, Increment.x,
and Index Array.index. Note that Increment names net 154.x in its own inputs
even though 154 is its own uid — the net simply happens to be named after that terminal.
Fan-out is expressed by repeating the net string, never by duplicating elements.
Consequences for authoring:
- Net names are only labels. LabVIEW picks a representative endpoint when exporting, and it is not always the driver — so do not try to derive the name from the source. What must hold is consistency: every terminal on a net spells it identically.
- An empty right-hand side means unwired —
outputs="floor(x/y):"says the terminal exists but carries no wire. Omitting the terminal entirely is not the same thing. - Exactly one output terminal should drive a net; several inputs may read it.
Terminal names are the literal LabVIEW terminal labels, symbols included. Real examples
from the corpus: x+y, x-y*floor(x/y), size(s), error out, dup Message Queue.
Get one wrong and validation reports Object terminal not found for input: ....
uids you author are symbolic — treat them as local labels. In one generation they were renumbered (10, 11, 12, 13 came back as 43, 57, 71, 88); in another, widely spaced values (10, 20, 100, 110, …) survived unchanged. Do not depend on either behaviour; only internal consistency matters. Widely spaced values leave room to insert elements later.
| Element | Purpose | Attributes |
|---|---|---|
VI |
root | _name, description |
Control |
front-panel input | _name, type, value, conIdx, connection, style, description, items, values, outputs |
Indicator |
front-panel output | same, but inputs instead of outputs |
Constant |
diagram constant | _name, type, value, outputs |
FixedConst |
fixed terminal (e.g. loop iteration) | _name, outputs |
Node |
primitive / palette function | _name, inputs, outputs, fields, element, type |
Call |
subVI call | target, instance, adapt, inputs, outputs |
Structure |
loop / case / event container | _name, count, label, selectin, maxin, maxout |
CaseFrame |
one frame of a case or event structure | selector, selectout, label |
Diagram |
one frame of a disable structure | selector, label |
Tunnel |
wire crossing a structure border | _id, inputs, outputs, mode, cond |
Condition |
loop stop/continue terminal | inputs, value |
ShiftReg |
shift register, wraps Left + Right |
— |
Left / Right |
shift-register terminals | inputs, outputs |
FreeLabel |
comment | comment |
Enumerated values observed:
connection:required·recommended·optionalTunnel._id:In1…InN,Out1…OutN— numbering is per structure and matters only for pairing input to output sideTunnel.mode:index(auto-indexing on a loop border); absent means plain tunnelTunnel.cond:trueseen on an event-structure border tunnelControl.style:latched(mechanical action of a boolean)Structure._name:While Loop·For Loop·Case Structure·Event Structure·Flat Sequence Frame(each frame is its ownStructure, there is no parent element)
An easy trap when walking the tree: the two disable structures do not use
Structure/CaseFrame. They appear as a Node whose children are Diagram elements.
<Node _name="Diagram Disable Structure" uid="19338" uid_parent="root">
<Diagram selector=" Disabled " uid="19361" uid_parent="19338"> ... </Diagram>
<Diagram selector=" Enabled " uid="19372" uid_parent="19338"> ... </Diagram>
</Node>
<Node _name="Conditional Disable Structure" uid="1184" uid_parent="root">
<Diagram label="functionality not available in exe"
selector=" RUN_TIME_ENGINE==False " uid="2425" uid_parent="1184"> ... </Diagram>
<Diagram label="" selector=" Default " uid="12071" uid_parent="1184"> ... </Diagram>
</Node>Diagram carries selector (with the same leading/trailing spaces as a CaseFrame
selector), an optional label, uid and uid_parent. Code that only recognises
CaseFrame will silently skip everything inside a disable structure.
Types are a compact expression language in the type attribute:
bool | string | double | int32 | uint32 | uint8 | variant
array{ELEM}
cluster{TYPE.FieldName,TYPE.FieldName,...}
uint8{Label A,Label B,Label C} enum: base type + labels
ref{Queue}{ELEM} refnum, kind + payload
ref{Notifier}{ELEM}
ref{UserEvent}{ELEM}
ref{LV.Boolean} | ref{LV.Control} | ref{LV.String} control refnums
ref{LV.VI} VI refnum
ref{UDClassInst} reference to a user-defined class instance
tag{14} IO name control (a DAQmx physical channel)
{LV.VI} | {LV.Control} | {LV.String} VI/control class references
ref{UDClassInst} carries no payload in the type string — the class identity is not encoded,
so two unrelated class references are indistinguishable by type alone. Frameworks that thread
an object through a VI hierarchy (DQMH's module admin, for instance) show up as this.
Composition nests freely. The standard error cluster is:
cluster{bool.status,int32.code,string.source}
A multi-dimensional array is array.N{ELEM}, not a nested array{array{ELEM}}. The
dimension count is an infix on array, and the value literal nests with brackets:
<Indicator _name="Case 2 - Replace One Element in 2D Array"
type="array.2{double.Numeric}" value="[[0,1,2,3],[4,5,6,7]]" .../>Attested throughout NI's own exports — array.2{double.Numeric}, array.2{int32.Numeric},
array.2{string} — while array{array{ appears nowhere in 57 corpus files. Write the nested
form and it is refused in both Constant and Indicator position with
Error 53 ... Unrecognized or unsupported attribute set in Constant with UID 62
— a message that names the element but not the attribute, so it reads like a typo in value
rather than a wrong type spelling.
An earlier revision of this section, written the same day, drew the wrong conclusion from that
error. It reported the rejection correctly and then declared that a 2D array "cannot be
declared at all", advising two parallel 1D arrays as the workaround. That advice was unnecessary:
only the nested spelling is refused. scripts\lvai_run_and_read.xml still takes its input names
and values as two 1D lists — built under the wrong belief, harmless, and left alone because it
works.
A trailing .Name after a closing brace names the instance, not the type — a cluster
field holding a queue reference reads
ref{Queue}{cluster{string.Message,variant.Payload}.Inner Name}.Field Name.
Measured 2026-08-28. Nothing above can say that a control is an instance of a .ctl. NI's
unsupported list says as much for authoring — "custom controls or typedefs (.ctl)" — but the
consequence on the way out is the expensive one: an export renders a typedef as the type it
wraps and drops the identity entirely.
vi.lib\Utility\AggHandler\Bounds.vi carries two typedefs on its connector pane. Its export
carries neither:
| what the VI really has | what the AIXML says |
|---|---|
Bounds — XNodeBounds.ctl, owned by NI_XNodeSupport.lvlib |
cluster{int32.Width,int32.Height} |
that cluster's Width and Height — each an XNodeInt.ctl |
int32.Width, int32.Height |
State In — the SavedState typedef of Cls_AggHandler.xnode |
the whole nested cluster, written out |
Not one .ctl name and not one library name survives, at any depth. So regenerating a VI from
its own export replaces every typedef with a de-linked copy of its type — and since editing an
existing VI through this route is a full regeneration, that is true of every AIXML edit, not of
some exotic corner.
Nothing reports it. The structure is identical, so the VI compiles, the connector pane is
unchanged, and every caller's wires still bind. The damage surfaces much later, when someone edits
the .ctl and the change does not reach a VI that used to follow it.
pylv_route does not catch it either — measured on the same VI:
route: aixml
routeReason: AIXML round-tripped its own export of this VI cleanly and the diagram
contains no silently-unsupported node family.
silentlyUnsupported: [] validateErrorCode: 0
Both of its checks are blind to this by construction. Check A validates the export — and the export
is valid, precisely because the typedef is already gone from it. Check B scans for unsupported
node families, and a typedef is a property of a type, not a node. So the router gives its
cleanest possible answer to one of the cases it exists to prevent. Until that is fixed, ask
separately whether the VI uses a typedef before accepting route: aixml for an edit.
pylv_extract answers that without LabVIEW. A bound typedef is recorded in two places at once:
- in
VCTP, a<TypeDesc Type="TypeDef" Flag1="0x…">whose<Label>children name the owning library and the.ctl—<Label Text="NI_XNodeSupport.lvlib"/>,<Label Text="XNodeInt.ctl"/>. A loose typedef with no owning library carries the second label only. - in the front-panel heap, an object whose class is
typeDef—<ddo class="typeDef" …>— with its ownpartsList,bounds, a<stamp>matching thatFlag1, and the real control nested inside it.
Can pylabview put a binding back? Only where one already exists. The VCTP half is a text edit:
wrap the TypeDesc in place, the way §1 of docs/lvclass-creation.md wraps the private data
cluster, so the wrapper takes over the FlatTypeID and no type id moves. The heap half is not,
because the object has to be of class typeDef — and synthesising one is composing a heap object
of a class pylabview never writes from scratch, the same wall that document records for
udClassDDO.
| the control is | re-pointing it at a different .ctl |
|---|---|
already a typeDef object naming the wrong file |
NOT a plain <Label> substitution: measured 2026-08-28, the typedef file name sits 12 times in VCTP and 3 times in VITS, a block pylabview cannot parse and copies through unchanged. Untested, and no longer cheap. |
| a plain control with no binding at all | composition — out of reach |
So a stripped binding is repaired in the IDE, or by not stripping it in the first place.
Two layers stack, and both are needed:
- XML entities —
"&<>as usual. A primitive whose name contains&appears as e.g.Quotient & Remainder. - AIXML backslash-hex escapes —
\plus two hex digits, because:and,are structural separators insideinputs/outputs:
| Escape | Character | Where it shows up |
|---|---|---|
\3A |
: |
qualified subVI names, property paths, event selectors, terminal names |
\2C |
, |
any comment or description containing a comma |
\0A |
LF | multi-line description |
\0D |
CR | multi-line description |
Both : and , are separators inside inputs/outputs, which is why they are escaped
everywhere — including in free text. You may write a literal comma in a comment and
LabVIEW will accept it, but it comes back as \2C on the next export, so emit \2C
yourself if you want authored and exported files to match.
What is accepted is wider than what is emitted. \3B for a semicolon was accepted on
input and came back as a plain ;. So the decoder handles the general \XX form, while the
encoder escapes only the characters it must. Do not infer the escape set from an export
alone — but for round-trip stability, emit only what an export emits.
The rule above is about commas that are content. Inside a value attribute a comma is
normally structure — the separator inside an array or cluster literal — and structure is
never escaped. Counted across the 57-file corpus:
| attribute | raw , |
\2C |
|---|---|---|
description= |
0 | 17 |
value= |
51 | 1 |
Every one of the 51 is a literal separator — value="[false,0,]",
value="[[0,1,2,3],[4,5,6,7]]". The single escaped case is the tell: it sits inside a picture
constant's binary payload, where the byte 0x2C is data. So the rule is one rule after all:
Escape a comma that is content. Leave a comma that is structure.
For a plain string constant both spellings happen to work — a scalar has no structure to
confuse, and value="," and value="\2C" were each measured producing a working comma
delimiter. Prefer \2C there anyway: it is what an export emits for content, and it stays
correct if the constant later becomes part of something with structure. Getting this wrong on a
delimiter is silent — the file parses into one column of zeros with no error.
So a library-qualified call target is written MyLib.lvlib\3AHelper.vi, and a nested
property is Front Panel Window\3ACloseable.
A backslash escape is exactly what every shell and most string literals also want to consume.
Passing AIXML through a heredoc into a script ate both escapes of a Windows path:
value="C\3A\5Ctemp\5Cout.txt" arrived as value="CACtempCout.txt" — the backslash and the
first hex digit gone, the second digit left behind as a letter.
The failure then arrives disguised. ValidateAIXML answered
Error -2628 ... Load XML String.vi ... An error occurred while parsing the document.
which reads like malformed XML and is really a quoting bug two layers up. Author AIXML by writing the file directly. If a script must generate it, build the escapes with an explicit character map and re-read the file to confirm the escapes survived — never assume the string that left your source is the string that reached disk.
Python's octal escapes are a live instance of that, and the damage is invisible in a diff.
Building an AIXML string in Python, '\3A' is not backslash-three-A: it is chr(3) followed by
A. Measured 2026-08-29 — a description written that way put literal ETX and STX bytes into
the file, which rendered as TRAP^CA under cat -v and as an ordinary-looking TRAPA everywhere
else. Use raw strings, or write the file with an editor tool rather than through a language whose
escapes overlap AIXML's.
AIXML rejects XML comments. <!-- … --> between elements gives Error 42, Generic error, with
nothing naming the comment. Notes belong in the description attribute.
<Structure _name="While Loop" count="" uid="85" uid_parent="root">
<Tunnel _id="In1" inputs="value:52.value" outputs="value:91.value" uid="91" uid_parent="85"/>
<Condition inputs="value:136.value" uid="131" uid_parent="85" value="stop"/>
<ShiftReg uid="104" uid_parent="85">
<Left inputs="value:" outputs="value:154.x" uid="100" uid_parent="104"/>
<Right inputs="value:162.x-y*floor(x/y)" outputs="value:" uid="106" uid_parent="104"/>
</ShiftReg>
<Tunnel _id="Out1" inputs="value:142.element" outputs="value:95.value" uid="95" uid_parent="85"/>
</Structure>count on a While Loop is the ITERATION terminal i, not a count input — and the example
above showing count="" is only correct for a loop that never reads i. To use the iteration
number, name the wire on the loop itself and consume it from there:
<Structure _name="While Loop" count="100.value" uid="100" uid_parent="root">
<Node _name="Greater Or Equal?" inputs="x:100.value,y:200.value"
outputs="x >= y?:210.x >= y?" uid="210" uid_parent="100"/>The same holds for a Timed Loop, where NI's own export reads count="216.value" with a
Current Iteration indicator consuming value:216.value.
The failure mode is what makes this worth writing down: the error names the wrong element.
With count="" and a node consuming 100.value, validation reports
Greater Or Equal?: Contains unwired or bad terminal
— an accusation against the consumer, whose inputs are in fact written correctly. Nothing
points at the loop, and re-checking the consumer's terminal names finds nothing wrong because
nothing is. Measured 2026-08-22: adding count="100.value" and changing nothing else took the
same document from errorCode 1 to errorCode 0, and the generated VI then ran and returned
["hello world", "", "hello world", ""].
Condition value="stop"is the loop-condition terminal (stop-if-true).Leftcarriesoutputs,Rightcarriesinputs— the left terminal feeds the diagram with the previous iteration's value, the right terminal consumes the next one. This looks inverted and is the most common authoring mistake.- A
Tunnelappears once with bothinputs(outside) andoutputs(inside).
Same shape, with maxin / maxout alongside count.
maxin wires N. count does not — it names the loop's own i output net. This section said
for two revisions that nothing wires N and that auto-indexing was the only way; that was wrong,
and it pushed at least two generations into building a literal array or a While-Loop counter to get
a fixed iteration count. The measurements below are all still true — they simply tested the wrong
attribute. maxin was in the attribute vocabulary in §2 the whole time, listed and never tried.
NI's own export of Flush Written TDMS Data.vi settles it:
<Structure _name="For Loop" count="427.value" maxin="1426.value" maxout="" uid="427" uid_parent="root">
<Constant outputs="value:1426.value" type="int32" uid="1426" uid_parent="root" value="100"/>maxin takes the net of an int32 constant — that is the N terminal — while count holds
427.value, the structure's own uid, which is the i terminal's net. Confirmed in the other
direction by generating maxin="210.value" from an int32 constant 5: it validated, generated, and
the round-trip export preserved it, with no indexing tunnel anywhere on the loop.
What count is not, measured before maxin was tried:
| what was written | answer |
|---|---|
count="5", nothing indexed |
For Loop: N is not wired, and there are no indexing inputs. |
count="137.value", a net from an int32 constant |
same, plus two misleading follow-ons: Wire: Wire connected to an undirected tunnel twice, and You have connected two terminals of different types … source 1D array of long, sink long |
<Tunnel _id="N" …>, reaching for the terminal itself |
schema violation: value 'N' does not match regular expression facet '(In|Out)[1-9][0-9]*' |
count="" plus one mode="index" input tunnel |
errorCode 0 |
The two extra errors in the second row are worth recognising: they name wires and types, they appear
because count holds a net feeding the wrong terminal, and they vanish when it does not. Anyone
chasing them is looking in the wrong place.
For a fixed iteration count with no array to index, use maxin. The two workarounds this
document used to prescribe — indexing over a literal <Constant type="array{int32}" value="[0,0,0,0,0]"/>, or a While Loop with an int32 shift register, Increment and Greater?
against n-1 — both work and both still validate, but neither is necessary and each costs three to
five extra elements on the diagram.
Auto-indexing is mode="index" on the tunnel — the attribute the rest of this section was
missing. A tunnel without mode passes its whole value through unchanged; with it, the loop
indexes one element per iteration and supplies N.
It works on an Out tunnel too, which this section only ever showed on In tunnels — that is
how a loop builds an array, one element per iteration, without Build Array and a shift register.
<Tunnel _id="Out1" inputs="value:530.waveform out" mode="index" outputs="value:540.value"/>
validated, generated and round-tripped, and NI's own example exports carry the same shape. The pair
of them is the whole accumulate-per-channel idiom: index two In tunnels, index the Out tunnel
back out.
Measured from OpenG's
Filter 1D Array (String)__ogtk.vi, which is also a compact model of the accumulate-in-a-loop
idiom:
<Structure _name="For Loop" count="" maxin="" maxout="" uid="124" uid_parent="root">
<Tunnel _id="In1" inputs="value:341.Output Array" mode="index" outputs="value:131.value" uid="131" uid_parent="124"/>
<Tunnel _id="In2" inputs="value:4.value" outputs="value:205.value" uid="205" uid_parent="124"/>
<Node _name="Build Array" concat="true" inputs="array:195.array,array:256.Indices"
outputs="appended array:195.appended array" uid="195" uid_parent="124"/>
<ShiftReg uid="144" uid_parent="124">
<Left inputs="value:160.value" outputs="value:195.array" uid="148" uid_parent="144"/>
<Right inputs="value:195.appended array" outputs="value:146.value" uid="146" uid_parent="144"/>
</ShiftReg>
</Structure>The Right terminal's output net leaves the loop on its own — do not add an Out tunnel for
it. The snippet above is silent on how 146.value reaches a consumer outside, and the obvious
completion is wrong. Measured on an accumulate-and-join loop: naming the Right output net
460.value and reading it directly from a root-level Write to Text File validates; inserting
<Tunnel _id="Out1" inputs="value:460.value" outputs="value:470.value"/> between them fails with
For Loop: Is a member of a cycle
Wire: Is a member of a cycle
So the shift register's border crossing is implicit in AIXML, and an explicit tunnel on top of it
makes the loop appear to feed itself. The same holds on the way in: Left inputs="value:140.value"
takes its initialiser straight from a root-level constant with no In tunnel. Explicit Tunnel
elements are for wires that cross the border on their own, not for shift-register terminals.
Do NOT accumulate across NESTED loops with shift registers — measured 2026-08-29, the outer one
keeps only the LAST iteration. An inner loop accumulating a string per subVI and an outer loop
concatenating those blocks validated, generated, ran with errorCode 0 and returned exactly one
subVI's worth of records out of two. Nothing reports it: the answer is well-formed, plausible, and
silently missing everything but the final outer iteration. It was only visible because switching the
outer source from SubVIs[] to All Objects[] changed which single block came back — with the
last object being a constant, the report went empty instead of wrong.
The shape that works is two SEQUENTIAL loops with indexed output tunnels, never a nested pair:
one loop to find and enumerate, a second over what it found. scripts/lvbd_coercion_dots.xml is a
worked example — a Select plus shift register to carry the match out of the first loop, then
mode="index" Out tunnels on the second.
To filter rather than transform, put a Case Structure inside the loop, selected by the test, and
let one frame append while the other passes the accumulator through. Every frame must declare
every tunnel — a frame that does not use one still lists it with an empty net,
<Tunnel _id="In2" outputs="value:" …/>. A conditional output tunnel was not needed for this and
its AIXML shape is still unverified.
<Structure _name="Case Structure" selectin="439.value" uid="350" uid_parent="root">
<CaseFrame selector="No Error" selectout="410.value" uid="410" uid_parent="350">
...
</CaseFrame>
<CaseFrame selector="Error" selectout="364.value" uid="364" uid_parent="350">
<Structure _name="Case Structure" selectin="1745.code" uid="3080" uid_parent="364">
<CaseFrame selector="0" selectout="" uid="3104" uid_parent="3080"/>
<CaseFrame selector="Default" selectout="" uid="3144" uid_parent="3080"/>
</Structure>
</CaseFrame>
</Structure>selectinon theStructureis the wire feeding the selector.selectoron eachCaseFrameis the case label as typed in LabVIEW:"No Error","Error","0","Default", an enum label, a string.selectoutoptionally exposes the selector value inside the frame;""when unused. The net is named after the frame's ownuid—selectout="400.value"onuid="400", and a node inside reads400.value. That is how the offending value reaches an error message in aDefaultframe.- Nesting works by pointing the inner
uid_parentat the frame'suid.
A case tunnel is split across two levels, and it is not shaped like a loop tunnel. The While
Loop snippet above writes one Tunnel element carrying both inputs and outputs; copying
that shape into a case structure is the obvious move and it is wrong. A case tunnel appears once at
structure level for the wire outside, and again inside every frame for the wire inside — same
_id, different uid, and each half carries only one direction:
<Structure _name="Case Structure" selectin="60.lowercase file extension" uid="100" uid_parent="root">
<Tunnel _id="In1" inputs="value:60.dup file" uid="110" uid_parent="100"/> <!-- outside -->
<CaseFrame selector=""png"" selectout="" uid="200" uid_parent="100">
<Tunnel _id="In1" outputs="value:210.value" uid="210" uid_parent="200"/> <!-- inside -->
<Tunnel _id="Out1" inputs="value:220.image data" uid="230" uid_parent="200"/> <!-- inside -->
</CaseFrame>
<CaseFrame selector="Default" selectout="400.value" uid="400" uid_parent="100">
<Tunnel _id="In1" outputs="value:" uid="410" uid_parent="400"/> <!-- unused -->
<Tunnel _id="Out1" inputs="value:460.value" uid="470" uid_parent="400"/>
</CaseFrame>
<Tunnel _id="Out1" outputs="value:120.value" uid="120" uid_parent="100"/> <!-- outside -->
</Structure>So: In carries inputs at structure level and outputs in each frame; Out is the mirror image.
Consumers outside the structure read an output tunnel's net as <that tunnel's uid>.value. A frame
that does not use a tunnel still declares it with an empty net — the same per-frame rule the For
Loop section states, and it applies to Out as well as In.
Measured 2026-08-07 while building a PNG/BMP loader from Get File Extension.vi, Read PNG File.vi, Read BMP File.vi and Draw Flattened Pixmap.vi: a three-frame case in this shape
validated on the first attempt and re-exported byte-identically to what was authored — frames,
selectors and tunnels all intact. The shape came from exporting Read BMP File.vi, which is a
compact model of the idiom and the reliable way to check it again.
Two smaller confirmations from the same build. A cluster Constant works, nested cluster and
all — type="cluster{int32.image type,…,cluster{int16.left,int16.top,int16.right,int16.bottom}.Rectangle}"
with value="[0,0,[],[],[],[0,0,0,0]]" generated an empty Image Data constant for the Default
frame. And a terminal whose name embeds its default value must be spelled out in full inside
inputs: Error Cluster From Error Code.vi takes error code (0) and
error message (""), quotes included.
Reading only — and it fails silently. NI lists
Event Structure(andTimed Loop) among the node families the generator does not support (§9). Exports are complete and the syntax below is accurate, but authoring one does not produce an error. Measured:
ValidateAIXMLon an event structure with oneCaseFrame→errorCode 0ConvertAIXMLToVI→errorCode 0, a real 8 KB VI- re-export of that VI → the
Structure _name="Event Structure"is there, but everyCaseFrameis gone, and theEvent Data Nodecame back asfields="Source,Type,Time"instead of the requestedNewVal- the rendered diagram shows a single frame labelled
[0] TimeoutSo the shell is created and the event registration is dropped. You get a plausible-looking event structure that handles nothing. See the silent-degradation entry in §11.
<Structure _name="Event Structure" uid="615" uid_parent="1390">
<Tunnel _id="In1" cond="true" inputs="value:1480.value" uid="269" uid_parent="615"/>
<CaseFrame selector=" "Start"\3A Value Change " uid="3202" uid_parent="615">
<FreeLabel comment="Start button was pressed." uid="2988" uid_parent="3202"/>
<Control _name="Start" outputs="value:" style="latched" type="bool"
uid="2645" uid_parent="3202" value="false"/>
<Node _name="Event Data Node" fields="NewVal" outputs="NewVal:"
uid="3228" uid_parent="3202"/>
</CaseFrame>
</Structure>Event frames are CaseFrames whose selector encodes the event, always with leading and
trailing spaces as part of the string. Three distinct forms occur:
| Selector form | Event kind |
|---|---|
"<Control>"\3A Value Change |
static front-panel control event |
<<RegRef>.<Field>>\3A User Event |
dynamic user event from a registration refnum |
Panel Close? |
filter event — no control, no payload reference |
The dynamic form is the one that is easy to miss: the angle brackets are XML-escaped, the
name inside is <registration terminal>.<cluster field>, and the trailing text is literally
User Event. Such frames only exist where a Register For Events node feeds the structure.
Event Data Node exposes event fields (NewVal, Source, code, …) via fields; expect
one per frame. Filter events additionally use an Event Filter Node.
fields is a comma-separated terminal list whose meaning depends on the node:
| Node | fields example |
Meaning |
|---|---|---|
Unbundle By Name |
Field A,Field B,Field C |
which cluster fields to expose |
Bundle By Name |
Field A,Field B |
which fields to replace |
Event Data Node |
NewVal |
event data items |
Property Node |
write+Disabled |
property, write+ prefix = write access |
Property Node |
write+Front Panel Window\3ACloseable |
nested property class |
A Property Node without the write+ prefix reads. Index Array with two index:
entries in inputs returns two element outputs — repeated terminal names are how
expandable nodes are described.
An Invoke Node is a Node with target (the method) and type (the refnum
class) instead of fields. Verified by exporting a VI that calls FP.Open:
<Node _name="Invoke Node" target="FP.Open" type="{LV.VI}"
inputs="reference:97.value,error in (no error):170.value,Activate:88.value,State:"
outputs="reference out:,error out:43.error out" uid="43" uid_parent="root"/>inputs starts with reference and error in (no error), then carries the method's
own parameters by their literal LabVIEW names; outputs gives reference out and
error out. So VI Server scripting is expressible in AIXML — an earlier reading of
this document concluded the opposite from the fact that only Property Node was
documented here.
But target cannot be looked up from outside LabVIEW. Method names are not stored
as literals in a .vi (they are binary IDs — grepping VI files for FP.Open finds
nothing), they are not in LabVIEW.exe's string table beyond a few like FP.Open
itself, and SearchInfoCache covers palette items, not VI Server methods. The only
reliable way to obtain the target for a method you have not seen before is to place
that node in a scratch VI in the IDE and export it with ConvertVIToAIXML. Budget for
that step rather than guessing — a wrong target is exactly the kind of thing that
fails late.
They are the literal LabVIEW terminal labels, spaces, punctuation and all — and several are surprising. Verified from exports:
| Node | inputs | outputs |
|---|---|---|
Increment |
x |
x+1 |
Decrement |
x |
x-1 |
Greater? |
x, y |
x > y? |
Less? |
x, y |
x < y? |
Equal? |
x, y |
x = y? |
Or |
x, y |
x .or. y? |
Select |
t, s, f |
s? t\3Af |
Add |
x, y |
x+y |
Subtract |
x, y |
x-y |
Multiply |
x, y |
x*y |
Divide |
x, y |
x/y |
Reciprocal |
x |
1/x |
Concatenate Strings |
string, repeatable |
concatenated string |
Quotient & Remainder |
x, y |
x-y*floor(x/y), floor(x/y) |
Array Size |
array |
size(s) |
Index Array |
array, index |
element |
Wait (ms) |
milliseconds to wait |
millisecond timer value |
Get Waveform Components |
waveform |
per fields, e.g. Y |
Build Waveform |
waveform, then one per fields, e.g. dt, Y |
output waveform |
Sort 1D Array |
array |
sorted array |
Array To Spreadsheet String |
format string, array, delimiter (Tab) |
spreadsheet string |
Build Array |
array / element, repeatable |
appended array |
Unbundle By Name |
input cluster |
one per fields, e.g. status, code, source |
Bundle By Name |
one per fields, then input cluster — the order matters |
output cluster |
Bundle |
element, repeatable, then cluster |
output cluster |
Unbundle |
cluster |
element, repeatable |
Unbundle / Bundle Elements |
input cluster |
output cluster — the In Place Element border node |
String To Path |
string |
path |
Path To String |
path |
string |
Number To Decimal String |
number |
decimal integer string — not string |
Variant To Data |
variant, type, error in — not error in (no error) |
data, error out |
Read from Text File |
file (use dialog), count, error in, prompt (Open existing file) |
refnum out, text, cancelled, error out |
Write to Text File |
file (use dialog), text, error in, prompt (Choose or enter file path) |
refnum out, cancelled, error out |
Close File |
refnum, error in |
path, error out |
Open VI Reference |
application reference (local), vi path, options, error in (no error), type specifier VI Refnum (for type only), password ("") |
vi reference, error out |
Close Reference |
reference, error in (no error) |
error out |
Build Array takes concat="true" for concatenating mode, and then names each input by what is
wired to it — array for an array, element for a scalar — which is why the same node can carry
inputs="array:…,element:…". Read from Text File and Write to Text File need no refnum at all
when handed a path: they open and close the file themselves.
Array To Spreadsheet String appends a platform line ending after the last element.
Measured on a five-element string array with delimiter (Tab) wired to \0A: the result ended
…\nbanana\r\n, so the delimiter separates the elements and a \r\n is added on top. If you want
the elements separated and nothing trailing, strip it — which is exactly what OpenG's
1D Array to String__ogtk.vi does internally, with Match Pattern anchored on the platform line
ending followed by $.
Everything above was measured, most of it by exporting a VI that already used the node — a shipped
example under examples\File IO\, and OpenG's own Filter 1D Array (String)__ogtk.vi for the
accumulating loop. That remains the reliable way to add a row here.
A terminal's default value is part of its name, in parentheses. Four rows above show it —
prompt (Open existing file), password (""), type of dialog (OK msg\3A1),
delimiter (Tab) — and it is the first thing to try when a name that looks obviously right is
rejected. Measured: delimiter on Array To Spreadsheet String gives
Object terminal not found for input: delimiter:130.value, while delimiter (Tab) validates.
The same node's format string and array carry no suffix, so the rule is per terminal, not per
node: only terminals that actually have a documented default get one.
Note Greater? uses spaces around the operator while Add does not, and Select's
output contains a colon that must be escaped (s? t\3Af) — the escaping rules of
section 6 apply inside terminal names too. In XML the < in x < y? additionally needs
<, and & in Quotient & Remainder needs &.
The reliable way to get a name right: export a VI that already uses the node and copy the string verbatim.
Ask for every node at once, not one per call. lvai_aixml_reference takes node= as a
comma-separated list — node='Select,Index Array,Build Waveform,Greater?' — and that is the
intended way to use it, because a term is matched by substring and single lookups therefore
hand back the same text repeatedly. Three concrete overlaps, all from one VI generation: the
2D-indexing code block below answers Index Array, disabled index and Array Subset; the
Build Waveform subsection answers Build Waveform, waveform and Time Stamp; and the
| Node | inputs | outputs | header trio is re-emitted with every single term.
Measured on exactly the 18 terms that generation looked up one at a time:
| characters | round trips | |
|---|---|---|
| one term per call | 21 973 | 18 |
| one batched call | 13 427 | 1 |
38.9 % less text, and 17 round trips gone, with every terminal name still present — there is
a test pinning that (ALargeBatchStillCarriesEveryTerminalNameItWasAskedFor). A batch prints each
passage once and says so when a term's hits all appeared under an earlier term, which is the
difference that matters: silence would read as "not documented", and that is what sends a reader
off to re-derive a fact the document already carried.
A batch also divides the passage budget across its terms — three each at the default limit —
because "the terminals of these eighteen nodes" is a different question from "everything about
this one term". Ask a term on its own when you want the depth.
Note what this is not: a cache. Caching keyed on the argument would have saved nothing here, because no two of those 18 calls had the same argument. The duplication was in the output.
The documents are cached as well — each one and its line index is built once per process rather than once per call — but that is a separate and much smaller win, and the numbers say so plainly:
| before | after | |
|---|---|---|
| one lookup | 0.841 ms | 0.039 ms |
| the 18-term workload, server side | 23.3 ms | 0.8 ms |
23 ms was never what made a generation session slow. A round trip is a model turn, and that is
the term that dominates: measured over three lvai_* calls, 30.4 s of wall clock against 74 ms of
LabVIEW time for the run and under a second for validate plus convert — roughly 7 s per turn, all
latency. The 17 turns a batch removes are therefore worth about two minutes, against milliseconds
for the cache. Optimise the number of calls, not the cost of one.
For scale on the LabVIEW side: LabVIEWMCP --selftest over a VI and its project costs 3.30 s cold
and 0.76 s warm, whole process included.
The table above is curated: each row was measured by hand and several carry a warning that only a person can give. It is also small. The corpus sweep found 377 distinct node kinds across the shipping examples, so the table below is all of them — every node seen in more than one example VI, with the ordered terminal lists LabVIEW itself writes.
The two overlap on purpose. The curated table is where the caveats live; this one is where completeness lives, and it deliberately does not skip a node just because the prose mentions it. An earlier version did, which meant writing a sentence about a node deleted its terminals from this reference.
It is generated. Do not edit it; regenerate it, and put anything worth saying about a node in the prose above where a regeneration cannot overwrite it:
python scripts/aixml_corpus_report.py --update-docsThree things to read correctly:
- The order is the measurement. Terminal order inside
inputsis load-bearing on at leastBundleandBundle By Name(below), so a row is the whole string to copy, not a set of names. varies per instancemeans the node has no fixed terminal names at all — aLocal Variable's terminal is the variable it points at, aProperty Node's is the property. Printing one VI's spelling as the signature would be a fabrication, so the count of distinct shapes is given instead. For those, export the VI you are working from.(n/m)after a row means that shape was the commonest but not the only one:nsightings ofm. Expandable nodes and nodes whose terminal names follow the wired data do this.
A node absent from both tables is not necessarily absent from LabVIEW — it may simply appear in
only one example. The complete list, single sightings included, is
docs/aixml-node-gaps.tsv, which lives in the repository and is not
embedded in the assembly: on a binary-only install the two tables above are all there is, which is
the other reason this one is spliced into the document rather than left beside it.
| Node | inputs (in order) | outputs (in order) |
|---|---|---|
Property Node |
varies per instance (165 shapes) | varies per instance (101 shapes) |
Event Data Node |
— | varies per instance (80 shapes) |
Multiply |
x, y |
x*y |
Bundle |
varies per instance (217 shapes) | output cluster |
Build Array |
varies per instance (22 shapes) | appended array |
Unbundle By Name |
input cluster |
varies per instance (281 shapes) |
Merge Errors |
error in, error in (252/324) |
error out |
Wait (ms) |
milliseconds to wait |
millisecond timer value |
Index Array |
varies per instance (18 shapes) | varies per instance (11 shapes) |
Add |
x, y |
x+y |
Bundle By Name |
varies per instance (189 shapes) | output cluster |
Subtract |
x, y |
x-y |
Local Variable |
varies per instance (116 shapes) | varies per instance (65 shapes) |
Divide |
x, y |
x/y |
Invoke Node |
varies per instance (73 shapes) | reference out, error out (126/233) |
Or |
x, y |
x .or. y? |
Select |
t, s, f |
s? t\3Af |
Close Reference |
reference, error in (no error) |
error out |
Build Path |
base path, name or relative path |
appended path |
Random Number (0-1) |
— | number\3A 0 to 1 |
Format Into String |
initial string, error in, input 1, format string (93/150) |
resulting string, error out |
Increment |
x |
x+1 |
Unbundle / Bundle Elements |
input cluster (72/144) |
output cluster (72/144) |
Unbundle |
cluster |
varies per instance (58 shapes) |
Array Size |
array |
size(s) |
Equal? |
x, y |
x = y? |
VI Server Reference |
— | varies per instance (68 shapes) |
Concatenate Strings |
string, string (74/108) |
concatenated string |
Compound Arithmetic |
value, value, value (78/107) |
result |
Generate User Event |
user event, event data cluster, error in, priority (normal) |
user event out, error out |
Feedback Node |
initializer, next value |
previous value |
Decrement |
x |
x-1 |
Create User Event |
user event datatype, error in |
user event, error out |
To More Specific Class |
reference, error in, target class |
specific class reference, error out |
Square |
x |
x^2 |
Quotient & Remainder |
x, y |
x-y*floor(x/y), floor(x/y) |
Static VI Reference |
— | value |
Event Filter Node |
Discard? (61/70) |
— |
Destroy User Event |
user event, error in |
error out |
Variant To Data |
Variant, error in, type |
data, error out |
Initialize Array |
element, dimension size (62/68) |
initialized array |
Negate |
x |
-x |
Greater? |
x, y |
x > y? |
Call Library Function Node |
error in (no error), input, output (46/59) |
error out, input, output (46/59) |
Not |
x |
.not. x? |
Sine |
x |
sin(x) |
Current VI's Path |
— | path |
Transpose 2D Array |
2D array |
transposed array |
Expression Node |
input |
output |
In Range and Coerce |
upper limit, x, lower limit |
coerced(x), In Range? |
Reciprocal |
x |
1/x |
Absolute Value |
x |
abs(x) |
Unregister For Events |
event registration refnum, error in |
error out |
And |
x, y |
x .and. y? |
Replace Array Subset |
array, index, new element/subarray (26/39) |
output array |
Call Parent Class Method |
varies per instance (15 shapes) | Actor out, error out (26/37) |
Array Index / Replace Elements |
varies per instance (7 shapes) | varies per instance (7 shapes) |
Not A Number/Path/Refnum? |
number/path/refnum |
NaN/Path/Refnum? |
Equal To 0? |
x |
x = 0? |
Tick Count (ms) |
— | millisecond timer value |
Open VI Reference |
application reference (local), vi path, options, error in (no error), type specifier VI Refnum (for type only), password ("") |
vi reference, error out |
One Button Dialog |
message, button name ("OK") |
true |
Build Waveform |
varies per instance (9 shapes) | output waveform |
Not Equal? |
x, y |
x != y? |
Register For Events |
event registration refnum, error in (no error), event source (25/33) |
event registration refnum, error out |
TDMS Read |
tdms file, group name in, channel name(s) in, error in (no error), offset (0), count (-1\3A all), data type, return channels in file order? (F) |
end of file?, tdms file out, group name out, channel name(s) out, data, error out |
Less? |
x, y |
x < y? |
Array Subset |
array, index, length (23/31) |
subarray |
String Length |
string |
length |
Number To Fractional String |
number, width (-), precision (6), use system decimal point (T) |
F-format string |
Conditional Disable Structure |
— | — |
Wait Until Next ms Multiple |
millisecond multiple |
millisecond timer value |
Square Root |
x |
sqrt(x) |
Enqueue Element |
queue, element, timeout in ms (-1), error in (no error) |
queue out, timed out?, error out |
Max & Min |
x, y |
max(x\2Cy), min(x\2Cy) |
Obtain Queue |
name (unnamed), element data type, create if not found? (T), error in (no error), max queue size (-1\2C unlimited) |
queue out, created new?, error out |
To Double Precision Float |
number |
double precision float |
Type Cast |
x, type |
*(type *) &x |
Delete From Array |
array, length, index (21/24) |
array w/ subset deleted, deleted portion |
Dequeue Element |
queue, timeout in ms (-1), error in (no error) |
queue out, element, timed out?, error out |
Release Queue |
queue, force destroy? (F), error in (no error) |
queue name, remaining elements, error out |
Reshape Array |
array, dimension size (17/24) |
output array |
Less Than 0? |
x |
x < 0? |
Greater Or Equal? |
x, y |
x >= y? |
Match Pattern |
string, regular expression, offset (0) |
before substring, match substring, after substring, offset past match |
Add Array Elements |
numeric array |
sum |
To Variant |
anything |
Variant |
Array Max & Min |
array |
max value, max index (indices), min value, min index (indices) |
Exponential |
x |
exp(x) |
And Array Elements |
Boolean array |
logical AND |
Number To Decimal String |
number, width (-) |
decimal integer string |
Get Date/Time In Seconds |
— | seconds since 1Jan1904 |
First Call? |
— | First Call?\3A T/F |
To Unsigned Long Integer |
number |
unsigned 32bit integer |
Insert Into Array |
array, index, new element/subarray (18/19) |
output array |
New VI Object |
owner refnum, style, position/next to, error in (no error), vi object class, auto wire? (F), path, bounds |
object refnum, error out |
Diagram Disable Structure |
— | — |
Empty Array? |
array |
empty? |
Sine & Cosine |
x |
sin(x), cos(x) |
Greater Than 0? |
x |
x > 0? |
Global Variable |
varies per instance (4 shapes) | varies per instance (10 shapes) |
Formula Node |
varies per instance (11 shapes) | varies per instance (12 shapes) |
TDMS Set Channel Information |
tdms file, group name (Untitled), channel name(s), error in (no error), data layout (0\3Anon-interleaved), data type, samples per channel |
tdms file out, error out |
TCP Close Connection |
connection ID, abort (F), error in (no error) |
connection ID out, error out |
Delete |
path (use dialog), entire hierarchy (F), confirm (F), error in, prompt (Delete) |
deleted path, cancelled, error out |
TDMS Open |
file path, operation (0\3Aopen), byte order (2\3Alittle-endian), error in (no error), file format version (2.0), create index file? (T), disable buffering (T) |
tdms file out, error out |
TDMS Close |
tdms file, error in (no error) |
file path out, error out |
Wait For Front Panel Activity |
do not wait! (False), front panel (this VI's panel), timeout ms (-1 never timeout) |
millisecond timer value |
VISA Close |
VISA resource name, error in (no error) |
error out |
Strip Path |
path |
stripped path, name |
Get Waveform Components |
waveform |
varies per instance (6 shapes) |
Rotate 1D Array |
n, array |
array (last n elements first) |
Index & Bundle Cluster Array |
component array, component array (14/16) |
array of clusters |
Search 1D Array |
1D array, element, start index (0) |
index of element |
Fract/Exp String To Number |
string, offset, default (0 dbl), use system decimal point (T) |
offset past number, number |
Sort 1D Array |
array |
sorted array |
TDMS Advanced Open |
file path, operation (0\3Aopen), error in (no error), disable buffering? (T), enable asynchronous? (T) |
tdms file out, sector size, error out |
TDMS Write |
tdms file, group name in (Untitled), channel name(s) in (Untitled), data, error in (no error), data layout (0\3Adecimated) |
tdms file out, group name out, channel name(s) out, error out |
TDMS Advanced Close |
tdms file, truncate file? (F), error in (no error), timeout (10 s) |
file path out, error out |
Start Asynchronous Call |
varies per instance (7 shapes) | reference out, error out |
TCP Read |
connection ID, bytes to read, timeout ms (25000), error in (no error), mode (standard) |
connection ID out, data out, error out |
Byte Array To String |
unsigned byte array |
string |
Not Equal To 0? |
x |
x != 0? |
Close File |
refnum, error in |
path, error out |
Enqueue Element At Opposite End |
queue, element, timeout in ms (-1), error in (no error) |
queue out, timed out?, error out |
Cosine |
x |
cos(x) |
TDMS Get Properties |
tdms file, group name, channel name, error in (no error), property name, data type |
found, property value, tdms file out, group name out, channel name out, error out (13/14) |
TDMS Advanced Synchronous Write |
tdms file, data, error in (no error) |
tdms file out, error out |
TDMS Advanced Synchronous Read |
tdms file, error in (no error), count (-1), data type |
read process finished?, tdms file out, data, error out |
Open/Create/Replace File |
file path (use dialog), operation (0\3Aopen), access (0\3Aread/write), error in, prompt, disable buffering (F) |
refnum out, cancelled, error out |
To Lower Case |
string |
all lower case string |
Two Button Dialog |
message, T button name ("OK"), F button name ("Cancel") |
T button? |
Empty String/Path? |
string/path |
empty? |
Round Toward -Infinity |
x |
floor(x)\3A largest int <= x |
Join Numbers |
hi, lo |
(hi.lo) |
Read from Text File |
file (use dialog), count, error in, prompt (Open existing file) |
refnum out, text, cancelled, error out |
Reverse 1D Array |
array |
reversed array |
Greater Or Equal To 0? |
x |
x >= 0? |
Less Or Equal? |
x, y |
x <= y? |
Release Notifier |
notifier, force destroy? (F), error in (no error) |
notifier name, last notification, error out |
Open VI Object Reference |
owner refnum, name/order, error in (no error), vi object class |
object refnum, error out |
Python Node |
session in, module path, function name, error in (no error), return type, input parameter, input parameter (9/11) |
session out, error out, return value, value, value (9/11) |
Close Python Session |
session in, error in |
error out |
String Subset |
string, offset (0), length (rest) |
substring |
Obtain Notifier |
name (unnamed), notification data type, create if not found? (T), error in (no error) |
notifier out, created new?, error out |
Send Notification |
notifier, notification, error in (no error) |
notifier out, error out |
Wait on Notification |
notifier, ignore previous (F), timeout in ms (-1), error in (no error) |
notifier out, notification, timed out?, error out |
Merge Signals |
input signal, input signal (10/11) |
combined signal |
Add with Error Terminals |
x, y, error in (no error) |
x+y, error out |
TCP Write |
connection ID, data in, timeout ms (25000), error in (no error) |
connection ID out, bytes written, error out |
To Long Integer |
number |
32bit integer |
Seconds To Date/Time |
time stamp (now), to UTC (F) |
date time rec |
Write to Text File |
file (use dialog), text, error in, prompt (Choose or enter file path) |
refnum out, cancelled, error out |
Boolean To (0\2C1) |
Boolean |
0\2C 1 |
Get Queue Status |
queue, return elements? (F), error in (no error) |
max queue size, queue name, elements, # elements in queue, queue out, # pending remove, # pending insert, error out |
VISA Write |
VISA resource name, write buffer, error in (no error) |
VISA resource name out, return count, error out |
To Time Stamp |
number |
Time Stamp |
Call By Reference |
varies per instance (7 shapes) | varies per instance (7 shapes) |
To Unsigned Byte Integer |
number |
unsigned 8bit integer |
String To Byte Array |
string |
unsigned byte array |
Unflatten From JSON |
JSON string, type and defaults, error in (no error), path, enable LabVIEW extensions? (T), default null elements? (F), strict validation? (F) |
value, error out |
Insert Menu Items |
menu reference, item names, item tags, error in (no error), menu tag, after item |
menu reference out, item tags out, error out |
To Word Integer |
number |
16bit integer |
Complex To Re/Im |
x + iy |
x, y |
VISA Open |
VISA resource name, duplicate session (F), access mode, error in (no error), timeout (0) |
VISA resource name, error out |
VISA Read |
VISA resource name, byte count, error in (no error) |
VISA resource name out, read buffer, return count, error out |
Interpolate 1D Array |
array of numbers or points, fractional index or x |
y value |
Build Matrix |
varies per instance (4 shapes) | appended array |
String To IP |
name |
net address |
Path To String |
path |
string |
Shared Variable |
error in (no error) (5/8) |
varies per instance (6 shapes) |
TDMS List Contents |
tdms file, group name, error in (no error) |
tdms file out, group names, group/channel names, error out (6/8) |
Re/Im To Complex |
x, y |
x + iy |
Preserve Run-Time Class |
object in, error in, target object |
object out, error out |
New VI |
application refnum, template, vi type (standard vi), error in (no error), not connected, type specifier VI Refnum (for type only), password |
vi refnum, error out |
TCP Open Connection |
address, remote port or service name, timeout ms (60000), error in (no error), local port |
connection ID, error out |
Matrix Size |
number of rows |
number of columns, matrix |
Split Number |
x |
hi(x), lo(x) |
Open Python Session |
python version, python path, error in (no error) |
session out, error out |
Decimal String To Number |
string, offset, default (0L) |
offset past number, number |
String To Path |
string |
path |
Quit LabVIEW |
quit? (T) |
— |
Constructor Node |
error in (no error) (4/6) |
new reference, error out (4/6) |
Get Variant Attribute |
Variant, name, default value (empty Variant), error in |
duplicate Variant, names, values, error out (3/6) |
Flatten To XML |
anything |
xml string |
Get Date/Time String |
date format (0), seconds (now), want seconds? (F) |
date string, time string |
UDP Close |
connection ID, error in (no error) |
connection ID out, error out |
Array Split / Replace Subarrays |
varies per instance (4 shapes) | varies per instance (4 shapes) |
Natural Logarithm |
x |
ln(x) |
Complex To Polar |
r * e^(i*theta) |
r, theta |
Call MATLAB Function |
varies per instance (4 shapes) | varies per instance (4 shapes) |
Array To Cluster |
array |
cluster |
Swap Values |
y, ?(T), x |
y', x' |
Cluster To Array |
cluster |
array |
Split 1D Array |
array, index |
first subarray, second subarray |
Scan From String |
input string, initial scan location, error in, default value 1, format string (4/5) |
remaining string, offset past scan, error out, output 1 (4/5) |
Flatten To JSON |
anything, error in (no error), enable LabVIEW extensions? (T) |
JSON string, error out |
DataSocket Open |
URL, mode, ms timeout (10000), error in (no error) |
connection id, error out |
DataSocket Read |
connection in, type (Variant), ms timeout (10000), error in (no error), wait for updated value (T) |
status, quality, timestamp, connection out, data, timed out, error out |
DataSocket Close |
connection id, ms timeout (0), error in (no error) |
timed out, error out |
Delete Menu Items |
menu reference, menu tag, items, error in (no error) |
menu reference out, error out |
Get File Size |
file, error in |
refnum out, size (in bytes), error out |
Read from Binary File |
file (use dialog), count, byte order (0\3Abig-endian\2C network order), error in, prompt (Open existing file), data type |
refnum out, data, cancelled, error out |
TDMS Configure Asynchronous Writes |
tdms file, max asynchronous writes (4), error in (no error), pre-allocate? (F), max write size, data type, timeout (5 s) |
tdms file out, error out |
TDMS Advanced Asynchronous Write |
tdms file, data, error in (no error) |
tdms file out, error out |
Unflatten From XML |
xml string, type, error in (no error) |
value, error out |
Power Of X |
y, x |
x^y |
Power Of 2 |
x |
2^x |
IP To String |
net address, dot notation? (F) |
name |
Array To Spreadsheet String |
format string, array, delimiter (Tab) |
spreadsheet string |
Scan String For Tokens |
input string, offset, operators (none), delimiters (\\s\2C\\t\2C\\r\2C\\n), allow empty tokens? (F), use cached delim/oper data? (F) |
string out, offset past token, token string, token index |
Flush Queue |
queue, error in (no error) |
queue out, remaining elements, error out |
Set Variant Attribute |
Variant, name, value, error in |
Variant out, replaced, error out |
TCP Wait On Listener |
listener ID in, resolve remote address (T), timeout ms (wait forever\3A -1), error in (no error) |
connection ID, listener ID out, remote address, remote port, error out |
TCP Create Listener |
service name, port, timeout ms (25000), error in (no error), net address |
listener ID, port, error out |
Less Or Equal To 0? |
x |
x <= 0? |
Write Single Element to Stream |
endpoint in, data in, timeout ms (-1), error in (no error) |
endpoint out, timed out?, error out |
Destroy Stream Endpoint |
endpoint in, error in (no error) |
error out |
Flush Stream |
endpoint in, wait condition, timeout in ms (-1), error in (no error) |
endpoint out, timed out?, error out |
UDP Open |
port, service name, timeout ms (25000), error in (no error), net address |
connection ID, port, error out |
UDP Read |
connection ID, max size (548), timeout ms (25000), error in (no error) |
address, port, connection ID out, data out, error out |
UDP Write |
connection ID, data in, timeout ms (25000), error in (no error), address, port or service name |
connection ID out, error out |
Round Toward +Infinity |
x |
ceil(x)\3A smallest int >= x |
TDMS Reserve File Size |
tdms file, reserve size, error in (no error), append? (T), data type |
tdms file out, error out |
TDMS Configure Asynchronous Reads |
tdms file, number of buffers (4), buffer size, error in (no error), data type, timeout (5 s) |
tdms file out, error out |
TDMS Start Asynchronous Reads |
tdms file, total count (-1), error in (no error), data type |
tdms file out, error out |
TDMS Advanced Asynchronous Read |
tdms file, error in (no error), data type |
read process finished?, tdms file out, data, error out |
TDMS In Memory Open |
byte array or file path, error in (no error) |
tdms file out, error out |
TDMS In Memory Close |
tdms file, error in (no error), file path, overwrite (F) |
error out |
Polar To Complex |
r, theta |
r * e^(i*theta) |
Open MATLAB Session |
release name, error in (no error) |
session out, error out |
Set Waveform Attribute |
waveform, name, value, error in |
waveform out, replaced, error out |
Variant Attribute Get / Replace |
variant, attribute name (2/4) |
attribute, found? (2/4) |
Variant To / From Element |
Variant, type (2/4) |
data, error out (2/4) |
Waveform Unbundle / Bundle Elements |
waveform (2/4) |
output waveform (2/4) |
VI Library |
— | path |
Wait On Asynchronous Call |
reference, error in (no error) |
reference out, error out, X + Y (2/3) |
Temporary Directory |
— | path |
Create Folder |
path (use dialog), error in, prompt (Create Folder) |
created path, cancelled, error out |
Look In Map |
map, key, default value |
key not found?, value |
Insert Into Set |
set in, element |
set out, already included? |
Register Event Callback |
event callback refnum, error in (no error), event source, VI Ref, Meter (2/3) |
event callback refnum, error out |
Automation Open |
Automation Refnum, machine name, open new instance, error in (no error) |
Automation Refnum, error out |
Spreadsheet String To Array |
format string, spreadsheet string, array type (2D Dbl), delimiter (Tab) |
array |
Search/Split String |
string, search string/char (-), offset (0) |
substring before match, match + rest of string, offset of match |
Inverse Tangent (2 Input) |
y, x |
atan2(y\2Cx) |
Bluetooth Read |
connection ID, bytes to read, timeout ms (25000), error in (no error), mode (standard) |
connection ID out, data out, error out |
Bluetooth Write |
connection ID, data in, timeout ms (25000), error in (no error) |
connection ID out, bytes written, error out |
Write to Binary File |
file (use dialog), data, byte order (0\3Abig-endian\2C network order), error in, prompt (Choose or enter file path), prepend array or string size? (T) |
refnum out, cancelled, error out |
IrDA Read |
connection ID, bytes to read, timeout ms (25000), error in (no error), mode (standard) |
connection ID out, data out, error out |
IrDA Write |
connection ID, data in, timeout ms (25000), error in (no error) |
connection ID out, bytes written, error out |
New TLS Configuration |
load OS trusted CAs?, error in (no error) |
TLS configuration out, error out |
Make TLS Configuration Immutable |
TLS configuration, error in (no error) |
immutable TLS configuration, error out |
Close TLS Configuration |
TLS configuration, error in (no error) |
error out |
Sign |
number |
-1\2C 0\2C 1 |
TDMS Set Properties |
tdms file, group name, channel name, error in (no error), property names, property values |
tdms file out, group name out, channel name out, error out |
TDMS Set Next Read Position |
tdms file, offset (0), from (0\3A start), error in (no error), group name in, channel name in |
tdms file out, error out |
Build Cluster Array |
component element, component element (2/3) |
array of clusters |
VISA Enable Event |
VISA resource name, event type, mechanism (1\3A VI_QUEUE), error in (no error) |
VISA resource name out, error out |
Threshold 1D Array |
array of numbers or points, threshold y, start index (0) |
fractional index or x |
Interleave 1D Arrays |
array, array |
interleaved array |
Get Drag Drop Data |
data name, type, error in (no error) |
data, error out |
To More Generic Class |
reference, target class |
generic class reference |
Logarithm Base 10 |
x |
log(x) |
Insert Into Map |
map in, key, value |
map out, key already included?, value unchanged? |
Remove From Map |
map in, key |
map out, key not found?, value |
To Unsigned Word Integer |
number |
unsigned 16bit integer |
Or Array Elements |
Boolean array |
logical OR |
Not And |
x, y |
.not. (x .and. y)? |
Create Network Stream Reader Endpoint |
reader name, writer url, data type, error in (no error), reader buffer size, timeout in ms (-1), element allocation mode |
reader endpoint, error out |
Create Network Stream Writer Endpoint |
writer name, reader url, data type, error in (no error), writer buffer size, timeout in ms (-1), element allocation mode |
writer endpoint, error out |
Read Single Element from Stream |
endpoint in, timeout ms (-1), error in (no error) |
endpoint out, data out, timed out?, error out |
Bluetooth Close Connection |
connection ID, abort (F), error in (no error) |
connection ID out, error out |
IrDA Close Connection |
connection ID, abort (F), error in (no error) |
connection ID out, error out |
Start TLS |
TCP connection, immutable TLS configuration, server hostname, error in (no error), timeout ms, server certificate validation |
TLS connection, server certificate chain, error out |
Search and Replace String |
input string, search string, replace string, offset, error in, replace all?, case sensitive? |
result string, number of replacements, offset past replacement, error out |
Round To Nearest |
number |
nearest integer value |
Current VI's Menubar |
— | menu reference |
Open/Create/Replace Datalog |
datalog path (use dialog), operation (0\3Aopen), access (0\3Aread/write), error in, prompt, record type |
refnum out, cancelled, error out |
TDMS Get Asynchronous Read Status |
tdms file, error in (no error) |
tdms file out, number of buffers available, all buffers full?, error out |
TDMS Set Next Write Position |
tdms file, offset (0), from (0\3Astart), error in (no error), group name in, channel name in |
tdms file out, error out |
TDMS In Memory Read Bytes |
tdms file, error in (no error), offset (0), byte count (-1\3A all) |
tdms file out, data, error out |
Sinc |
x |
sin(x)/x |
VISA Disable Event |
VISA resource name, event type (all enabled), mechanism (1\3A VI_QUEUE), error in (no error) |
VISA resource name out, error out |
VISA Wait on Event |
VISA resource name, event type (all enabled), event resource name (for class), error in (no error), timeout (0) |
VISA resource name out, event type, event resource name, error out |
File Dialog |
start path, default name, error in, prompt, button label, pattern (all files), pattern label |
selected path, exists, cancelled, error out |
Decimate 1D Array |
array |
decimated array, decimated array (1/2) |
Multiply Array Elements |
numeric array |
product |
Script Node |
error in (1/2) |
2-D Array of Real, error out (1/2) |
Corrected 2026-08-09. This section previously claimed "Bundle By Name does not work" and
concluded that "a generated diagram cannot build a cluster". Both are wrong. The node works.
What fails is one particular spelling of it, and the difference is the order the terminals are
listed in.
input cluster must come after the field terminals:
<!-- validates, generates, and re-exports unchanged -->
<Node _name="Bundle By Name" fields="code" inputs="code:11.value,input cluster:10.value"
outputs="output cluster:20.output cluster" uid="20" uid_parent="root"/>
<!-- same nets, same names, cluster listed first: rejected -->
<Node _name="Bundle By Name" fields="code" inputs="input cluster:10.value,code:11.value"
outputs="output cluster:20.output cluster" uid="20" uid_parent="root"/>The rejection is the misleading part, because it complains about a type and never mentions order:
Bundle By Name: Cluster is invalid or empty
Bundle By Name: Contains unwired or bad terminal
Cluster , a cluster of 0 elements, conflicts with cluster error out, a cluster of 3 elements.
The type of the sink is cluster of 0 elements.
Read literally that says the cluster arrived untyped, which is why the earlier reading concluded
the node was unusable. It is not a type problem: the same document with the two inputs
entries swapped answers errorCode 0.
Measured 2026-08-09 on LabVIEW 2026, four documents differing only in that order:
| Cluster source | inputs order |
Result |
|---|---|---|
Control, cluster{bool.status,int32.code,string.source} |
code, then input cluster |
validates |
Control, same type |
input cluster, then code |
Cluster is invalid or empty |
Constant, cluster{int32.Module ID,bool.Power State,bool.Self Test Result} |
fields, then input cluster |
validates |
Constant, same type |
input cluster, then fields |
Cluster is invalid or empty |
So the cluster Constant is fine too — the old text blamed it for the same reason it blamed the
node. The working document was then generated with ConvertAIXMLToVI and re-exported: LabVIEW
writes back inputs="code:99.value,input cluster:43.value", fields first, byte-identical in shape
to what was authored.
It does NOT apply to a Call. Measured on the same day: a Call to
Read Delimited Spreadsheet.vi with all eight inputs and all six outputs deliberately scrambled
— delimiter first, file path last, outputs reversed — validates with errorCode 0. A Call's
terminals are resolved by name, so only the spelling matters. That is worth knowing because the
export's own order is neither the connector-pane order nor anything derivable: for that VI the
Call lists conIdx 0, 5, 7, 9, 11, 1, 12, 13. Do not try to reconstruct it — and do not fear
getting it wrong. lvai_vi_terminals prints a ready-to-paste Call for any VI.
The order rule below is for positional nodes, Bundle By Name above all.
The canonical order is whatever LabVIEW's own export writes, and NI's shipping code shows it
directly. From Device Under Test_Cloneable_DQMH.lvlib:DUT Status Updated.vi, three fields and
the cluster last:
<Node _name="Bundle By Name" fields="Module ID,Power State,Self Test Result"
inputs="Module ID:308.value,Power State:384.value,Self Test Result:356.value,input cluster:250.value"
outputs="output cluster:494.output cluster" uid="494" uid_parent="root"/>Do not generalise §3's "document order carries no meaning" to this. That rule is about the
order of elements in the file, which LabVIEW regroups on export. The order of terminals inside
an inputs attribute is a different thing and it is load-bearing for at least this node. Since
the failure disguises itself as a type error, the cheap habit is the same one §8 already
recommends for names: take the whole inputs string from an export of a VI that uses the node,
order included, rather than assembling it from a terminal list.
The rule is not special to Bundle By Name — plain Bundle obeys it too, and the corpus says
so without a single counter-example. Across 507 example exports, every Bundle lists its element
terminals first and cluster last, and every Bundle By Name lists its fields first and
input cluster last:
Bundle inputs element, element, cluster 28x (the commonest shape)
Bundle inputs Plant Output, SP\3A, cluster 5x
Bundle By Name inputs Message, Message Data, input cluster 3x
Unbundle inputs cluster 29x (nothing to order)
Unbundle outputs element, element 15x
Two details the table above also settles. Bundle's cluster terminal is called cluster, not
input cluster — that is Bundle By Name's spelling — and its output is output cluster in both.
And a plain Bundle's element terminal is named element only when what is wired to it has no
label; where the wire carries a labelled signal the terminal takes that label (Plant Output,
SP\3A). So the shape is per instance, and it is one more reason to copy the whole inputs
string from an export rather than assemble it.
Unbundle By Name was never affected — it has one input, so there is no order to get wrong.
Neither is Unbundle, for the same reason; its outputs repeat element once per field.
Consequence for design: a generated diagram CAN build a cluster. The old advice to route
around clusters — prefer scalar-terminal siblings, e.g. NI_AALBase.lvlib:Sine Wave.vi over
NI_MABase.lvlib:Sine Waveform.vi with its sampling info cluster — is still a reasonable
simplification when a scalar sibling exists, but it is no longer a necessity, and a palette VI
must not be rejected merely for taking a cluster.
_name is optional on Constant — anonymous constants are normal. Array literals are
written JSON-style in value, e.g. type="array{double}" value="[1.5,2.5,3.5]".
_name becomes the constant's block diagram LABEL, and that is the only handle anything has on
it afterwards — measured 2026-08-29. Nothing else distinguishes two boolean constants on one
diagram: {LV.Diagram} All Objects[] order is not stable across VIs, so an index does not survive a
regeneration. So when a constant feeds a subVI call, name it after the terminal it feeds:
<Constant _name="Borkenkaefer" type="bool" value="false" outputs="value:60.value" uid="60" uid_parent="root"/>That is what lets lvai_bind_typedef_constants find it later and re-point it at the typedef the
terminal expects — the repair for the coercion dots a placeholder-plus-retarget always leaves behind
on a typedef-carrying pane. docs/typedef-constants.md.
"JSON-style" does not extend to quoting string elements — and getting this wrong frames an innocent node. A string array literal is split on commas and each element taken literally, quote characters included:
type="array{string}" value= |
Elements produced |
|---|---|
["Zebra","apple"] |
"Zebra", "apple" — five characters plus two quotes each |
[Zebra,apple] |
Zebra, apple |
Measured, and it cost a redesign. A VI joining a sorted string array into lines produced
"Apple"\n"Mango"\n…, which was read as Array To Spreadsheet String quoting its fields
CSV-style — an entirely plausible story, since that node really does exist to build spreadsheet
text. The node was replaced with a For Loop + Concatenate Strings accumulator, and the output
came back still quoted. Only then was the test data itself the suspect. Array To Spreadsheet String had been innocent throughout.
The lesson is about attribution, not about arrays: when a node's output is wrong, verify the
input constant before rewriting the node. There is no delimiter and no separator character
inside value's element text — so an element containing a comma cannot be written this way at
all, and needs a Build Array of scalar constants instead.
\0A in a value gives a real LF. The escape table of §6 is documented against
comment/description/inputs, but it decodes in value too:
<Constant type="string" value="\0A"/> produced a genuine line feed — verified by running the VI
and reading the written file as bytes (31 bytes for five elements plus five LFs, no CR anywhere).
That is the portable way to get a line-ending constant onto a generated diagram.
For a primitive Node you look the terminal names up in the table above. For a Call there is no
table and there never can be one — the terminals are the target VI's own control and indicator
names, so every palette VI has its own set. Export the target and read them off:
ConvertVIToAIXML "…\user.lib\_OpenG.lib\string\string.llb\1D Array to String__ogtk.vi"
comes back as <VI _name="openg_string.lvlib:1D Array to String__ogtk.vi" …> with
Control _name="Array of Strings", Control _name="delimiter (Tab)" and
Indicator _name="delimited string". Those three strings are the whole wiring contract, and the
root element's _name is the target — colon escaped:
<Call target="openg_string.lvlib\3A1D Array to String__ogtk.vi"
inputs="Array of Strings:210.sorted array,delimiter (Tab):130.value"
outputs="delimited string:220.delimited string" uid="220" uid_parent="root"/>Validated, generated and ran on LabVIEW 2026. No instance — this VI is not polymorphic — and no
adapt, since its types are fixed.
Two things the export gives you for free. The _name is already the library-qualified form, so
there is nothing to assemble by hand. And the VI's description plus each terminal's
description come with it, which is the Context Help — for a palette VI that is the
documentation (§10), where a primitive gives you nothing but the terminal name.
Finding the file at all is the fiddly part: OpenG installs under user.lib\_OpenG.lib\, not
vi.lib, and the .llb in its path is a real directory here rather than a container. Search for
the VI by name across both roots rather than assuming either.
Create a constant for an input the callee marks required. Leave recommended and optional
inputs unwired unless you have a real value for them — an unwired input keeps the callee's own
default. lvai_vi_terminals prints the flag beside every terminal and names the required set
outright.
The reason this needs saying is that validation cannot teach it. AIXML enforces required and
says nothing at all about the rest, so wire whatever the validator demands behaves like a rule
and is not one: it is right only while every input you care about happens to be required. Measured
2026-08-29 — a second call to the same subVI was authored by mirroring the first call's wiring
without re-reading the flags; it was correct purely because the terminal was still required.
Changing that terminal to recommended produced no error and no warning anywhere, and the
mirrored constant became surplus.
A surplus constant is not harmless. On a typedef-carrying pane it also has to be typedef-bound
(§ docs/typedef-constants.md) and kept in step with the .ctl — so it is maintenance bought for
nothing.
Re-read the flags rather than trusting a cached view of them. They live on the callee's pane
and change when someone edits it. Note in particular that lvai_placeholder_subvi caches its stub
by a signature that includes connection, so a flag change does invalidate it — but a stub cached
by an older build of this server does not carry that in its key, and the symptom of a stale one is
an error blaming YOUR document: required input 'X' is not wired. refresh settles it.
A Call to a polymorphic VI names the concrete instance in a separate attribute:
<Call adapt="true"
instance="DAQmx Create Channel (AI-Voltage-Basic).vi"
target="DAQmx Create Virtual Channel.vi"
inputs="task in:,physical channels:100.value,minimum value:110.value,…"
outputs="task out:200.task out,error out:200.error out"
uid="200" uid_parent="root"/>targetis the polymorphic VI,instancethe selected member. Withoutinstancethe generator has no way to know which terminal set you mean.adapt="true"appears on calls whose type adapts to the wired data.- The terminals in
inputs/outputsare the instance's, not the polymorphic wrapper's.
Instance names follow LabVIEW's own convention and are worth copying from an export rather
than inventing — DAQmx Read (Analog 1D DBL NChan 1Samp).vi,
DAQmx Read (Analog 1D Wfm NChan NSamp).vi. A wrong instance name is reported by
ValidateAIXML, so it is cheap to check.
Some nodes carry boolean attributes for their right-click modes — Read from Text File has
convertEol and readLines. Setting one is not the whole story: readLines="true" with
count unwired still returns a scalar string, one line. The output only becomes
array{string.String} once count is wired, e.g. a -1 constant for "the whole file":
<Constant _name="count" outputs="value:15.value" type="int32" uid="15" uid_parent="root" value="-1"/>
<Node _name="Read from Text File" convertEol="true" readLines="true"
inputs="file (use dialog):10.value,count:15.value,error in:,prompt (Open existing file):"
outputs="refnum out:,text:20.text,cancelled:,error out:20.error out" uid="20" uid_parent="root"/>Wire the scalar form to an array indicator and ValidateAIXML says
You have connected a scalar type to an array of that type ... The type of the source is string.
— a precise message, but only if you are looking for a type problem rather than a mode problem.
The lesson generalises: when a node has modes, copy a variant that is in the state you want
from an export. A single specimen of the node does not reveal the type consequences of its modes.
Read from Text File in read-lines mode also needs no refnum handling at all — hand it a path
and it opens and closes the file itself.
Index Array and Replace Array Subset take dimensions="2", and their terminal names change
with which dimension you leave unwired: the unwired one is prefixed disabled . This is the
whole mechanism — disabling a dimension is not a formality, it is what turns "one element" into
"one whole row" or "one whole column".
NI's Replace Array Elements.vi puts all three side by side and labels each with an indicator,
which makes it the specimen to copy:
<!-- one element: both indices wired -->
<Node _name="Replace Array Subset" dimensions="2"
inputs="array:705.value,index (row):1367.value,index (col):1413.value,new element/subarray:1259.value" .../>
<!-- one ROW: the column is disabled -->
<Node _name="Replace Array Subset" dimensions="2"
inputs="array:705.value,index (row):1367.value,disabled index (col):,new element/subarray:543.value" .../>
<!-- one COLUMN: the row is disabled -->
<Node _name="Replace Array Subset" dimensions="2"
inputs="array:705.value,disabled index (row):,index (col):1413.value,new element/subarray:543.value" .../>Index Array follows the same shape and returns subarray rather than element once a
dimension is disabled. Terminal order still matters (§8): row before column, always.
The reason this is worth a section rather than a table row: §8 lists Index Array as "varies per
instance (18 shapes)" and aixml-node-gaps.tsv has no row for it at all, so the names are not
discoverable from either. Nothing warns you — a wrong name is the ordinary
Object terminal not found error, but guessing index (col) when you meant to disable it
silently indexes an element instead of a column.
§8's node table lists Build Waveform as "varies per instance (9 shapes)", which is honest and
useless — and a VI generator was measured guessing t0 from LabVIEW convention because nothing
here said it. It happens to be right, so here it is as a measurement instead. Generated, then
re-exported by LabVIEW unchanged:
<Node _name="Build Waveform" fields="t0,dt,Y"
inputs="waveform:,t0:54.Time Stamp,dt:53.s? t\3Af,Y:47.subarray"
outputs="output waveform:55.output waveform" uid="55" uid_parent="root"/>Three things the shape does not show:
fieldsselects which terminals exist, andinputsthen listswaveformfirst followed by one entry per field in the same order. Leavewaveform:empty to build a new one.t0is a Time Stamp, not a DBL, and there is no coercion. Wire a double and validation saysthe source is double, sink is Time Stamp. Convert withTo Time Stamp(number→Time Stamp).- Reading the result back, a Time Stamp's four I32 words are ordered fraction-low, fraction-high,
seconds-low, seconds-high — see
vi-server-reference.md, or everyt0looks like zero.
Continuous acquisition hands you one block of N channels per iteration, and the block has to be accumulated or all but the last is lost. The obvious shape does not work, and the failure is silent rather than an error.
Append Waveforms.vi has no array instance. lvai_palette_index returns exactly one candidate
(Categories\Programming\_WaveForm\AnalogWDT.mnu), and lvai_vi_terminals on
vi.lib\Waveform\WDTOps.llb\Append Waveforms.vi shows it is polymorphic with 7 instances, every
one of them scalar: waveform A, waveform B, error in (no error) → waveform out,
error out. So an accumulator of type array{doublewaveform} cannot be appended in one Call.
The correct shape is an inner For loop that auto-indexes the accumulator and the new block
together, one channel per iteration, calling instance="WDT Append Waveforms DBL.vi", with
waveform out auto-indexed back into an array. Rebuilding Y-array concatenation from primitives is
not needed and is the wrong instinct.
The empty accumulator makes that loop run zero times. Initialise the shift register with
<Constant type="array{doublewaveform.Waveform}" value="[]"/> — that type is accepted in Constant
position, measured — and iteration 0 indexes an empty array against an N-element block, so the inner
For loop runs min(0, N) = 0 times and the accumulator stays empty forever. Nothing reports
this: it validates, it runs, and the output is an empty array that looks exactly like a device error.
Guard it with a seed case (Empty Array? → Select, or a Case on the accumulator's size) that takes
the first block as-is.
Worth a third frame while you are there. If a late read fails and hands the append an empty
block, the same zero-iteration arithmetic wipes everything already accumulated. A three-way
selector — seed / append / keep — returns the partial data instead, which is what a caller reading
error out alongside the waveforms will expect.
The symptom that led here: a generated DAQ-to-TDMS VI wired DAQmx Read's data straight to the
output terminal, so four of five blocks were discarded. The user spotted it in the block diagram —
neither validation nor a run can see it, and on a station without a device the empty output is
indistinguishable from the expected -200220.
The polymorphic file reader is the standard answer to "load a CSV", and three of its behaviours are the kind that get tested by hand every time because nobody wrote them down. All measured on LabVIEW 2026 through the DBL instance:
-
The default
formatdoes not truncate on scan.format (%.3f)left unwired reads six decimals back intact —1.234567,-2.718281,3.141592and-0.000123all survived exactly. The%.3fgoverns writing, not the precision of a read, so leave it unwired. -
A trailing newline does not produce a ghost row. An eight-data-line file yields
Dimsize 8, a four-line fileDimsize 4. No trailing empty element to trim. -
There is no header-skip option. The header line scans to
0.0like any unparseable text, so drop it by index — oneArray Subsetfrom index 1 per column. -
Prefer DISABLING the row index over transposing. This paragraph used to advise
transpose? = TRUE, after which "row 0 is the whole first column and row 1 the whole second". That works, but only if the boolean constant feeding it is spelledvalue="true"— lowercase — and getting it wrong is silent. See §11, "The VI runs, reports no error, and computes the WRONG ANSWER":value="TRUE"reads asfalse, the file is then read untransposed, and "row 0" is the two-element header row rather than a column.Indexing the column directly avoids the boolean entirely, and is therefore the shape to copy:
<Node _name="Index Array" dimensions="2" inputs="array:40.all rows,disabled index (row):,index (col):23.value" outputs="element:41.element" uid="41" uid_parent="root"/>
Verified end to end on a nine-row two-column CSV with
transpose? (F)left unwired: column 0 gave the time series, column 1 the amplitudes, and afterArray Subsetfrom index 1 the waveform carried all eight samples withdt = 0.1.This trap has now been sprung twice, which is why the advice is inverted rather than merely annotated. The second run, 2026-08-13, saw
Ywith one element anddt = 0, concluded "transpose has no effect", and switched approach without recognising the boolean-literal cause that §11 already had in writing — a documented fact re-derived at the cost of a debugging round. The cross-reference above is the fix for that: the symptom is named here, where a CSV reader gets built, instead of only under the failure mode.
The dangerous one is not here but in §11: its file path (dialog if empty) input opens a modal
dialog on an empty path and stops the whole gRPC session.
A Ring carries its items and their numeric values as two parallel attributes:
<Control _name="Terminal Configuration" style="Ring" type="int32"
items="default,RSE,NRSE,Differential,Pseudodifferential"
values="[-1,10083,10078,10106,12529]"
value="-1" outputs="value:2154.value" uid="2154" uid_parent="root"/>Note the difference from an enum, which encodes its labels inside the type
(uint8{Label A,Label B}, §5): a Ring keeps type plain and lists the labels separately,
because its values need not be consecutive.
type="doublewaveform" alone produces the cluster display — t0, dt and the Y array as
three fields. To get a Waveform Graph the indicator needs style="graph21703":
<Indicator _name="waveform" style="graph21703" type="doublewaveform"
inputs="value:61.output waveform" uid="80" uid_parent="root" value="[0,0,[]]"/>An empty waveform-array CONSTANT is legal, and it is how you type an accumulator.
<Constant type="array{doublewaveform.Waveform}" value="[]"/> validates and generates, so a shift
register collecting waveforms can be initialised from it rather than left uninitialised — measured
while regenerating DaqReadAndTDMS.vi, where the accumulator carries the read blocks out of the
loop. Note the element name inside the braces: doublewaveform.Waveform, not bare
doublewaveform.
The token is an internal identifier, not a name, and nothing tells you when you get it
wrong. WaveformGraph, Waveform Graph and Graph are all plausible, all wrong, and all
fail without a word: ValidateAIXML returns errorCode 0 for every one of them, generation
succeeds, and the VI comes back with a cluster on the panel. Measured in one round trip — five
indicators of the same type, four spellings plus a control:
| written | survives the round trip? |
|---|---|
style="WaveformGraph" |
no — attribute absent on export |
style="Waveform Graph" |
no — attribute absent on export |
style="Graph" |
no — attribute absent on export |
style="graph21703" |
yes |
no style at all |
(baseline: cluster display) |
The symptom to recognise: you asked for a graph, validation was clean, the VI generated, and
the panel shows a cluster. There is no error to search for — the only evidence is the missing
attribute in a re-export, so export the VI you just generated and grep for style=.
graph21703 is not invented here: it is what LabVIEW itself writes when exporting a VI that
has a Waveform Graph on it — NI's own Feedback Node with Graph example exports it twice. That
is also how to re-derive it after a LabVIEW upgrade, and how to find the token for any other
front-panel style: drop the control by hand, export the VI, read the attribute. Whether the
21703 suffix is stable across LabVIEW versions has not been measured; it was taken on
LabVIEW 2026.
Charts and XY graphs are untested — do not assume the token generalises. The style vocabulary
confirmed so far is small: latched (boolean), Ring (§ above), graph21703.
LabVIEWMCP --corpus exports every VI in a tree and hands each export straight back to
ValidateAIXML; scripts/aixml_corpus_report.py mines the exports afterwards. That is the
standing way to re-derive this section after a LabVIEW or addon update, and it replaces the
13-VI corpus the rest of this document was built on. Run it before trusting anything below.
The full run over LabVIEW 2026: 1687 VIs, 1679 exported, 627 round-tripped — 37 %.
Two numbers frame everything else:
- 8 VIs could not be exported; 1052 exported and then failed to validate. Reading a VI out is close to always possible; reading it back in is where the gaps are. So a construct being visible in an export is no evidence at all that it can be generated.
- 377 distinct node kinds, 313 of them not named anywhere in this document. The terminal table
in §8 is a small fraction of the vocabulary NI actually uses.
undocumented.tsvfrom the report is the working gap list, most frequent first —Static VI Reference,Build Path,New VI Object,To More Specific Class,Open VI Object Reference,Format Into String,Feedback Node,In Range and Coercewere the first eight. That list is checked in asdocs/aixml-node-gaps.tsv, with each node's commonest orderedinputsandoutputsbeside it, so a node absent from §8 still has a spelling to copy. Regenerate it rather than editing it.
Every failure, classified once each:
| Cause | Count | Reading |
|---|---|---|
Error 53 — a Call to a project- or library-local subVI |
737 | the documented boundary below. Expected, and it dominates everything else: two thirds of NI's examples call their own subVIs |
Error 1 … An input parameter is invalid, no further detail |
146 | the generator refuses the document and does not say why. Unexplained |
Event Data Node / no events defined |
54 | the event registration is lost — see below |
other validation errors (type mismatches, unwired terminals on Array Index / Replace Elements, Feedback Node, Global Variable, …) |
51 | one-offs, each worth reading on its own |
Static VI Reference 'X': SubVI is missing |
31 | a static VI reference does not survive the trip |
Property Node / Invoke Node / Constructor Node: invalid property or method |
23 | the VI Server name is not rebound |
excluded: the project targets RT Generic |
8 | not attempted; out of scope for a plain LabVIEW |
| LabVIEW unavailable or too slow | 6 | see §"What the sweep has to survive" in the README |
Error -2628 — missing required attribute |
4 | a malformed document |
The headline is that Error 53 is not a defect and everything else is small. Excluding it,
1687 VIs produce 315 real round-trip failures — so the format handles NI's own code far better
than the 37 % headline suggests, and the single biggest constraint on generating LabVIEW code
remains the one already known: a generated VI cannot call your own subVIs.
The event-structure row is worth spelling out, because §7 lists event structures as working and
that is only half true. Of 160 exports containing an Event Structure, 87 fail for reasons that
have nothing to do with events (Error 53, mostly), and of the 73 that do return a verdict on the
event structure itself:
| Frame kind | passes | fails |
|---|---|---|
static — a control's event, selector=" "Exported VI"\3A Value Change " |
12 | 48 |
| dynamic — a user event through a registration terminal | 9 | 4 |
The static frames are the fragile ones, which is the reverse of the obvious guess. The selector
comes back intact, spaces and all, and the generator still reports Event Structure: One or more event cases have no events defined with an Event Data Node: Cluster is invalid or empty behind
it. The plausible reading is that a static frame names its control as text and the generator
never rebinds it, while a dynamic frame's event arrives structurally through the wire from
Register For Events — but that is inference, and only the counts above are measured.
Practical consequence: before planning any edit to a VI with a front-panel event structure, validate the untouched export. Four in five of NI's own do not come back.
Structure kinds over the whole corpus: Case Structure, While Loop, For Loop,
Event Structure, In Place Element Structure, Flat Sequence Frame, Stacked Sequence Structure. The In Place Element Structure is the one to know about beyond §7 — it is how NI
modifies a cluster or array element without a copy, and its border node is
Unbundle / Bundle Elements (§8).
Round-trip validation (ValidateAIXML) of the 13-VI corpus: 11 passed, 2 failed. All
failures share one cause.
Works: every primitive Node seen (arithmetic, comparison, array, cluster, queue,
notifier, user-event, property node, local variable, Merge Errors, Wait (ms),
Variant To Data), all four structure kinds, shift registers, nested cases, event
structures, all type constructs above, FreeLabel.
Fails: a Call whose target is a project- or library-local subVI:
Error 53 ... Manager call not supported.
Errors:
Unsupported SubVI: MyLib.lvlib:Helper.vi
Object terminal not found for input: parameter 0:4971.value on Call
The second line is a knock-on effect: an unresolvable target has no terminals, so its
wires fail too. Express VIs (Ex_Inst_*.vi) fail the same way.
The boundary is palette-reachability, not library membership — now isolated. The trick is
that the error message itself discriminates: Unsupported SubVI means the target was never
resolved, while Object terminal not found means it was resolved and only the terminal name
was wrong. Feeding a deliberately bogus terminal name therefore probes resolution alone:
Call target= |
Result | Reading |
|---|---|---|
General Error Handler.vi (vi.lib, on the palette) |
Object terminal not found for input: bogusTerminalName |
resolved |
ScratchEdit.vi (a plain .vi on disk, in no library) |
Unsupported SubVI: ScratchEdit.vi |
not resolved |
an absolute path, C\3A\5CTemp\5C…\5CX.vi |
Unsupported SubVI: C:\Temp\…\X.vi |
not resolved |
<Module>.lvlib:X.vi, a project library loaded in the IDE |
Unsupported SubVI: <Module>.lvlib:X.vi |
not resolved |
openg_array.lvlib:Filter 1D Array__ogtk.vi, a palette library |
validated, generated and ran | resolved |
Draw Image from File__ogtk.vi, the bare name of that same kind of VI |
Unsupported SubVI: Draw Image from File__ogtk.vi |
not resolved |
There is therefore no target syntax that reaches your own code — not a bare name, not a full path, and not a library-qualified name even while that library is open in LabVIEW.
Measured 2026-08-27, eight probes, all by the bogus-terminal trick above. The rule this section had — "palette reachability" — is a consequence of the real one and is wrong in both directions: it excludes targets that do resolve, and it does not explain the ones that do not.
Call target= |
where the VI lives | Result |
|---|---|---|
Caraya.lvlib\3AVI Name.vi |
vi.lib, library member, in no palette |
resolved |
openg_variant.lvlib\3AconvertClusterToObject__ogtk.vi |
user.lib, library member, in no palette |
resolved |
Test Addition.vi |
vi.lib, plain folder, no library, no palette |
resolved |
LVMCP Socket 1x1 q7k2.vi |
user.lib, plain folder, freshly generated, no library, no palette |
resolved |
Inverse Error Function_Estimation.vi |
vi.lib, inside SpecialFunctions.llb, no library, no palette |
not resolved |
ProbeLib.lvlib\3ACelsius To Fahrenheit.vi |
a project library, hand-written .lvlib |
not resolved |
LV_MCP\5CPlaceholder 1 in 1 out.vi |
the resolving VI above, addressed by relative path | not resolved |
\3Cuserlib\3E\5CLV_MCP\5CPlaceholder 1 in 1 out.vi |
the same, by symbolic-root path | not resolved |
So the target is resolved by name, against what the installation can find:
- a loose VI in a plain folder under a LabVIEW symbolic root — bare file name. A palette entry
is irrelevant. This is what makes a generated placeholder dropped into
user.libcallable with no.lvlib, no.mnuand no LabVIEW restart. - a library member — the library-qualified name, nested for classes
(
Caraya.lvlib\3AAssert.lvclass\3AAssert Equal Value_Variant.vi). Again no palette entry needed. - inside an
.llb— the bare name does NOT reach it..llbcontents are exposed through the palette or through the owning library, not by name alone. This is what the old rule was really observing: most palette VIs live in.llbs. - a path — never, in any spelling. Relative, absolute and symbolic-root forms were all refused.
- project-local code — never, loose or in a project library, palette or not.
Two consequences worth stating separately, because each was a wrong belief that cost work:
A miss in lvai_palette_index is not proof that a Call is illegal. The index scans menus\
and LVAddons\; it does not see Caraya at all (its .mnu files live under
vi.lib\addons\_JKI Toolkits\dynamic_palette\), yet every Caraya target in labview-unit-testing.md
validates and runs. Query the index to find a VI; settle a target spelling with a throwaway probe.
A hand-written .lvlib does not make a generated VI a library member. Dropping a .lvlib next
to a generated VI in user.lib and listing it produced Unsupported SubVI, and still did with the
library loaded in an open project. A real member carries LIvi/LIbd ownership blocks that only
LabVIEW writes when it saves the library; a generated VI has none. Do not build on library
membership for anything the generator produces — put the VI loose in a plain folder instead, which
resolves anyway.
A library-owned palette VI needs the lvlib: prefix, and lvai_palette_index does not print
it. The index lists bare file names, so following it literally — "query the index, then Call the
hit" — produces Unsupported SubVI for every VI a palette library owns, which reads exactly like
"this VI is not callable" and sends you back to rebuilding from primitives. It is the last two rows
of the table read together: the same VI fails bare and resolves qualified. Measured 2026-08-07 on
LabVIEW 2026 — target="Draw Image from File__ogtk.vi" was refused,
target="openg_picture.lvlib\3ADraw Image from File__ogtk.vi" validated, generated and ran.
A polymorphic VI needs adapt and instance as well as target. Its own export is nothing
but one Call per instance, so the terminal names to use are the instance's, not the
polymorphic wrapper's. Measured 2026-08-07 generating SinusFFT.vi against
Extract Single Tone Information.vi, which is NI_MAPro.lvlib-owned and has four instances:
<Call adapt="true"
target="NI_MAPro.lvlib\3AExtract Single Tone Information.vi"
instance="NI_MAPro.lvlib\3AExtract Single Tone Information 1 Chan.vi"
inputs="time signal in:40.output waveform,export mode:,error in (no error):12.value,advanced search:"
outputs="exported signals:,measurement info:,detected frequency:50.detected frequency,detected amplitude:,detected phase (deg):,error out:50.error out"
uid="50" uid_parent="root"/>Two details that are easy to get wrong:
- List every terminal of the instance, wired or not, and give the unused ones an empty net
(
export mode:,advanced search:,detected amplitude:). Omitting them is what producesContains unwired or bad terminal. - Get the instance name by exporting the polymorphic VI itself: the export is the instance
list, one
Callper line, with each instance's inputs and outputs spelled out. Two calls —lvai_convert_vi_to_aixmlon the wrapper, then copy — replace all guessing.
The qualifier is not derivable from the index either: the palette prints
Categories\OpenG\functions_oglib_picture.mnu and the VI lives in picture.llb, neither of which
names openg_picture.lvlib. Get it the way §9 already recommends for the target itself — export a
VI that calls it, where the _name comes back in the library-qualified form. Cheaper still: put
both spellings in one throwaway probe document, one Call each, and validate once. Unresolvable
targets are reported by name, and a resolved one only complains about its unwired terminals, so a
single errorCode 1 message tells you which spelling to use.
The boundary is palette reachability, not library membership. An earlier version of this section read the fourth row as "library-qualified targets do not resolve" and concluded that only library-free
vi.libVIs are callable. That is wrong, and it costs real work: it argues away the 336 OpenG, 208 MGI and 63 JKI palette entries on a station that has those packages. Measured 2026-08-06 on LabVIEW 2026 — aCallwithtarget="openg_array.lvlib\3AFilter 1D Array__ogtk.vi"andinstance="openg_array.lvlib\3AFilter 1D Array with Scalar (String)__ogtk.vi"validated, generated, ran, and produced output byte-identical to the same filter hand-built from a For loop, a Case structure, a shift register andBuild Array. Three nodes instead of seven elements.The failing row is a project library — a library belonging to the code you are editing. Both rows are true; the difference is not the
.lvlib.And it is not the palette either — that was this section's second wrong answer, corrected 2026-08-27 in "Palette reachability is NOT the boundary" above. The difference is whether the installation can find the VI by name at all. A project library cannot be found that way; an installation library can, palette entry or none.
So reuse first. Query
lvai_palette_indexfor the operation before designing a diagram, and rebuild from primitives only when the target genuinely does not resolve. When reuse costs a third-party dependency, name it and let the caller decide — the generated VI will not open on a machine without that package.
This is not a DQMH quirk — it stops NI's own example applications too. Temperature Monitoring.vi from examples\Industry Applications\ exports cleanly (39.6 kB, errorCode 0) and
then fails regeneration with ~30 Unsupported SubVI errors, hitting both forms at once: its two
project libraries (… Message Queue.lvlib:Enqueue Message.vi) and its five loose support VIs
referenced by bare name (Simulate Temperature Acquisition.vi). Any application organised the way
real LabVIEW applications are organised — support VIs in a folder, a couple of .lvlibs — is
outside regeneration. Treat "export succeeded" as saying nothing whatsoever about whether the same
XML can be generated back: the two directions are independent, and reading is the one that works
broadly.
Escape the backslashes in a path target, or debug a phantom.
targetis an ordinary AIXML attribute value, so\starts an escape (§6) — a literal backslash is\5C, a colon\3A. WritingC:\Scratch\Demo\Libraries\…raw silently mangles the string, and the error then reports the corrupted path —Unsupported SubVI: C:cratchemoibraries— which looks like a resolution bug and is really a quoting bug. Escaped correctly the path arrives intact; it simply still does not resolve.
So a generated VI may freely call the palette — every vi.lib utility by bare file name, and every
palette VI owned by a library by its qualified name. What it may not call is your code:
project-local, library-local to the project you are editing, and even a loose .vi sitting in a
directory.
lvai_palette_index answers which names those are. It reads the installed LabVIEW's own
menus\*.mnu palette files, so the set is the one this station actually has — installed toolkits
hook themselves into the palettes, so it is not a fixed list. Measured on a stock LabVIEW 2026:
460 palette files, 2 202 reachable VIs, against 19 322 .vi files in vi.lib — so roughly
one file in nine is a legal Call target, and guessing from the filesystem is nine times more
likely to be wrong than right. Built-in functions are deliberately absent from that index: they
are Node elements, not Calls, and a palette entry for one carries only its display label,
which is not the AIXML node name (To XML on the palette, Flatten To XML in AIXML).
Two consequences worth stating plainly:
- Generated VIs cannot call each other. A VI this server just produced is not
palette-reachable, so it is not a legal
Calltarget for the next one. There is no way to build a hierarchy of generated code. - "No subVIs" is the wrong mental model. Do not strip subVI calls out of a design; keep the palette-reachable ones, which covers a great deal — error handling, file and string utilities, timing, the instrument and DAQ palettes.
Practical consequence: generated VIs must be self-contained with respect to your own code. Build them from primitives plus the palette. A VI that must call your own subVIs cannot be produced this way.
NI publishes a "not-yet-supported" list for the LabVIEW Coding Agent — the same generator
these RPCs drive. It constrains generation only: everything below still reads fine
through ConvertVIToAIXML, which is how this document was written in the first place.
Program types and domains
- external-language interop wrappers (Python, C#)
- VIs with complex UI or front-panel design requirements
- FPGA-targeted and Real-Time-targeted VIs
- SCPI and serial VIs needing robust parsing, framing or session patterns
- QMH with Event Structure, DQMH, Actor Framework
- plugin generation other than FlexLogger (VeriStand, measurement plugins, VI Analyzer tests, CLI commands)
- VIs that depend on user VIs outside the supported LabVIEW node catalog
- new polymorphic VIs, new malleable VIs
- custom controls or typedefs (
.ctl), Global Variable VIs - LabVIEW libraries (
.lvlib), classes or interfaces (.lvclass), XNodes, XControls, project files (.lvproj) - non-default VI properties beyond basic
description - VI icon graphics
- connector pane layout or wiring
Node families: Timed Loop, Event Structure.
How this squares with the measurements above:
| Measured here | NI's wording |
|---|---|
Unsupported SubVI for project-local and Express VIs, while a vi.lib VI resolved |
"user VIs outside the supported node catalog" — catalogue membership, not library membership or palette presence. Use the probe in the table above to test a specific target. |
conIdx survives generation. A VI authored with conIdx 0/1/2 in and 5/6 out came back from a fresh export with those indices intact — so terminals really are assigned to the connector pane. connection is dropped only when no conIdx accompanies it. |
"connector pane layout or wiring" is not supported. Read that as the pane pattern and its wiring, not the index assignment, which demonstrably works. |
description survives; nothing else was attempted |
"non-default VI properties beyond basic description" |
An icon can be applied after generation. A generated helper VI calling Set VI Icon from File + Save\3AInstrument, driven by RunVIAsTopLevel, replaced a generated VI's icon with a 32×32 PNG; the read-back was pixel-identical. Recipe in vi-server-reference.md. |
"VI icon graphics" — true of the generator. The icon is not out of reach, only out of AIXML. |
| Event structures export correctly and their syntax is documented in §7 | they are not generatable. §7 is a reading aid for them, not an authoring recipe. |
A DQMH module needs .lvlib + .ctl + .lvclass + cross-calling VIs |
each of those four is independently on the list |
Two practical consequences. Generated VIs stay self-contained and flat: primitives, loops, case structures, shift registers, typed front-panel terminals — no custom types, no calls into your own code, no event handling. And a failure is not always a bug in your XML: check this list before debugging.
How the two kinds of refusal differ — this matters more than the list itself:
Unresolvable Call |
Unsupported node family | |
|---|---|---|
ValidateAIXML |
errorCode 1, Unsupported SubVI: X |
errorCode 0 |
ConvertAIXMLToVI |
refuses, no VI written | errorCode 0, VI written |
| Result | nothing | container built, configuration silently discarded |
So the list's entries are not equally visible. A Call tells you. A node family does not.
Putting an unresolvable Call inside the Disabled diagram of a Diagram Disable Structure
changes nothing. Measured with a control — the identical call at root level and inside a
disabled diagram produce the same message, Unsupported SubVI: <name>.
The reason is not arbitrary: disabled means excluded from execution, not absent. The code still exists on the diagram, so the generator must still instantiate the node, and resolution happens before any question of compilation. Deleting the call and substituting constants for its wired outputs does avoid the error — but see the silent-degradation row in §11 before concluding that the result is what you wanted.
ConvertVIToAIXML read an existing VI as text (does not modify the VI)
ValidateAIXML check without creating anything ← always run this first
ConvertAIXMLToVI create a new .vi works
ApplyAIXMLToVI modify an existing .vi unusable, see below
The reliable loop when authoring something new: export a VI that already contains the construct, copy its exact shape, edit, validate, generate. Validation is cheap and its messages are specific enough to work from.
An export is not cheap, and the cost is not where it looks. Measured over the whole examples
tree of LabVIEW 2026 — 1677 VIs, one export-and-validate pair each (--corpus, results in
roundtrip.tsv):
| export + validate | export size | |
|---|---|---|
| median | 331 ms | 4.1 kB |
| p75 | 1.0 s | 7.4 kB |
| p90 | 3.3 s | 14.6 kB |
| p95 | 6.6 s | 25 kB |
| p99 | 24 s | 161 kB |
| max | 93 s | 5.8 MB |
| total | 52 min | 28.8 MB |
Size and duration are uncorrelated: r = 0.002 over those 1677 rows. The 5.8 MB export
(MATLAB Node\Cell Phone Towers\SubVIs\Set Up Objects.vi) finished in 1.9 s; the slowest VI took
93 s and produced 20 kB. So the time is not spent writing XML — it is spent loading the VI and
its dependencies, which is why the slow ones are the DQMH libraries, the malleable VIs and the
3D graph controls. Two consequences, and both are counter-intuitive enough to be worth stating:
a big export is not a slow one, and no amount of memory inside the MCP server can avoid the cost,
because the work happens in LabVIEW.
lvai_convert_vi_to_aixml therefore caches exports on disk:
%USERPROFILE%\.labviewmcp\cache\aixml\<hash>.xml the export
%USERPROFILE%\.labviewmcp\cache\aixml\<hash>.json its sidecar: full key, source VI, time, size
Not under %LOCALAPPDATA%, and this document said otherwise for a while. The cache moved out of
AppData because a server started by the Claude desktop app inherits that packaged app's filesystem
redirection: every level created under %LOCALAPPDATA% becomes a reparse point into the package's
private store, so the same machine ended up with two caches depending on who launched the server.
CacheDirectory.Root carries the measurement; LABVIEWMCP_CACHE_DIR overrides it. The stale path
cost real work on 2026-08-13 — a session looked there, found nothing, reported "no AIXML cache on
this machine" and stopped using it, while 2 382 exports sat in the real location.
The key is the VI's normalised path, its last-write time and its size; <hash> is an MD5 of that.
The sidecar is written last and is the commit record, so a crash between the two writes leaves
an orphan payload that reads as a miss rather than as an entry that lies. A hit is two file reads
and a copy, and needs no running LabVIEW — an example stays readable while the IDE is closed
or still starting.
Only VIs belonging to the LabVIEW installation are cached — the examples tree, vi.lib,
user.lib and every LVAddon, which is exactly the example and palette VIs you read to learn from.
Your own code never is. The reason is the one thing the key cannot see: an export depends on
the VI's subVIs as well, and user code changes one subVI at a time behind a caller whose own
timestamp never moves — a stale entry there would be silent and wrong. Installation trees do not
have that problem: they change when something is installed or upgraded, which rewrites the files,
and an upgrade lands in a new versioned directory anyway.
Every answer carries fromCache and a one-line cacheNote, so which of the two happened is
visible rather than guessed. Pass refresh to re-export a cached VI. A failed export is never
stored — it can leave a partial file behind, and caching that would serve the failure back for as
long as the VI sits untouched.
Pre-exporting the whole corpus is deliberately not done. It is 52 minutes of LabVIEW time, and the sweep that measured it had to recycle LabVIEW along the way: about 130 handles leaked per VI, 116 000 handles and 1.3 GB after 900 of them, and one VI killed the process outright. Entries are written as they are asked for.
Measured, and worth knowing before designing around it. Six ConvertAIXMLToVI calls for the
same small VI, issued together against one channel, versus the same six one after another — two
rounds each, alternating:
| 6 VIs | per VI | |
|---|---|---|
| sequential | 543 ms | 90 ms |
| concurrent | 559 ms | 93 ms |
| sequential | 507 ms | 84 ms |
| concurrent | 539 ms | 90 ms |
Concurrent is a hair slower. With any real overlap six calls would finish in roughly the time of
one or two; instead they finish in the time of six. LabVIEW serialises the work, and this is
not our client getting in the way: the semaphore in LvaiConnection guards only the connection
setup, and once the channel is up the calls go out unimpeded. It matches the other measurement
from the corpus sweep, where a single slow export left every later RPC queued behind it.
Consequences: never fan out lvai_* calls hoping for throughput — you get the same total, plus the
risk that one slow VI blocks the rest. Real RPC parallelism would need several LabVIEW instances,
each with its own port, which is expensive (the handle leak above, ~1 GB apiece) and fragile (the
service starts with Nigel, not with the IDE).
What does parallelise is anything that never reaches LabVIEW — reading files. That is exactly what the export cache turns candidate triage into.
lvai_convert_vis_to_aixml takes a list of VIs, one path per line, and writes them all into one
directory. It exists for the question "which of these examples is the right starting point", where
the alternative is one tool call per candidate.
It splits the work along the line the measurements above draw:
- cached exports are served concurrently — a hit is a file copy in the MCP server's own process, no LabVIEW involved, and concurrent reads are worth about 21x cold;
- everything else is exported sequentially, because LabVIEW would serialise it anyway.
So the first pass over a candidate set costs what it costs, and every later pass is nearly free.
Content is not returned inline by default — a batch of full exports is large, and the point is
to get the files onto disk cheaply so you can read the few worth reading; each row carries its
xmlPath.
Two details that are easy to get wrong and are handled here: output files are named
<VI name>.<hash>.xml, because Read Data.vi exists in several example folders and the leaf name
alone would have one silently overwrite another; and the path list is split on newlines only,
not on commas, because a comma is legal in a Windows path (C:\Data\Rev 2, final\) while a line
break is not.
CLAUDE.md has long said that the cost of a generation is round trips rather than LabVIEW, on the
strength of one session where three lvai_* calls took 30.4 s while LabVIEW's own share was 74 ms.
That was right but thin. Here is the whole profile, taken from subagent transcript timestamps —
which costs nothing and, unlike having the agent time itself, does not add the very round trips
being measured. Scripts: tprof.py / tphase.py in the session scratchpad; attribution is
tool exec = tool_result.ts − assistant.ts and model turn = assistant.ts − previous.ts.
One fresh, complete generation of a DAQmx-to-TDMS VI by labview-vi-generator, 41 tool calls,
455 s active (idle gaps > 60 s excluded):
| time | share | |
|---|---|---|
| text-only turns — reasoning, plans, the final report | 240.3 s | 52.8 % |
| model time in turns that end in a tool call | 146.3 s | 32.2 % |
| tool execution — LabVIEW and the MCP server together | 72.2 s | 15.9 % |
A second profile over 54 calls (a generation plus a correction round) agrees: 89.7 s of tool execution against 634 s of model turns, 12.5 % vs 88.1 %.
So the unit worth counting is a tool call, and it is worth about 11 s of wall clock (455 s / 41)
regardless of what the call does. That reframes the batching rule in CLAUDE.md as arithmetic
rather than advice:
lvai_aixml_reference, 6 calls in that run, 0.1 s of server time in total — the document cache did its job — but 6 turns, so roughly 66 s of wall clock. Five of them were avoidable.lvai_palette_index, 3 calls, again 0.1 s total, ~33 s of wall clock.
Batching saves nothing measurable on the server and everything on the clock. A cache on these calls would be pointless twice over: they are already sub-millisecond, and no two arguments repeat.
No single lvai_* call exceeded 5 s. The expensive ones are exactly the ones that make LabVIEW
work — ConvertAIXMLToVI 3.9 s, lvai_connector_pane with viPath 4.2 s,
lvai_run_vi_and_read_values 4.0 s, lvai_set_vi_icon 3.6 s — and the read-only indexes are free.
LabVIEW is not the slow part; it is a sixth of the total.
By phase, over the 218.5 s that attributes to a tool call:
| phase | calls | model s | tool s | total |
|---|---|---|---|---|
| authoring the AIXML — write, edit, validate, convert | 10 | 84.2 | 24.5 | 108.7 s |
| research — palette, examples, references | 23 | 40.9 | 16.1 | 57.0 s |
| verification — run, pane, project | 6 | 10.5 | 21.2 | 31.7 s |
| icon | 2 | 10.7 | 10.4 | 21.1 s |
Bucket by what a call did, not by which tool ran it. A first cut of this table charged every
Bash and PowerShell call to the icon and reported it at 48.1 s — a fifth of the run — and that
number was quoted onward before it was checked. Reading the actual commands back out of the
transcript showed three of those Bash calls were sed/python edits to the AIXML. The icon is
21.1 s, under a tenth; authoring is half the run. Dump the command strings, do not trust the
tool name.
Research is 23 of 41 calls, most individually free, which is where batching has room.
Inside the icon's 21.1 s the split is worth knowing before anyone tries to optimise the VI Server
side of it: 12.5 s goes on drawing the PNG (a PowerShell call through System.Drawing, 6.8 s
of it, plus 5.7 s of model time composing that call) and only 8.6 s on lvai_set_vi_icon — which
opens the VI reference, invokes Set VI Icon from File, Save:Instrument and Save VI Icon to File,
and closes the reference, all in one helper run of 3.6 s. The write-and-read-back chain is already
minimal; the cost is producing the image, and that never touches LabVIEW.
The other lever is not a call count at all: the largest single turn in the run was 40.3 s of
reasoning and the final report cost 30.1 s for 6 001 characters. Over half the wall clock is
the model thinking and writing prose. Prescribing what the agent would otherwise derive — the
station's conIdx set, the polymorphic instance name, the accumulator shape — buys more than any
change on the LabVIEW side can.
A worked example of that, from the same run: the agent authored TDMS file path on conIdx 1, a
middle-column slot, and lvai_connector_pane flagged it — costing an extra ConvertAIXMLToVI and
an extra pane measurement plus their turns, some 30 s, to arrive at the value the station's
DefaultConPane=4833 already fixed before the run began.
The shipped .chm files are stubs — help\glang.chm, the LabVIEW Function and VI
Reference, is 14 kB and hh.exe -decompile extracts nothing from it. The real help is online.
Locally there is a better source anyway: an AIXML export carries the Context Help. A palette
VI exports its own description plus a description on every Control/Indicator, with
default values baked into the terminal names. General Error Handler.vi came back with all 15
terminals documented, e.g.
type of dialog (OK msg\3A1) "type of dialog determines what type of dialog box to display, if any."
error out "...<a href="nihelplauncher\3A//docs/csh?context=lvcore..."
— the nihelplauncher://docs/csh?context=… id even names the online topic. So one export gives
terminal names and their meaning.
This does not extend to primitives. Sort 1D Array, Read from Text File and friends are
nodes, not VIs: an export gives their terminal names and nothing else. For those, the only
documentation is the terminal name itself and LabVIEW's online help.
The RPC moves control values as strings through a variant, which constrains the connector pane of any VI you intend to drive this way. Measured, all three on LabVIEW 2026:
| Terminal | Result |
|---|---|
string in, string out |
works |
path in |
Error 91 ... Control Value\3ASet — the variant will not coerce string to path, and it fails before the VI runs (inputsSent counts the attempt) |
array or cluster out |
Error 91 ... Variant To Data — the VI has already run correctly; only the read-back fails, and the outputs come back as empty strings |
bool, int32 or cluster out |
Error 91 ... Variant To Data, and that one indicator comes back empty. Marshalling is per indicator, not all-or-nothing: one response carried a string indicator's full value while status (bool), code (int32) and error out (cluster) were all blank. So errorCode 91 means "at least one output could not be read" — never "the VI failed". |
double in |
Error 91 ... Control Value\3ASet — the same wall as path, and it also fails before the VI runs. Measured twice on the same control, as the JSON string "100" and as the JSON number 100: neither coerces to a DBL. This contradicts lvai_run_vi_as_top_level's own description, which says to pass numbers as their text form; that does not work. Numeric controls cannot be driven at all — take the number in as string and convert on the diagram. |
There is now a way out of the read-back half of this, and it is a shipped tool.
lvai_run_vi_and_read_values sets the inputs, runs the target and reads every control and
indicator back through VI Server, flattening them to XML so the whole result crosses as one
string. A boolean, cluster, array or waveform output is fully readable through it; measured on
a VI whose three outputs — waveform, bool, error cluster — were all blank under plain
RunVIAsTopLevel, and came back complete (dt = 0.1, eight Y samples, loaded? = 1) with
errorCode 0. The advice below still governs what you can send in, and still applies to any
helper you drive directly; it no longer forces you to design outputs around the marshaller.
Which operations burn a path for regeneration — measured in one isolated A/B run. Same trivial VI, same path, one LabVIEW session, regenerating after each step in turn:
| done before regenerating | ConvertAIXMLToVI |
|---|---|
| nothing (control) | 0 |
ConvertAIXMLToVI itself |
0 |
ConvertVIToAIXML — exporting it |
0 |
RunVIAsTopLevel — actually running it |
0 |
lvai_run_vi_and_read_values |
0 |
OpenFile |
1357 |
This corrects the row in §11, which claimed RunVIAsTopLevel leaves a VI loaded too. It does
not — running a VI as top level costs you nothing, and the fresh-name-per-iteration rule is
therefore more conservative than it needs to be. What holds a VI in memory is an open window,
not the fact that it executed; that also explains why the escape is a person closing the window.
Caveat worth stating: measured on a two-element VI with no subVIs, so a VI that pulls a hierarchy
in may behave differently — but the cheap operations are now known to be safe, and
lvai_run_vi_and_read_values reaches its target through a reference it closes again, which is
why it too leaves the path free.
Set, run and read must be ONE call. Measured, and it is the trap that makes the obvious
composition wrong: a RunVIAsTopLevel followed by a separate read of the same VI returns that
VI's defaults — Y empty, dt = 1.0, loaded? = FALSE — not the values of the run that
just happened. The two calls do not share the VI's data space, and nothing about the answer
looks stale. Hence one helper holding one VI reference across all three steps
(scripts\lvai_run_and_read.xml), not two tidy calls.
Consequences for authoring:
- Take paths and numbers in as
stringand convert on the diagram —String To Path(inputs="string:…"→outputs="path:…"). This is why the icon/connector-pane helper VI inscripts\lvdoc_print.xmlhas string controls and three conversion nodes. - Return strings. Only
stringindicators survive the round trip.bool,int32and clusters come back blank and raiseerrorCode 91— but only for themselves: mixing is fine, the strings still arrive. So convert on the diagram and read text, e.g.Selectbetween two string constants to turn a bool into a readable answer, and unbundle an error cluster'ssourcerather than wiring the cluster out whole. - An empty string means "no error" at least as often as it means "not marshalled" — this document
got that wrong. An earlier revision claimed a
status/code/sourcetrio came back entirely empty because the non-string types poisoned the response. It did not:sourcewas blank because the helper had genuinely succeeded, and onlystatusandcodefailed to marshal. The reading cost real work, because the icon helper's result was declared unverifiable and checked against the filesystem instead. Give a helper one string output that is never empty on success — a state word, a path, anything — and the ambiguity disappears. errorCode 91is not proof of failure. Distinguish the two: an emptysourcestring means the VI ran clean, and a non-empty one carries the real error. When the output type cannot be read at all, verify out of band — write the result to a file and inspect that, rather than trusting an empty answer either way.
uid may be a symbol rather than a number — uid="read", outputs="value:read.data" — and the
MCP server assigns the numbers immediately before the file goes to LabVIEW, which never sees a
symbol. lvai_validate_aixml, lvai_convert_aixml_to_vi and lvai_apply_aixml_to_vi all accept
it.
The reason is measured, not aesthetic: profiling one whole generation put a single turn of 47 s on "planning out the unique uids", pure notation overhead with no bearing on what the diagram does. The format needs numbers; the author does not need to keep a numbering scheme in their head while deciding what to wire to what.
A symbol starts with a letter or underscore and contains letters, digits, underscore or hyphen.
Dots and colons are reserved — they separate a uid from a terminal name. root keeps its meaning
and is never treated as a symbol. Numbers and symbols may be mixed in one file.
What the response tells you when symbols were used: symbolicUids maps each symbol to the number
it was given, and aiXmlSentToLabview is the numbered file LabVIEW actually read. Messages naming
a uid are translated back; a bare number is deliberately not, because turning -200220 or
Error 1357 into a symbol name would be a worse failure than leaving one number untranslated.
Three properties this was built to keep, because a mistake here yields a VI that validates, runs and is wired wrongly rather than an error:
- A file with no symbol is not rewritten at all — the original path goes to LabVIEW. The same applies to anything unreadable: a missing file, a locked one, malformed XML. That inertness is not a nicety; adding the feature without it broke 13 existing tests at once, which pass paths that were never created and relied on LabVIEW to say so.
- The mapping is injective, one number per distinct symbol.
- Numbering starts above the highest number already in the file, so a symbol can never take a number the author used.
scripts/aixml-skeletons/ holds complete, validated AIXML whose purpose is to be read and copied
from. It answers a different question from lvai_example_index: that one says has NI already built
this diagram, this one says how is this shape spelled in AIXML. Skeletons use symbolic uids, so
elements can be lifted out of one and dropped into your own file without renumbering.
accumulate-across-a-loop.xml carries the shape that has been got wrong most often here: a For Loop
given its N by maxin, an accumulator on a shift register seeded from an empty-array constant,
and the seed / append / keep case pair without which the inner indexing loop runs zero times on
iteration 0 and the accumulator stays empty for the whole run.
A skeleton is a frozen measurement, and this repository has already shipped one that stopped being true. "A generated VI always gets pattern 4815, so this map is a constant" was correct when measured and produced a wrongly-populated connector pane on a station configured differently. So:
- skeletons carry no
conIdx— that is a station setting, andlvai_connector_panewith no argument is the only honest source; - a polymorphic
instance=in a skeleton is what was measured on the station named in its header, and still wants confirming withlvai_vi_terminals; - each file states the date, the LabVIEW version and the station it was validated on, so a reader can see how old the claim is.
Copying a skeleton must not replace the lookups that produced it. It replaces the typing.
| Symptom | Cause |
|---|---|
Error 7 ... File not found on the output path |
The target directory does not exist. LabVIEW's file write does not create directories — create them first. |
Error 53 ... Unsupported SubVI: X |
Call target not resolvable; see section 9. |
Error 1357 ... A LabVIEW file **from that path** already exists in memory on Save\3AInstrument |
The VI at that exact path is loaded, so the second iteration of author-generate-run cannot overwrite it. OpenFile is the only operation measured to cause it — see the table below, which narrows this considerably. |
Error 1051 ... A LabVIEW file **of that name** already exists in memory |
A different file with the same filename is loaded. Rename the target. The two errors are distinct and the wording is the tell: 1357 says "from that path", 1051 says "of that name". The commonest source is your own last validation. A ValidateAIXML that fails appears to leave a VI named after the document's _name behind, and the next ConvertAIXMLToVI for that name is then refused. Observed 5 for 5 across one session: every 1051 followed a failed validation of the same _name, and every file that validated cleanly on the first attempt generated without complaint. The fix is free — bump _name (and the output file name) after any validation error, which you want anyway per the fresh-name rule. An earlier note here blamed a sibling probe VI that carried the same _name; that explanation fitted one case and this one fits all of them. But it is the output FILE NAME that has to change, not _name — measured 2026-08-29: after two failed validations of _name="MainVI.vi", generating with _name="MainVI_g1.vi" to the same path gave the identical 1051, and generating with the same _name to MainVI_g1.vi went through. This row and the tool's own hint both used to say "generate under a fresh name" without distinguishing the two, which sends you to change the harmless one. Recovery without restarting LabVIEW: generate to a scratch file name in the same folder so relative subVI links still resolve, then copy the file over the target — LabVIEW derives a VI's name from its file name, and no name is stored in the file (strings finds none). Verified by re-exporting the copy: _name came back as the target's name, diagram intact. |
<Structure>: Is a member of a cycle plus Wire: Is a member of a cycle |
A redundant border crossing, most often an Out tunnel added for a shift register's Right terminal — see §7. The net already crosses the border implicitly, so the extra tunnel routes the loop's output back to its own input. Delete the tunnel and let consumers outside read the Right output net directly. |
Error 1051 on the first generation of a path that does not exist yet |
A different file carrying that VI's internal name is loaded — and the usual cause is self-inflicted: a scratch iteration generated from the deliverable's own XML keeps _name="Final.vi" while being saved as Probe.vi, so "Final.vi" is in memory under the wrong path. Change _name in every scratch variant, not just the file name. Measured: viExisted: false, viExistsNow: false — nothing was written, and a LabVIEW restart cleared it. |
Object terminal not found for input: width\3A on Number To Decimal String |
A guessed terminal name. Every wrong guess is reported exactly like this, naming the node and the terminal, so the cheap move is to drop the terminal and re-validate rather than guess again. Number To Decimal String has no width input. |
Control with type=UDClassInst is not supported / Property Node with type=UDClassInst is not supported |
LabVIEW classes cannot be expressed at all. A class instance is rejected both as a front-panel control and inside a property node, so any VI whose connector pane carries an object — all LabVIEW OOP, and DQMH 5's Module Admin — is outside the type grammar. This is a deeper wall than Unsupported SubVI: inlining the subVIs would not help, because the VI's own terminal cannot be typed. Usually accompanied by Could not find control with name "X" to apply fixup. |
Object terminal not found for input: ... |
Misspelled terminal name, or fallout from an unresolved Call. |
An export of 100–200 bytes containing only <VI _name=… description=…/> |
Silent failure, not an empty VI. The diagram was not readable — inaccessible, password-protected or otherwise withheld — and ConvertVIToAIXML still returns errorCode 0 / "No Error". Cross-check with the rendered diagram: if GetDescribeVIPromptInfo also carries no viImage, the diagram is unavailable. Never conclude "this VI is empty" from a childless <VI> element. |
| Everything reports success and the VI is hollow | The generator has two ways of refusing. A Call it cannot resolve is a hard error (Unsupported SubVI). An unsupported node family is silent: the container is created, its configuration is discarded, errorCode stays 0. Measured on Event Structure — frames dropped, one [0] Timeout frame left (§7). Never take errorCode 0 as proof that what you asked for was built: re-export the result and compare, or render it with --diagram. |
| You asked for a graph and got a cluster | Third member of the silent family, and the same rule applies one level down: an unknown style token on a control or indicator is discarded without a word. ValidateAIXML returns errorCode 0 for WaveformGraph, Waveform Graph and Graph alike; only style="graph21703" produces a Waveform Graph (§8). Attribute values are not validated at all, so the check is the same one as for a hollow VI — re-export and compare. |
| The VI runs, reports no error, and computes the WRONG ANSWER | value="TRUE" on a boolean is silently read as false. The worst member of the family, because the other two leave something visibly missing and this one leaves a working VI that is simply wrong. Measured, four spellings in one probe VI, all validating with errorCode 0 and all generating cleanly: value="true" → TRUE — the only one that worksvalue="TRUE" → false value="True" → false value="1" → falseIt cost a generated CSV loader a whole debugging round: its transpose? constant read TRUE, so the file was read untransposed, and the VI returned 1 sample with dt = 1.0 instead of 8 with dt = 0.1 — no error anywhere. It was caught only by comparing the numbers against the source file. Emit exactly lowercase true/false, which is what an export writes, and check a boolean constant's effect against real data rather than against errorCode. |
| A VI in memory CAN be evicted — via the active project | Read this row's ending first: there is a working recipe, in vi-server-reference.md under "Unloading a VI so its path can be regenerated". Reach the IDE's application through {LV.Application} → Project\3AActive Project → {LV.Project} → Application, open the VI reference there, and write Front Panel Window\3AState = Closed. Measured A/B: 1357 before, errorCode 0 after. The rest of this row is the long road that found it, kept because every step of it is a thing that does not work. The fallback rule remains sound when no project is active: generate each iteration under a fresh name, and do not lvai_open_file a VI you still intend to regenerate. Measured, in one helper run that itself reported no error: writing Front Panel Window\3AOpen and Block Diagram Window\3AOpen to False, then FP.Set Close If Lonely, then Close Reference — and the regeneration still failed with 1357. The catalogue carries no unload or remove-from-memory method at all across its 3 078 entries. Earlier advice here said "or make LabVIEW release the VI"; that is not achievable through this interface. Closing the VI in the IDE by hand, or restarting LabVIEW, is the reset. Re-measured on a freshly restarted machine, with the one remaining explanation tested and killed: the idea that closing the window modifies the VI and that a modified VI cannot be unloaded. Reading Modifications\3AUser Changes before the close, after it, and after a Save\3AInstrument gave clean, clean, clean — unsaved changes were never what held it. Same run, no error anywhere in it, regeneration still 1357. What every one of these attempts shared, and what took an evening to see: they all ran in the addon's application instance, where the VI's windows do not exist. That is why closing them changed nothing — see the recipe named at the top of this row. The escape hatch is real, and measured: a person closing the VI in the IDE by hand frees the path immediately — the very next ConvertAIXMLToVI on it returned errorCode 0. So when you are stuck on a path, the fix is a human closing that window, not another property write. Opening the VI inside a project changes nothing — tested, because "we never opened it in a project, which would be the normal case" is the obvious objection. A hand-written .lvproj (§2 of the lvproj reference), the VI generated beside it and opened with both the VI and project pairs, describe_project confirming it loaded as a real member with missingFiles: []: regeneration still 1357. Project membership is not what holds the file. |
Error 42 ... Generic error from ApplyAIXMLToVI |
Not a payload problem — see §14. The RPC itself works; it is gated on a per-VI attachment a third-party client cannot obtain. |
Error 42 ... Generic error from ValidateAIXML |
Suspect an XML comment first — but know that comments are not always fatal, which is what makes this row long. Measured 2026-08-27 on a generated test VI: the file failed with Error 42, and the SAME file with its three <!-- case n --> lines stripped and nothing else changed validated errorCode 0 and generated. A single <!-- plain --> put back between two child elements of <VI> failed again, so the comment's text is irrelevant. That reproduces the original 2026-08-13 finding. And yet scripts/aixml-skeletons/accumulate-across-a-loop.xml still validates errorCode 0 today carrying two comments, one of them in exactly that position — re-checked 2026-08-27, and against the renumbered file LabVIEW actually receives, which does keep both comments. So the discriminator is something else, and three candidates were tested and killed on the failing file: adding an <?xml … ?> declaration (still fails), adding a UTF-8 BOM (still fails), and the comment's content (a bare word fails too). The practical rule: do not emit comments into AIXML you generate. They buy readability in a file nobody reads twice and they cost a failure whose message names nothing. When you do meet this error, bisect — this message carries no line, no column and no element, while the well-formedness errors the RPC does report are precise to the column (Line 5, Column 308, missing required attribute 'outputs'), so a bare generic error reads as a deep structural fault and invites rewriting a diagram that is fine. |
Every lvai_* call stops answering after you ran a generated VI |
The VI you generated is showing a MODAL DIALOG, and LabVIEW answers nothing until a human dismisses it. The known cause is the missing-subVI prompt, but there is a second, entirely independent one that fires on ordinary input: a palette VI whose path input is named … (dialog if empty) opens a file dialog when handed an empty path. Read Delimited Spreadsheet.vi has exactly that — file path (dialog if empty) — so a generated VI that passes an unvalidated file name through it wedges the session on the emptiest possible input. The terminal name is the only warning. Guard it on the diagram: compare the string against "" and Select a placeholder path, which turns the hang into an ordinary file error. Measured: with the guard, an empty name returns in under 40 ms and no dialog appears. Which error code you get depends on the placeholder you chose — an absolute one that does not exist gives 7 (file not found), a bare relative name gives 1430 (path is empty or relative). Both are fine; just do not copy a code out of this table into a VI description without measuring your own. Those two codes were measured on a READ, and they do not transfer to a CREATE — see the row below. |
| A relative placeholder path silently writes into the LabVIEW installation directory | The guard above, applied to a node that creates. TDMS Open with operation = create or replace does not reject a bare relative name: it resolves it against LabVIEW's working directory and creates the file there — measured in a generation run on 2026-08-14, which left DaqReadAndTDMS_no_path_given.tdms and its _index inside C:\Program Files (x86)\National Instruments\LabVIEW 2026\ while the VI's own description claimed a file error had been returned. The 1430 in the row above came from Read Delimited Spreadsheet.vi, a read, where a relative path is invalid; for a create it is perfectly valid and gets created. A file-path error code measured on one direction says nothing about the other. Two consequences. If you use a placeholder at all, make it absolute. And check first whether the node needs a guard: the terminal name is the tell — TDMS Open takes file path, not file path (use dialog) or (dialog if empty), so it cannot open a dialog, and an empty path returns error 118 in 48 ms on its own. The guard was protecting against something that node does not do. Confirmed independently the same day with a purpose-built two-node probe (TDMS Open → TDMS Close, create-or-replace, path taken as a string and converted on the diagram, error cluster unbundled): the bare relative name lvai_probe_relative.tdms returned status false, code 0 — success, not an error — and the file plus its _index were then found at C:\Program Files (x86)\National Instruments\LabVIEW 2026\; the same probe with an empty path returned code 118 in 25 ms, with file path out reading <Not A Path> and no dialog. Two runs of one VI settle both halves. |
| A generated VI wedges LabVIEW on the SECOND run, having worked the first time | A valid path is enough. Export Waveforms to Spreadsheet File.vi and its Write Delimited Spreadsheet.vi siblings open a modal Replace existing "<path>"? dialog whenever the target file already exists and append to file? (new file\3AF) is FALSE — no empty path, no missing subVI, nothing the terminal names warn about. Measured 2026-08-24 on DaqReadAndTDMS.vi: the VI both logged TDMS to a path and handed that same path to the exporter, so the file existed within the same run and the very first execution hung; every lvai_* call answered DeadlineExceeded while LabVIEW.exe still answered the OS and the main frame reported IsWindowEnabled = false. The dialog carries an empty window title, so a window enumeration filtering on title finds nothing and the wedge looks like a deadlock — enumerate on IsWindowEnabled instead: an enabled LVDChild under a disabled LVFrame is a modal dialog, and WM_CLOSE to it is Cancel. The fix is on the diagram, not in the call: open the target with Open/Create/Replace File (operation = 4, replace or create — or 3, open or create, when appending) and Close File it before the exporter runs, then pass append to file? = TRUE. The file is then guaranteed to exist in the state you want and no confirmation is ever asked. Verified in WriteWaveformsToCSV.vi: repeated runs over an existing CSV return status false, code 0, no dialog. |
A VI was authored from nothing but the rules above — while loop, uninitialised shift
register, a three-terminal fan-out net, Increment, Greater?, a stop condition, two
border tunnels, a connector-pane control and indicator, and a comment. Result:
ValidateAIXML→errorCode 0ConvertAIXMLToVI→ a real 7.3 KB.viConvertVIToAIXMLon that new VI → output identical to the authored input, element for element, attribute for attribute
So the format description round-trips. If an authoring attempt fails, suspect a terminal
name (section 8) or a Call target (section 9) before suspecting the structure rules.
- Whether
MonitorCodeCompletionaccepts AIXML inCodeSuggestion.changes. Probably moot: the monitors deliver to a single subscriber and NI's own service always wins that race (§14), so a third-party client never receives the event to answer in the first place. - Whether
Tunnel.condhas meanings beyond the observedtrue. - The full set of
Structure._namevalues for reading. Five kinds are confirmed (While Loop, For Loop, Case Structure, Event Structure, Flat Sequence Frame) and both disable structures are confirmed asNode+Diagram. Still unseen: Stacked Sequence, In Place Element Structure. (Timed Loop is on NI's unsupported list for generation; whether it exports is untested.) - What exactly the "supported node catalog" contains. NI names the concept (§9) but does not
enumerate it, so the resolution probe remains the way to test a specific target. One data
point: the DAQmx API resolves and generates fine — including its polymorphic
Create Virtual ChannelandRead— although it ships as an LVAddon rather than in corevi.lib. So the catalog is not limited to what LabVIEW installs by itself.
This RPC patches an existing VI surgically, and that is worth knowing before writing it off. Measured on a VI patched through NI's own assistant:
1a2
> <FreeLabel comment="Hello World" uid="60" uid_parent="root"/>One added line. All 56 other elements byte-identical, every uid unchanged, order preserved.
So "apply" is literal — not a regenerate-and-overwrite behind a friendly name.
From a third-party client it always returns Error 42 (generic). Sixteen variables were
ruled out as the cause. Every run carried two controls in the same call —
ValidateAIXML and ConvertVIToAIXML against the same payload and the same target — and both
returned errorCode 0 every single time. So the server, the payload and the target VI were
demonstrably fine in each attempt.
| Ruled out | How |
|---|---|
| XML shape | full state, delta, <Changes> root, minimal single FreeLabel |
| Target VI | the same VI that NI's assistant patches seconds earlier still fails |
| Editor state | VI open, VI active, project open, project closed |
uid value or range |
5000, 5001, 90000, and low unused values 61–71 |
| Client stack | C# (Grpc.Net.Client) and Python (grpcio) with hand-generated stubs — so it is not an artefact of one implementation |
| LabVIEW process | before a crash, after it, and on a freshly started instance |
| Assistant service | running, killed, and disabled via the registry |
| Assistant login | logged in and logged out |
| VI activated for the assistant | labview:set_active_file confirmed successful in the log, Apply fired 31 s later — still 42. See below; this is the decisive one. |
| Paths | a missing XML file or VI gives a clean Error 7 instead |
| Parsing | malformed XML gives Error -2628 ("error occurred while parsing"), so well-formed input is parsed and 42 comes later |
It fails cleanly. Exporting the target afterwards shows none of the attempted uids and a
.vi of unchanged size — there is no partial write. Attempting it costs nothing but the round
trip, which is worth knowing before experimenting on real code.
Re-measured 2026-08-22, on the suspicion that the upgrade had opened this up. It has not. Six
configurations, every one Error 42 occurred at LV AI Core.lvlibp:Apply code changes.vi:
| Configuration | Result |
|---|---|
Patched payload (one added FreeLabel, unused uid), VI loose |
42 |
| Unmodified export as payload — no delta at all | 42 |
VI opened in the IDE via lvai_open_file |
42 |
VI a confirmed member of a project (describe_project, missingFiles: []) |
42 |
| VI generated into its project, never opened loose, first open through the project | 42 |
| Same, with that project confirmed active | 42 |
Two of those rows are new evidence rather than repetition. The unmodified export fails
identically, so the failure happens before any diffing logic — it is a gate, not a payload or
delta problem, which retires "XML shape" as a suspect for good. And the active-project row
closes the objection that the earlier sweep never opened the VI the way a person would: the project
was proven active because lvai_close_active_project later closed that project by name.
Verification that nothing happened: the target's export before and after is byte-identical
(1 193 bytes), with zero occurrences of the inserted uid. Note that the .vi file's md5 did
move while its length did not — that was LabVIEW saving the VI when the project closed, not the
apply. A changed hash at unchanged length is not evidence of a partial write; re-export and compare
the AIXML instead.
§14's conclusion therefore stands unchanged: the RPC works, it is gated on a per-VI attachment
established by the IDE's "Discuss with Nigel…" command, and that attachment is not reachable from a
third-party client. The one variable still untested from here is a human performing that command on
the exact VI and an Apply firing immediately after — worth trying if the opportunity arises, but
the table above already rules out everything a client controls on its own.
What it is instead: a per-VI attachment. The assistant's own trace shows the sequence
GetUserAttachedVIPathsAsync() -> which VIs is the user working on?
ConvertLabVIEWVIToAIXMLAsync() -> read it
ApplyLabVIEWAIXMLToVIAsync() -> patch it
and its chat agent is equipped with a tool named labview-set_active_file. That tool is
not among the 23 RPCs of lvai.LVAI; the corresponding operations (SetActiveVI,
ObserveForActiveVIChange) live in a second service, lv_ai_assistant_service, which is
not reachable over gRPC.
The attachment is established by the IDE's "Discuss with Nigel…" command, which fires
MonitorDiscussVI. And that event cannot be intercepted:
- with NI's service running, it consumes the event; a second subscriber gets nothing
- with the service stopped, the click starts it (measured: service up at 10:55:43,
[Discuss VI] Started monitoringat 10:55:44), and the event still goes to it — even though a third-party watch had subscribed first, while the hook was free
So the monitors are single-subscriber streams, and NI's service always wins. That single fact explains every unanswered monitor wait in this project. It also rules out intercepting the attachment — though as the settled experiment below shows, holding the attachment would not have helped anyway.
The last hope was to subscribe before NI's service exists: stop the service, close LabVIEW, attach a watcher that polls for the port, then start LabVIEW. The watcher attached 6 seconds after the port opened and still lost:
11:11:40 NI's service: [Discuss VI] Started monitoring for requests
11:11:46 third-party watcher: attached on port 59533
-> 0 events received; stream ended DEADLINE_EXCEEDED
LabVIEW brings its own consumer up with the service, and the service is launched on demand — by LabVIEW starting, or by the very click one is trying to observe. There is no window.
Disabling the assistant in the registry disables this interface too. With the assistant
switched off, LabVIEW started but never opened an lvai port at all: 220 s of polling, no
reflection endpoint anywhere on the machine, VI Server on 3363 the only thing listening.
lvai.LVAI is part of the AI feature, not a separate service — so "LabVIEW with the AI
feature active" in the README is a hard requirement, not a preference. Expect a slower first
start afterwards too: 115 s against the usual 30–70 s.
The assistant's own tool calls can look like failures in its log. Lines such as
Usage count for Code Generation feature left at 1 because operation failed or was canceled.
appear next to calls that demonstrably succeeded — the patch above was applied by the very call carrying that line. The message describes the usage counter, not the outcome. Verify by exporting the VI, never by reading that log line.
The assistant's chat accepts a direct command that activates a VI without any right-click. Run
it, then immediately call ApplyAIXMLToVI from your own client: if the attachment were
process-wide it would now succeed. Measured:
14:15:05 [PROMPT] labview:set_active_file C:\Temp\...\Start Module - Stub.vi
14:15:09 [INVOKING TOOL] labview-set_active_file(filePath)
14:15:10 [TOOL INVOCATION COMPLETE] <- succeeded, no FAILED line
14:15:41 third-party ApplyAIXMLToVI -> Error 42
Activation succeeded and changed nothing. The gate is on the caller, not on the VI. A
third-party client cannot borrow the attachment by arranging for the VI to be attached — NI's
assistant is permitted because it brings its own session over lv_ai_assistant_service.
That closes the question. ConvertAIXMLToVI (regenerate to a new file) is the only write path
available to a third-party client; §12 covers what it costs.
Two details from running this, in case anyone repeats it:
labview:set_active_fileneeds the full path. Passing just the file name logs[TOOL INVOCATION FAILED]; the parameter is calledfilePathand means it literally.- Check the log before trusting the precondition. The first attempt here looked like a
clean negative result but the activation had silently failed — the
Applymeasurement was meaningless. Confirm[TOOL INVOCATION COMPLETE]without a precedingFAILEDline, then measure.
ConvertVIToAIXML works on VIs inside packed libraries (.lvlibp) as well — a compiled
module can still yield its complete block diagram (~200 KB of AIXML for a large one). Paths
inside a .lvlibp are not real directories, so directory listing fails where the RPC
succeeds: address the VI by its path through the .lvlibp file.
"Can" and not "does": it depends on how that .lvlibp was built, and NI's own are built
without diagrams. Measured on the AI addon's two packed libraries: every VI answers Error 47,
Unknown heap, from the exporter. That message names nothing useful; GetDescribeVIPromptInfo on
the same path gives the real reason —
Error 1012 ... Cannot load block diagram.
Property Name: Block Diagram
The block diagram for ...\LV AI Core.lvlibp\...\XML generator.vi could not be loaded
but is required by this property or method. (Traverse Failed)
So treat Error 47 from the exporter as "diagrams were stripped at build time", and reach for
lvai_describe_vi when you need to know why. describe_vi is also the fallback that still
works: its viXml and viImage come back empty, but controlsIndicators is populated, so a
diagram-less VI still yields its terminals. For the connector pane of many VIs at once there is a
cheaper route that needs no diagram at all — Connector Pane\3AReference → Controls[] →
Label → Text, which is what scripts/lvai_inventory.xml does; see
vi-server-reference.md.
That makes read-only analysis possible on projects that link only compiled components, with no source checkout. Writing back is a different matter — see §9 and §11.
Both bit here, and both produce a false negative that looks like "the driver is not installed":
.llbis a single file, not a directory.find … -iname "DAQmx Read.vi"can never match a VI inside one, no matter which root you search. A driver's whole API can live in a handful of.llbs — e.g.read.llb,write.llb,create/channels.llb.- LVAddons live outside the LabVIEW tree. Drivers ship as add-ons under
C:\Program Files\NI\LVAddons\<name>\<version>\, with their ownvi.lib,examplesandmenus. Searching every…\National Instruments\LabVIEW <year>\directory finds nothing.
So: search for the .llb files themselves, and check LVAddons before concluding anything
is missing. The reliable existence test is not the filesystem at all — it is a Call in a
small AIXML file put through ValidateAIXML (§9).
A related trap in shell probes: suppressing 2>/dev/null turns a path typo into an empty
result that reads exactly like "no matches". Keep stderr visible when a negative result would
change a conclusion.
Measured 2026-08-28. ConvertVIToAIXML on a control file succeeds and returns 47 bytes:
<VI _name="Pflanze_export.ctl" description=""/>The control it was asked about had a six-field cluster with two bound typedefs. None of it appears —
not the cluster, not one field, not a name. NI lists .ctl as unsupported for authoring; this
records that reading one is equally empty, and that it reports success while doing it.
The consequence is worth spelling out, because the round trip it kills is an obvious thing to try:
export a class's private data to a .ctl, convert to AIXML, edit the structure, convert back,
import. Step two returns nothing to edit, so step four would write an empty control, and
importing that would erase the class's private data. errorCode 0 at every step.
Read a .ctl with pylv_extract instead — it needs no LabVIEW and carries the whole heap.