TeamcenterKnowledge

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_percent means ZERO, not centre. Above is x=0 (left edge), y=50%.
  • ConnectionDirection points away from the symbol: inlets are (-1,0), outlets (1,0).
  • <Unit> is per symbol and not uniform: Piping ANSI/ISO are in, Hydraulic (ISO 1219) are mm. 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 is Prototype). Nothing about a sheet restricts it to one domain.
  • Discipline is set per object on the builders: NodeBuilder.SetDisciplines([...]) / GetDisciplines(), same pair on ConnectionBuilder. 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:

  • SetDisciplines is NOT validated. ["Banana"] is accepted and reads back as Banana. 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.

  • SymbolId may 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. The Prefix in the attribute file drives the NX tag (Prefix="LB" gives LB000001).
  • 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.xsdNOT ENFORCEABLE. Shipped Liquid_pump.xml fails 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.xsdREAL. Hydraulic filter_symbol passes, and it catches genuine bugs: SymbolICO/@UNIT is {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 ICO DISCIPLINE only lives in the file).
  • The count is the proof. SetStartNode mints an anonymous dynamic port at the anchor: 5 blocks / 21 declared ports / 8 wires read back 37 ports. Switching to SetStart/SetEnd read 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.stringify writes JS 7.0 as 7, Python reads it as int, 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 SetUserAttribute read back as None through the typed getter. Walk node.GetUserAttributes() and match on .Title instead.

★ 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.port on 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 by port.Tag to 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.