THE NX BRANCH
NX Diagramming: an Authoring Seat, Not a Report
The fluid leg is not "generate a drawing and admire it". Cameo holds the skeleton, Teamcenter holds the definition, and the fuel or hydraulic SME details it out inside NX Schematic Designer — then the detail comes back into Teamcenter as real objects. The loop is closed for both components and wires, with receipts, an undo, and a guard that stops a pipeline the moment anything is lost.
The loop
Four decisions define the contract this was built against. If one of them changes, the build changes.
| # | Decision | Consequence |
|---|---|---|
| 1 | Enrichment goes to Teamcenter, never back to Cameo. | Teamcenter is permanently a superset of the SysML model. Cameo is not, and must never be treated as, a complete picture of the fuel or hydraulic system. |
| 2 | SME components become Fnd0LogicalBlock under the existing revision. | Same type the skeleton already uses, so the Capital and MADe branches read the enriched architecture with no changes. Skeleton and enrichment are distinguished by provenance, not by type. |
| 3 | The schematic becomes a managed NX part item, not a dataset. | A first-class Teamcenter object with its own revision and ownership. blocked — managed NX mode is regressed on this machine; the dataset route is shipping instead. |
| 4 | A later Cameo skeleton push must merge, never delete. | The highest-risk item in the whole loop, and not on the NX side at all. See the merge guard below. |
★ Why this is the one thing that cannot run headless
Everything else in this repository runs in run_journal.exe. This does not, and the failure mode is worth writing down because it is silent and it points at the wrong subsystem.
SchematicManager.CreateNodeBuilder works fine in batch. Committing a node whose symbol comes from the reuse library does not:
Trying to use the Routing reuse library outside of a Routing application.
The only way into an NX application 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 — so the switch looks like it worked, and the reuse library rejects you several calls later, pointing at a completely different part of the system.
tools/nx/run-nx-schematic.sh sets USER_STARTUP, launches a real ugraf.exe, waits for the journal's result JSON, and terminates NX. Roughly 25–30 seconds per build including startup. A user-exit journal gets no argv, so paths arrive as NX_SCHEMATIC_SPEC and NX_SCHEMATIC_RESULT. This generalises: any NX capability needing an application context can never be driven by a batch-only worker.What it produces
| Sheet | Source | Discipline | Result |
|---|---|---|---|
InvertedFlightFuelSystem_PID.prt | TC item 041908, live read | Piping ANSI/ISO, inch, D size | 11 symbols, 11 connections, 0 errors |
InvertedFlightHydraulic_Schematic.prt | TC item 042326, live read | Hydraulic ISO 1219, mm, A1 | 13 symbols, 16 connections, 0 errors |
InvertedFlightPlatform.prt | the two specs above | Piping, inch, D size | 2 blocks, tie check 2/2 |
Counts are read back out of the saved part, not counted in the write loop. Each part is saved with a PDF beside it.


TC 042326 — after promotion, the hydraulic sheet carries a real Teamcenter item, not a file path.Platform view and drill-down — settled by probing, not by reading API names
The ask was: "I need the ability to go both but have both tie. I can see us wanting to see the full platform and be able to dive into the detail."
The answer is yes, and the mechanism is not the one the API names suggest. Four candidate APIs were probed against a live NX rather than assumed, because existing is not working — the same class of trap as ApplicationSwitchImmediate.
| Candidate | Verdict |
|---|---|
SchematicManager.CreateSheet() — two sheets in one part | No. It does add a sheet and SchematicManager.Sheets grows, but DiagrammingManager.Sheets does not, the new sheet can never be made active (ActiveSheet wants a Diagramming.Sheet), and CreateSheetTemplateBuilder only re-templates an existing one. A sheet made this way is never drawable. |
CreateOffSheetConnectorBuilder | Commits. The call is SetConnection(ConnectionEndType.End, connection) — end type first — plus a Style, on a connection with one free end via SetEndLocation (SetEndPoint does not exist). |
OffSheetConnectorBuilder.SetDestination — the navigable link | Blocked, precisely. "The owning connection of destination off sheet connector is not in a run." NX-native navigation needs Schematic Designer runs, which the shipped templates do not set up. A committed connector also never appears in SchematicManager.OffSheetConnectors (reads 0). |
CreateSubNodeBuilder / NodeBuilder.GroupingAllowed / .Expanded | No. Sub-nodes reject an ordinary symbol ("The selected symbol is not valid as main symbol") — they are for instrument attachments, not nesting. The grouping flags do not exist on this release at all. |
So the shape NX imposes is one drawing sheet per part, therefore one part per level — and the tie is the stamp.

SOURCE_TC_ITEM, DRILL_TO and DRILL_SHEET, and those come back intact when the saved part is read again.tie check: 2/2 blocks tied (2 to a Teamcenter item, 0 by the detail part alone)
OK Fuel System 11 components share its Teamcenter item
OK Hydraulic System 13 components share its Teamcenter item
verifyPlatformTie is symmetric and refuses to grade a weak tie as a strong one: it catches a block pointing at a part that is not in the set, a detail sheet about a different Teamcenter item even when the path matches, an empty detail sheet, and a detail sheet no block drills into.
SetDestination will bind, plus a way to read an off-sheet connector back that is not the empty collection. Until then the descent is recorded in the drawing and verifiable from it, but a user follows it by opening the part DRILL_TO names rather than by clicking a symbol.The return leg, as built
Every symbol the generator places carries SOURCE_ID (the Teamcenter component id), SOURCE_TC_ITEM and the component name. A symbol with a SOURCE_ID is skeleton and already exists; a symbol the SME added has none and is new. That key is already in the parts on disk.
Read-back is complete — nodes and wire endpoints — after three dead ends worth recording:
| Route | Result |
|---|---|
Schematic.ConnectionBuilder.GetStartNode() | returns None on a committed connection, even though SetStartNode created it |
Schematic.Port | carries no owner reference at all |
Diagramming.NodeBuilder.GetAllPorts() | lists only static ports, never the dynamic ones a connection makes at commit |
| What works | entirely in the Diagramming layer: Diagramming.ConnectionBuilder.Start/.End → Diagramming.Port → PortBuilder.GetOwningConnectableElement() → the element. Then join that element to its Schematic node by position — the two layers hand out different object tags but both report the one location set via SetLocation. |
The diff was proven on a real edit. A simulated SME placed an unstamped angle valve called ReliefValve, wired it to the pump, and renamed FuelFilter:
nodesUnchanged 11 nodesAdded 1 nodesRemoved 0 nodesRenamed 1 nodesAmbiguous 0
wiresUnchanged 11 wiresAdded 1 wiresRemoved 0 wiresUnresolved 0 wiresArtifacts 1
needsReview 0
added node VA000004 ReliefValve reason: no SOURCE_ID
added wire ElectricBoostPump -> ReliefValve
reason: wire touches a component with no SOURCE_ID, so the generator did not draw it
Four design points in that diff matter for the write-back:
- Diff against the spec, not the exchange bundle. The generator deliberately omits the composite whole and out-of-discipline components, so a bundle diff would call them "deleted".
- A copied symbol inherits
SOURCE_ID. Identity is the pair (SOURCE_ID, NX tag). First occurrence matches; every later one is a new component with a stale stamp. Getting this wrong duplicates objects in Teamcenter. - Wires are a multiset of (from → to). The fuel system genuinely has two
FuelControlUnit → ElectricBoostPumpwires — command and power — and a set would swallow one. - A wire touching an unstamped component is ADDED, not "unresolved" — it cannot be keyed by id because its endpoint has no id yet.
The obvious implementation cannot work, and it fails silently
fnd0bl_connected_end1 / fnd0bl_connected_end2 are occurrence properties, and neither write route reaches them:
| Attempt | Result |
|---|---|
Core-2007-01-DataManagement/setProperties on the connector's BOMLine | HTTP 200, empty partialErrors, both ends still empty on read-back |
itemLineProperties on the addOrUpdateChildrenToParentLine that creates that occurrence | accepted, and likewise ignored |
What works is Teamcenter's own operation for this exact gesture: Diagramming-2014-06-DNDManagement/createConnectionPortsAndConnect with connectCase = create2PortsConxAndConnToPorts, then saveBOMWindows, which is what commits. Proven live:
OK ElectricBoostPump -> ReliefValve
ports fuelOut1 on ElectricBoostPump, fuelIn on ReliefValve
connector 042321; model now reports 12 connection(s) with both ends bound
typing both ports typed to FuelInterface
end1: fuelOut1.1 on 041948/-/ElectricBoostPump direction=Output implements=041932/-/FuelInterface
end2: fuelIn.1 on 042320/-/ReliefValve direction=Input implements=FuelInterface
CreateAndPasteInputInfo declares clientId, opType and pasteRelation all as required. Omit any one and the tier answers "An error has occurred during the JSON parsing" — which reads like a malformed body, not a missing field. The same message means the same thing on ItemElementLineInfo2 (needs clientId + occType) and on executeSavedQuery (needs limit). Treat that message as "you dropped a required attribute".
(b) The paste target is the BOMLine, not the revision — a revision comes back as "invalid object instance tag … property bl_bomview".
(c) The operation does not type the ports it creates. Typing is what makes a port routable, so it is applied afterwards with the same GRM relation and read back to confirm.Choosing where the new component goes
A Cameo push puts every block in two places — under the owning package (a namespace listing) and under the composing block (the architecture). The 11 fuel components therefore vote 11–11, and the original rule (demand unanimity) could never fire. The tie is broken on the model's own evidence: the existing connectors hang under exactly one of the candidates, and that is by definition the composite where this system is wired.
Behavior (12 functions) over FuelSystem (11 components), and filed a fuel component under behaviour. The data answers it instead: the diff's UNCHANGED nodes are exactly what the generator drew on that sheet, so their common BOM parent is the parent. If they disagree, skip and report rather than guess.Receipts and undo
The write-back mutates the system of record, so every created uid is written to a receipt as it is created — not at the end, because a run that dies half way is exactly the run whose objects need removing. The undo order is not negotiable; each step exists because of a failure that reported success:
saveBOMWindowsdeleteObjects on an item that a saved BOM line still references returns "ok" and deletes nothing.
A related object refuses with error 9043 ("may be checked out, or referenced").
Deleting a revision leaves the item as a childless husk that still answers a type query.
A port hangs on its block through the block's BOMView revision, so a port whose owner is being deleted in the same run cannot go until that owner has gone.
Every delete is confirmed by re-reading the uid. Round trip proven: apply → verify → undo → the model is back to 11 components and 11 connectors, with no orphans of any type.
Decision 4: the merge guard
The moment Teamcenter holds more than Cameo, the Cameo push stops being a replace and has to become a merge. This cannot be prevented from here — the push happens inside the HCL connector, Check-Out discards local state before it pulls, and the operation is a replace. So the guard is external and evidential.
node bin/guard-tc-model.mjs --item 041908 --snapshot out/guard/041908.before.json
# ... run the Cameo -> Teamcenter push ...
node bin/guard-tc-model.mjs --item 041908 --verify out/guard/041908.before.json \
--receipt out/nx/SME_EditTest.writeback.json
--verify exits 1 when anything was removed, so a pipeline stops. --receipt marks which removals destroyed Teamcenter-side enrichment — work that exists nowhere else. Proven in both directions: baseline 49/29/11 → OK; after enrichment 50/31/12 → added 1 object, 2 ports, 1 wire, 0 removed; after a destructive push → exit 1 with all four removals flagged ENRICHMENT by name.
Two subtleties the 27 tests pin down. A component legitimately has two parents, so parents are compared as a sorted set. And a port that survives with its typing stripped is a removal in substance — the object is still there, but Seg0Implements is what makes a port routable, so losing it trips the rule on its own and is reported by name with what it used to be.
Facts about the shipped symbol library
320 symbols under <NX>\DIAGRAMMING\schematic\library\contents, disciplines Piping (ANSI/ISO/DIN), Hydraulic (ISO 1219) and HVAC. Things that cost real time:
| Finding | Consequence |
|---|---|
Branch fittings cannot be placed at all. Eleven symbols — every tee, cross and junction across all disciplines — carry no <Annotation>, and Commit fails with "Cannot place the symbol because it has not defined the label" under every combination of NodeCreationType and NodeType. | They are flagged placeable: false and the generator refuses to emit one, so a future mapping rule cannot silently pick one and fail inside NX with a message that reads like a data problem. NX makes a tee by branching a connection (SetStartTeeSymbolId, untried). |
No active sheet, no builder. CreateNodeBuilder raises "No Diagramming Sheet is found in the part" until SheetManager.ActiveSheet is assigned — even though the template contains a sheet and DiagrammingManager.Sheets lists it. | It is a plain settable property. Nothing sets it outside the GUI; assigning it is the whole fix. |
| Sheet y grows downward from a top-left origin. | The opposite of modelling coordinates. A node at y=19.5 on a 22-inch sheet lands on top of the title block. |
| A missing percent attribute means zero, not centre. | <Location y_percent="0.5"/> on the filter inlet is x=0 (left edge), y=50%. |
Ports are single-use — every shipped port declares numberAllowedConnections="1". | Handing the same port to two wires loses one silently, so the generator keeps a per-node ledger of spent port ids. |
<Unit> is per symbol: Piping is inches, Hydraulic is millimetres. | One sheet per discipline, matching the template. --discipline pins it; an unpinned mixed run splits the system across two sheets and loses the pathways that cross. |
The PDF builder commits and writes nothing unless SourceBuilder.SetSheets names the sheet. | It hangs off Part.PlotManager, not Session. |
Design decisions worth knowing
- Pathway direction is repaired, not trusted. The bundle's from/to reflects the SysML connector's authoring order, not flow — 041908 contains
FuelFilter.fuelIn → CheckValve.fuelOut, which is the wire drawn backwards. Port direction is the evidence that fixes it; 5 of 11 fuel pathways were re-oriented, and the count is reported. - Back edges do not drive layout. Every real fluid system is a closed loop. Ranking the hydraulic return line as if it were a chain put the reservoir at the far right of the sheet while it also fed the leftmost component. Cycle-closing edges are dropped from the ranking graph — still drawn, just not laid out against.
- Name beats description.
InvertedPickupSelector's description mentions a reservoir; matching prose first drew a selector as a tank. - The composite whole is skipped, and says so. A component with no ports and no connections is the assembly, not an item on a P&ID.
FuelSystemis dropped and reported instats.skipped. - A regulator is drawn as a globe valve. The bare actuator symbol has vertical-only ports and forces a jog on a horizontal run. Overlaying the actuator glyph needs
SetAttachedSymbol, but no shipped valve declares a mounting port for it, so this is left alone rather than guessed at.
Getting the drawings into Teamcenter
Both schematics are checked in, hanging off item 041908 by IMAN_specification, each a Zip of the .prt plus its PDF. Getting the bytes there took finding the one upload shape this tier accepts:
POST http://<fsc>:4544/fms/fmsupload/?ticket=<TICKET VERBATIM>
multipart/form-data, ONE part named fmsfile_0
+ live session cookies + X-XSRF-TOKEN
%2f %3a %3d and + for spaces, and carries an FMS_SHA256_SIGNATURE over its own text. Running encodeURIComponent on it turns %2f into %252f, the signature stops matching, and you get TICKET_VALIDATION_FAIL_0. That bug led to a wrong conclusion that the file server was network-blocked. The error grammar is diagnostic: MISSING_TICKET_0 = no ticket parsed (wrong path or field form); TOO_FEW_TICKETS_0 = ticket fine, file part misnamed; TICKET_VALIDATION_FAIL_0 = ticket mangled. Decode the ticket — it names the volume, the user and the expiry.Cleanup gotcha: a dataset attached by GRM will not delete (code 9043). deleteRelations first, then deleteObjects.
Not done
- Real NX tee objects. Branches currently share an anchor point on the upstream symbol's outlet, which reads correctly on the drawing but is not an NX fitting object.
- Instrument balloon field text. Three of the eleven fuel symbols keep the bare tag: the ISO instrumentation balloon draws its own two-field text rather than a plain annotation. They are named in the result's
notRelabelledrather than passed over. - Managed NX mode — see the status board for the two independent breakages behind it, both diagnosed, neither fixed.
- The Teamcenter-side scoping rule. Which elements define the fuel or hydraulic specification still comes from the port-domain registry. It should eventually come from a classification, attribute, branch or saved query — never guessed from names.