Skills
Nx Schematic Diagramming
Skill
nx-schematic-diagramming. Author NX Diagramming / Schematic Designer content (P&ID, hydraulic, wiring schematics) from NXOpen Python - placing reuse-library symbols, wiring connections, tagging and plotting. Covers the one thing that blocks every attempt: this cannot run in run_journal.exe, and why the failure is silent. Use for any NX schematic, P&ID, or diagramming automation.
VERIFIED end to end on NX 2606 (2026-08-01): 11-symbol P&ID and 13-symbol ISO 1219 hydraulic schematic generated, wired, tagged, saved and plotted to PDF. Do NOT spend turns rediscovering the batch-mode dead end below.
★ It cannot run in run_journal.exe. Use USER_STARTUP instead.
Batch NX will let you build a SchematicManager.CreateNodeBuilder and set every property,
then fail on commit with a message pointing at an unrelated subsystem:
Trying to use the Routing reuse library outside of a Routing application.
The reuse library needs the session to be IN an NX application. The only way in is
Session.ApplicationSwitchImmediate, which NX Open documents as "only supported when
running interactively. It is not supported in batch mode." In batch it returns without
raising and Session.ApplicationName stays APP_NONE — the switch silently no-ops, so
you chase the reuse library instead of the real cause.
The way in is the USER_STARTUP user exit, which accepts a Python journal (per the NX
Open Programmer's Guide execution table, user exits are the one method that is
Interactive-or-Batch and runs Python journals) and fires it inside a real ugraf.exe
session. There, the application switch genuinely takes effect.
# The journal writes RESULT as its last act; that file appearing is the completion signal.
# ugraf.exe is a GUI and stays up, so kill it afterwards.
NX_SCHEMATIC_SPEC="$SPEC" NX_SCHEMATIC_RESULT="$RESULT" USER_STARTUP="$JOURNAL" \
/d/NX2606/NXBIN/ugraf.exe &
while [ ! -f "$RESULT" ]; do sleep 1; done
sleep 2; taskkill //IM ugraf.exe //F
A user-exit journal gets no argv — pass configuration by environment variable. Round trip including NX startup: ~25-30 s.
★ Never leave a USER_STARTUP session open for a person to use
The journal session works perfectly and is a trap the moment a human touches it:
Error: Library is missing required entry point
File name: .../tools/nx/read_schematic.py
Function name: ufsta
USER_STARTUP runs a .py journal at TRUE STARTUP only. NX re-invokes a registered user exit when
you switch applications (seen in Design Center), and that path wants a compiled library
exporting ufsta. A journal has no such symbol, so every build succeeds and then the session
explodes in the user's face the first time they navigate. Observed 2026-08-05.
Nothing is corrupted when it happens - the parts and plots are already written. But if you keep the
GUI open for review, kill the journal session and relaunch NX clean, with USER_STARTUP and
your own NX_SCHEMATIC_* variables removed (env -u ..., so they cannot leak from the caller).
One extra NX startup, paid only on keep-open runs. tools/nx/run-nx-schematic.sh does this.
Do not try to clear it from inside the journal with os.environ.pop: Python's environment writes go
through the CRT and NX reads with the Win32 API, so you cannot rely on it taking effect.
★ No active sheet, no builder
CreateNodeBuilder raises "No Diagramming Sheet is found in the part." until
Session.SheetManager.ActiveSheet is assigned — even though the template does contain a
sheet and DiagrammingManager.Sheets lists it. Nothing sets an active sheet outside the
GUI. It is a plain settable property:
session.Parts.OpenActiveDisplay(part_path, NXOpen.DisplayPartOption.AllowAdditional)
session.ApplicationSwitchImmediate("UG_APP_SCHEMATIC_DESIGNER")
part = session.Parts.Work
session.SheetManager.ActiveSheet = list(part.DiagrammingManager.Sheets)[0] # <- required
sm = part.SchematicManager
The working place-and-wire recipe
import NXOpen, NXOpen.Schematic as S
# --- place a library symbol ---
nb = sm.CreateNodeBuilder(S.Node.Null)
nb.SymbolSourceType = S.SymbolSourceOption.ReuseLibrary
nb.CreationType = S.NodeCreationType.Equipment
nb.NodeType = S.NodeType.Default
nb.SymbolId = r"D:\NX2606\DIAGRAMMING\schematic\library\contents\...\Fuel_filter.xml"
nb.SetLocation(NXOpen.Point2d(x, y)) # takes ONE Point2d, not (x, y)
node = nb.Commit(); nb.Destroy()
# NX assigns the tag (FL000001, PU000001, TA000001 ... by symbol class).
# NodeBuilder.Id and .Label are READ-ONLY; read the tag back through an edit builder.
eb = sm.CreateNodeBuilder(node); tag = eb.Id; eb.Destroy()
node.SetName("FuelFilter") # settable on the Node
node.SetUserAttribute("SOURCE_ID", -1, "041942", NXOpen.Update.Option.Now)
# --- wire two nodes ---
cb = sm.CreateConnectionBuilder(S.Connection.Null)
T = S.ConnectionTrimPolicyType.NodeAndPort
# (node, percentX, percentY, offsetX, offsetY, dirX, dirY, trimPolicy) - NX creates a
# dynamic port at that anchor. Schematic.Node has NO GetAllPorts; SetStart(port)/SetEnd(port)
# need Schematic.Port objects you do not get from a freshly placed node, so use these.
cb.SetStartNode(a, 1.0, 0.5, 0.0, 0.0, 1.0, 0.0, T)
cb.SetEndNode (b, 0.0, 0.5, 0.0, 0.0, -1.0, 0.0, T)
conn = cb.Commit(); cb.Destroy()
SymbolId is the absolute path to the library XML, not a name or a classification id.
Reading the symbol library (where the anchor numbers come from)
SetStartNode takes an anchor, not a port id, so land wires on real connection points by
reading the symbol's own definition at
<NX>\DIAGRAMMING\schematic\library\contents\<Discipline>\...\<name>.xml:
<Node id="Node1" portIDList="P1 P2"/> <!-- fixed ports -->
<Port direction="In" id="P1"><ConnectionDirection x="-1" y="0"/>
<Location ref="Node1" y_percent="0.5"/></Port>
<Node id="Node1" variablePortSides="15"/> <!-- no fixed ports; 1=L 2=R 4=T 8=B -->
- A missing
x_percent/y_percentmeans ZERO, not centre. Above is x=0 (left edge), y=50%. ConnectionDirectionpoints away from the symbol: inlets are(-1,0), outlets(1,0).<Unit>is per symbol and not uniform: Piping ANSI/ISO arein, Hydraulic (ISO 1219) aremm. A sheet template is one or the other — mixing them puts a 1.5 in engine and a 25 mm pump on one sheet at 25x scale difference. Emit one sheet per discipline.- Ports are single-use (
numberAllowedConnections="1"). Two wires on one port silently loses one; keep a per-node ledger of spent port ids. - ISO 1219 hydraulic symbols are vertical (In bottom, Out top); ANSI piping are horizontal.
★ Branch fittings cannot be placed at all
Every tee, cross and generic junction in the shipped library (11 symbols across Piping ANSI,
Piping ISO and HVAC) carries no <Annotation>, and NodeBuilder.Commit fails on them with
Cannot place the symbol because it has not defined the label.
under every combination of NodeCreationType and NodeType. They are not placed — NX
builds a tee by branching an existing connection (ConnectionBuilder.SetStartTeeSymbolId).
Test for <Annotation in the symbol XML before mapping anything to it; a symbol without one
is unplaceable. For a component that is genuinely a selector/junction, map it to a real
multi-port valve (3-way_valve, or the hydraulic proportional_directional_control_valve).
Sheet geometry
Sheet y grows DOWNWARD from a top-left origin — the opposite of modelling coordinates.
A node at y=19.5 on a 22-inch-high sheet lands in the bottom zone, on top of the title
block. Lay out as y = margin + row * pitch, and keep the bottom-right corner clear.
Templates ship at <NX>\DIAGRAMMING\templates\Schematic-{inch|mm}-{size}-template.prt.
Create a schematic part by copying a template file, then opening it.
Title block and labels
blocks = list(part.DiagrammingManager.TitleBlocks)
tbb = part.DiagrammingManager.TitleBlocks.CreatePopulateTitleBlockBuilder(blocks) # LIST, not one
tbb.SetCellValueForLabel("TITLE", "Inverted Flight Fuel System") # labels as printed
tbb.SetCellValueForLabel("SHEET NO.", "TC 041908") # also DRAWN BY, CHECKED BY...
tbb.Commit(); tbb.Destroy()
Symbol tag annotations are editable and accept newlines, so a generated drawing can show the source model's name above the NX tag. There is no Node → its-own-annotation accessor; match on the tag text (unique per sheet):
ab = part.DiagrammingManager.Annotations.CreateAnnotationBuilder(ann)
if ab.Text == tag: ab.Text = "%s\n%s" % (name, tag); ab.Commit()
ab.Destroy()
The ISO instrumentation balloon draws its own two-field text rather than a plain annotation, so those symbols keep the bare tag — report that gap, do not let a count hide it.
Plotting
pb = part.PlotManager.CreatePrintPdfbuilder() # PlotManager, NOT Session
pb.SourceBuilder.SetSheets([diagram_sheet]) # WITHOUT THIS IT COMMITS AND WRITES NOTHING
pb.Filename = out_pdf
pb.Colors = NXOpen.PrintPDFBuilder.Color.BlackOnWhite
pb.Commit(); pb.Destroy()
PrintSVGBuilder exists but has no Filename property; use PDF.
★ One sheet CAN hold every domain — discipline lives on the object, not the sheet
VERIFIED on NX 2606 (2026-08-05). A single sheet carried Piping + Electrical + Pneumatic + Hydraulic nodes and 5 connections, plotted clean. This is the basis of a multi-domain "Platform Schematic".
- There is no discipline/diagram-type property on
Diagramming.Sheet(its only type-ish member isPrototype). Nothing about a sheet restricts it to one domain. - Discipline is set per object on the builders:
NodeBuilder.SetDisciplines([...])/GetDisciplines(), same pair onConnectionBuilder. It survives commit and reads back off the saved part. - Cross-discipline connections commit without complaint (Piping node → Electrical node).
- Symbols from different library folders (Piping ANSI and Hydraulic) coexist on one sheet.
Two traps:
SetDisciplinesis NOT validated.["Banana"]is accepted and reads back asBanana. It is a free-form string tag, not an enumeration. NX will never catch a mis-typed cross-domain port for you — the writer owns correctness, so keep the port coverage gate.- The plural is a lie in practice:
["Piping","Electrical"]reads back["Piping",""]. Budget one discipline per object.
DiagrammingConfiguration (.../library/configure/SchematicLibraryConfiguration.xml) declares
four disciplines — Piping, HVAC, Hydraulic, Electrical — and exactly two diagram types,
Piping and Wiring (Electrical is the only Wiring one). But Electrical ships no symbols
of its own: its StartClassList points back at contents/Piping. Only HVAC, Hydraulic and
Piping have symbol content. Authoring electrical symbols is real work you must plan for.
Domain line types are the readability mechanism
.../library/contents/Piping/LineTypes/ ships a full multi-domain signal vocabulary, and
ConnectionBuilder.SetLineType(<abs path to xml>) renders them visibly distinct on one sheet:
Primary / Secondary / DoubleLine / TripleLine process + fluid
ElectricSignal1|2, ElectricBinarySignal1|2 electrical
HydraulicSignal, PneumaticSignal, PneumaticBinarySignal, CapillarySignal
InternalSystemLink_SoftwareOrDataLink data / software
MechanicalLink, ElectromagneticOrSonicSignal_{Guided,NotGuided}, UndefinedSignal
GetLineType() is an unreliable getter — it returned the path for 2 of 5 connections and
"" for the other 3, yet the plot showed all of them styled correctly (solid / dashed /
beaded / ticked). Confirm line type from the plotted PDF, never from the getter.
Ports also carry a domain in the library itself: the symbol's
*_attribute_definition.xml <ICOCatalog> has <Port ID="P1" DISCIPLINE="Hydraulic" DIRECTION="Both" .../>. That is the field to reconcile a TC port ledger against.
Unit mismatch still bites here: a mm Hydraulic symbol on an inch sheet renders ~25x too small next to ANSI piping. For a genuine mixed-domain sheet, restrict to one unit system's symbol set (or author your own generic blocks at a consistent unit).
★ Generate your own symbols, and the port API stops fighting you
VERIFIED on NX 2606 (2026-08-05). The shipped library only has device symbols in three
disciplines, with whatever ports that device has. For a platform view you generate one symbol
per logical element from the model's own port list. Generator:
Capital_TC_Integration/src/forward/nx-logical-block-symbol.mjs.
SymbolIdmay point ANYWHERE on disk. A generated symbol under your repo places fine; it does not have to live under<NX>\DIAGRAMMING\...\contents. No library re-config needed.- Emit the shipped two-file shape:
<Name>.xml+<Name>_attribute_definition.xml. ThePrefixin the attribute file drives the NX tag (Prefix="LB"givesLB000001). - SVG
<text>renders. The shipped symbols never use it, so this is worth knowing: you can draw the block name inside the body instead of relying only on the tag annotation. - An
<Annotation>is still mandatory, same as for shipped symbols.
Which XSD is a real contract (checked against Siemens' own files)
nxdiagram_symbol_definition_schema.xsd— NOT ENFORCEABLE. ShippedLiquid_pump.xmlfails it with the same errors your generated file will (Anchor/Size/Location"not expected"). Gate on the shipped file shape, never on this XSD.nxdiagram_attribute_definition_schema.xsd— REAL. Hydraulicfilter_symbolpasses, and it catches genuine bugs:SymbolICO/@UNITis{metric, inch}, not"english".
★★ Declared ports make SetStart/SetEnd work — correcting the note above
The anchor-based SetStartNode advice earlier in this skill applies to shipped symbols. Once
your own symbol declares fixed ports, the Schematic.Port objects exist in the model and the
port API is strictly better:
pb = sm.CreatePortBuilder(port); pid = pb.Id; owner = pb.GetNode(); pb.Destroy()
# index (node, port id) -> Port, then:
cb.SetStart(portA); cb.SetEnd(portB) # binds to the REAL port
PortBuilder.GetNode()resolves the owning node, so port ids only need to be unique within a block, not across the sheet.PortBuilder.SetDisciplines([...])puts the domain on the port instance in the model, which is what a coverage gate reads back (the library ICODISCIPLINEonly lives in the file).- The count is the proof.
SetStartNodemints an anonymous dynamic port at the anchor: 5 blocks / 21 declared ports / 8 wires read back 37 ports. Switching toSetStart/SetEndread back 21. Same picture on the plot, completely different data underneath — so never judge port binding from the PDF. - NX labels only unconnected ports on the sheet. A port name vanishing after you wire it is normal, not a lost port.
Two traps that cost a run each
JSON.stringifywrites JS7.0as7, Python reads it asint, and NXOpen rejects it:"First parameter is invalid. Expecting double type, found int."float()every number crossing a JSON boundary into NXOpen.- Node stamps written with
SetUserAttributeread back asNonethrough the typed getter. Walknode.GetUserAttributes()and match on.Titleinstead.
★ Auto-layout is a silent no-op (NX 2606)
SchematicManager.LayOutConnectionDiagram looks like the answer to bad routing. It is not, yet.
Getting it to accept the call needs a distinction worth knowing on its own — there are two sheet collections over the same drawing:
part.DiagrammingManager.Sheets # -> Diagramming.Sheet : the drawing sheet, what ActiveSheet wants
part.SchematicManager.Sheets # -> Schematic.Sheet : the schematic layer, what THIS call wants
sm.LayOutConnectionDiagram(list(sm.Sheets)[0]) # Diagramming.Sheet raises TypeError
With the right type it returns cleanly and moves nothing. Verified by reading
NodeBuilder.GetLocation() out of both saved parts: 12 nodes, 0 moved, positions identical.
Do not check this with a PDF md5 — PDFs embed timestamps, so two byte-different files can be the
same drawing. Compare node locations.
Probable cause is the same gate nx-platform-spec.mjs already hit for off-sheet connectors: these
features want Schematic Designer runs, which the shipped templates do not set up. Until that is
cracked, layout is yours to compute.
★ A generated-symbol sheet reads back with FULL port identity
Received wisdom (and this repo's own port-coverage gate) says the built .prt loses port identity, so connectivity can only be audited on the spec. That is a property of how the sheet was wired, not of NX. Verified 2026-08-05 on NX 2606 by reopening a SAVED part:
| wired with | reads back as |
|---|---|
SetStartNode (anchor, shipped symbols) |
{start:"CheckValve"} — no port on either end |
SetStart/SetEnd (declared ports, generated symbols) |
block.port on both ends, exactly |
cb = sm.CreateConnectionBuilder(conn)
cb.GetStart() # -> NXOpen.Schematic.Port (not a Node)
cb.GetStartNode() # -> None on a port-bound connection. Do NOT test ends with this.
- 11 of 11 wires resolved to
block.porton both ends and matched the Teamcenter wiring exactly. - 22 of 22 ports still carried their domain via
PortBuilder.GetDisciplines()after save+reopen. - Resolve a port to its owner with
PortBuilder.GetNode(); index byport.Tagto turn an end object back into (block, port id).
Consequence: a coverage gate can score the ARTIFACT rather than the intent.
Shipped symbols get this too — declare-bind them instead of anchoring. Index the declared ports
BEFORE creating any connection (at that point every sm.Ports entry is a declared one), then:
tag_to_key = {node.Tag: key for key, node in placed.items()}
declared = {}
for p in sm.Ports:
pb = sm.CreatePortBuilder(p); declared[(tag_to_key.get(pb.GetNode().Tag), pb.Id)] = p; pb.Destroy()
cb.SetStart(declared[(fromKey, "P2")]) # falls back to SetStartNode when absent/spent
★ Bind each END independently, never the wire. All-or-nothing degrades a good end: a wire whose start has no declared port (variable-side symbol, or one already spent) will throw away a perfectly good declared port at the other end. On the 041908 P&ID that alone accounted for the last two mis-scored cells. Per-end binding took it from 7/11 wires bound to 18/22 ENDS bound, ports in the file from 49 to 31, and made the artifact-scored and spec-scored gates agree 36/36.
Nothing on the drawing moves: the anchors were derived from these ports all along. Only the file's record of which nozzle a line is on changes.
Honesty rules
- Read node and connection counts back out of the saved part
(
len(list(sm.Nodes)),len(list(sm.Connections))) — never report the write loop's own count. - Report the NX-assigned tag only after reading it from an edit builder on the committed node.
- If a symbol fell through to a generic default, say which. A P&ID with an unlabelled generic box on it looks authoritative and is not.
Working reference implementation
Capital_TC_Integration/ — src/forward/nx-symbol-library.mjs (library reader),
nx-diagram-mapping.mjs (component → symbol rules), nx-schematic-spec.mjs (layout),
tools/nx/build_schematic.py (the journal), tools/nx/run-nx-schematic.sh (the runner),
docs/NX_DIAGRAMMING_BRANCH.md (the readout).
Generated from skills/nx-schematic-diagramming/SKILL.md in the tc-automation-skills library, which is the canonical copy and also serves as the agent skill set for Teamcenter work.