TeamcenterKnowledge

Skills

TC Classification Bulkload

Skill tc-classification-bulkload. Bulk-load any domain into Teamcenter Classification. The four-stage reconcile / resolve / generate-plmxml / idempotent-rerun pipeline proven end to end, why the plmxml plus CLI route beats direct SOA writes, the search-index caveat that makes REST-write success reports unreliable, and the real documented SOA op for creating hierarchy nodes (not just placements). Use for classifying items at scale (notes, functions, logical elements, signals, parts) or building the class tree itself.

Domain-independent pipeline, proven end to end on an 845-plus-48 placement load, and written to carry directly to the next domain by swapping the source spreadsheet and the class-tree branch.

The four stages

1. Reconcile

Parse the system of record (a spreadsheet, an extract) into a flat work-order of required (item, class-breadcrumb) placements, then diff it against what is actually live in Teamcenter today.

Do not trust that a prior one-time load did this correctly. Re-derive the true requirement from source data. On the proven run this surfaced that more than 60% of required placements were missing, and that the original loader supported only one classification per item and silently dropped the rest.

2. Resolve live

Given a class breadcrumb (an arrow-separated path) and a leaf name, resolve it to Teamcenter's real class ID:

Internal-IcsAw-2019-12-Classification/findClassificationInfo3
  searchAttribute: CLASS_NAME

Disambiguate candidates by walking classParents to match the full expected ancestor chain. This endpoint is undocumented (it is what the Active Workspace Classification UI itself calls) but read-only and therefore low risk. It is not in the SDK's WSDL kit; see tc-capture-awc-calls.

2b. Resolving items you JUST created in this same pipeline

If an earlier stage of this same pipeline created the items being classified (see tc-object-authoring's createAttachAndSubmitObjects), do not assume you can find them again by searching for the source data's original ID.

Found live 2026-08-05, reported by a real user whose classification stage resolved zero items despite the create stage having succeeded: most Requirement-family types have a site naming rule that faults on a manually-supplied ID ("no matching Naming Rule pattern could be found", code 74007), so the create step has to let Teamcenter auto-assign the real ID instead. The auto-assigned ID (e.g. REQ-000351) then has zero relationship to the source spreadsheet's original designation. A name/ID search for the original designation will never find it — not a staleness problem that resolves itself, a permanent mismatch. (A user who manually corrected the ID mismatch and still got nothing is proof of a second, separate cause stacked on top — see 2c below. It first reads as "must be staleness," but confirm rather than assume: it turned out to be something else entirely.)

Fix: track the mapping at creation time, don't search for it later. Have the create stage log {req_id, item_uid, revision_uid} per row as it creates (straight from the create call's own response). Have the resolve stage try the search first (for genuinely pre-existing items), and fall back to that log — keyed by the ORIGINAL untransformed identifier, not any domain-specific ID transform a reconcile step may have applied for a different, older identifier scheme — when search comes up empty.

2c. Search is scoped by Teamcenter's Project, not by account

Root-caused live 2026-08-11, superseding an earlier, incomplete diagnosis below this line in an older version of this skill. The earlier note said a DBA-privileged login "cannot find items via search at all" and reserved DBA strictly for the hierarchy-node create call. That was a real, reproducible observation but not the actual mechanism — DBA privilege itself does not block search.

The real variable is Teamcenter's session-level Project context: a setting distinct from user/group/role, visible in Active Workspace's own user-profile menu (top right — "Project: " / "No Project"), set post-login via Core-2015-10-Session/setUserSessionStateAndUpdateDefaults (pairs: [{"name": "currentProject", "value": "<TC_Project uid>"}] — it takes a UID, not the display name; passing a name faults cleanly with "The given string is invalid.", code 515029). executeSavedQuery (and therefore every "find item" step built on it) is scoped by this Project independently of which account is logged in.

Live A/B/A proof, same account throughout, same query, same real item: found under Project=F-35 (nFound 1) → not found under Project="No Project" (nFound 0) → found again after switching back (nFound 1). Separately, the account originally suspected of being "DBA and therefore write-only" turned out to simply have a different default Project set (an unrelated program) — pointing that same DBA session's Project at F-35 made its search behave identically to the ordinary account's, no privilege change involved.

Practical consequence: check/set Project before trusting any empty search result from ANY stage, not just the one using a DBA account — a stale or wrong Project on an otherwise-ordinary account produces the identical silent "nothing resolves" symptom. No SOA op to enumerate/resolve project name->uid was found working in this session (Core-2012-09-ProjectLevelSecurity's getUserProjects2 and siblings register but reject every body shape tried with a bare "invalid syntax" — no WSDL/XSD for the op was available locally to read the real shape from). Known project UIDs for a given site have to be captured once, live, from Active Workspace's own dropdown-switch network call (see tc-capture-awc-calls) and cached in a name->uid map; a raw UID always works directly, bypassing the map.

3. Generate the bulk-load artifact. Do not hand-write it.

The officially supported mechanism is server-side CLI:

plmxml_import                      # class hierarchy AND ICO/placement objects
clsutility -import -hierarchy      # hierarchy only
ics_connect -file=<item-list>.txt  # placements only
smlutility -create_indexing_views  # rebuild the classification search index

Prefer this over scripting direct SOA writes, for two reasons: it is the path Siemens' own delivery teams use on real programs, and it correctly rebuilds the search index (see the caveat below).

The plmxml schema:

xmlns:plmxml_cl="http://www.plmxml.org/Schemas/PLMXMLClassificationSchema"
transferContext="ICSExportICOs"

One <plmxml_cl:ICO icoId="<item_id>/-"> per (item, class) placement, carrying a <plmxml_cl:ClassId> for the leaf then repeated <plmxml_cl:Parent> elements walking the ancestor chain out to the classification root. Custom classes use numeric IDs; fixed built-in roots use symbolic IDs (SAM, ICM). Confirm the fixed-root suffix for whatever branch a new domain lives under: it may differ from the one you saw last time.

Multi-classifying one item is multiple <ICO> elements sharing the same icoId. That is exactly what the original single-classification loader got wrong.

3b. Creating hierarchy NODES (structure), not just placements

The three stages above build placements (ICOs) into an existing tree. Building the tree itself — the class/hierarchy nodes — is a different operation, and unlike the undocumented Internal-IcsAw placement endpoints, it has a real, documented SOA op:

ClassificationCore-2013-05-Classification/createOrUpdateHierarchyNodes

Found in the Help Server WSDL kit (ClassificationCore1305Classification.xsd / ...Service.wsdl, see tc-soa-docs-navigation) by grepping every .xsd for createOrUpdateHierarchyNodes — not discoverable by guessing service/domain names (a first blind guess of ClassificationCore-2013-05-ClassificationCore/... used the wrong domain segment and faulted with a generic syntax error indistinguishable from "this operation doesn't exist"; the real domain is Classification, matching the XSD's own .../ClassificationCore/2013-05/Classification namespace).

Request shape (hierarchyNodeDetails, one entry per node), fully live-confirmed 2026-08-05 under a DBA account — created a brand-new AbstractClass node plus a brand-new StorageClass child under it, neither existing beforehand, in one run, 0 errors, each independently verified via a direct getProperties read on the uid Teamcenter returned:

{
  "nodeToUpdate": NULLTAG,                 // NULLTAG = create mode
  "parent": {"uid": "<parent node uid>", "type": "unknownType"},
  "masterNode": NULLTAG,                   // only for Reference-type nodes
  "icon": NULLTAG, "images": [], "attachments": [],
  "nodeId": "<leaf name>", "nodeName": "<leaf name>", "nodeDesc": "<leaf name>",
  "nodeType": "HierarchyNode_Master",      // Group/Master/Reference; Group untested
  "storageClassTypeName": "",              // leave empty even for Storage Class -- see below
  "isLeafNode": false, "levelSortIndex": -1,  // yes, false/-1 for BOTH types -- see below
  "isAbstract": true,                      // true = Abstract Class, false = Storage Class
  "isAssembly": false, "allowMultipleClassification": true, "instanceCount": 0
}

An existing node's own parent reference (needed to attach the first new level) is its NODE_UID property, readable from findClassificationInfo3's clsClassDescriptors[...].properties — same read call as stage 2 above, no new lookup needed.

Three fields whose XSD prose is misleading, corrected by reading back a real existing node's actual getHierarchyNodeDetails:

  • storageClassTypeName looks like it should be a fixed type-name string (a first guess, "Cls0ClassBase", faulted with "class Cls0ClassBase cannot be found", code 132008) but a real Storage Class node's own value is just its own numeric class id — self-referential, doesn't exist until after creation. Leave it empty at create time; it gets populated automatically.
  • isLeafNode: a real, populated, genuinely-terminal Storage Class node reads back false. Not something creation intent sets — leave false.
  • levelSortIndex: a real node reads back -1, not a sequential per-sibling index. Leave -1 rather than trying to compute sibling ordering.

Response shape: the new node's uid is in the top-level nodeObjects field ([{"uid", "className", "type"}], one per input entry, in order) — NOT under ServiceData.created, the first (wrong) guess.

Same search-index caveat as REST placement writes (see below): a node created this way is invisible to findClassificationInfo3 immediately after — confirmed live, a query for the just-created node by name returned zero matches while a direct getProperties on its uid fully confirmed it exists. Track new nodes from the create call's own nodeObjects response within a run; don't re-query findClassificationInfo3 to "confirm" a just-created node, and don't trust a same-instance re-run's "already exists" check across sessions until smlutility -create_indexing_views has run.

Privilege: needs a DBA-privileged account/group (same "Active Admin" boundary as the Classification Manager UI) — a non-privileged session reaches the operation fine but faults with a real privilege error (code 51057), not a generic one.

RETRACTED 2026-08-11 — the generic delete op DOES work, contrary to the earlier note below. Core-2006-03-DataManagement/deleteObjects with {"uid": "<node uid>", "type": "Cls0MasterNode"} (the object's REAL type — passing "unknownType" is a plausible reason an earlier flat single-node test may have silently failed) successfully removed 5 real leftover test nodes, live-verified: a batch delete of a parent+child pair in one call partially failed with a real, honest error — "The Partition object cannot be deleted because it contains at least one child Partition. Please delete all child Partitions first." (code 280010) — for the parents, while the child in the same call succeeded. Re-running the delete call for just the parents, after their children were confirmed gone via a direct getProperties re-read, then succeeded cleanly with no errors. Delete bottom-up: children before parents, and re-verify by dereferencing the uid directly afterward (type comes back empty on a genuinely deleted object) — do not trust a same-batch "no error" as proof for objects with dependents.

An older attempt on a single, childless test node reported "no error, but the object was still fully readable afterward" — plausibly caused by passing "unknownType" instead of the object's real type rather than a genuine incapability of the operation; not independently re-confirmed which it was. Either way: prefer deleteObjects with the object's real type over assuming the Classification Manager UI is required. Search (findClassificationInfo3) staying stale for a deleted node proves nothing either way — dereference the uid directly to confirm.

This makes hand-rolling a class-hierarchy plmxml (a schema variant never actually seen) unnecessary — this documented SOA op is the confirmed path.

4. Idempotent re-run

Cache class and item resolution, diff against current live state, emit only the delta. This must be safe to re-run as the source data changes without tracking what was already done externally.

Actually do the second run. Re-running against already-populated state is the only thing that exercises the already-populated code path, and it is where latent bugs live: one generator's existence-check loop body was never reached on a first run and threw a NameError only on the second.

The caveat that makes REST writes untrustworthy here

Writes via saveClassificationObjects2 (undocumented AW-internal REST) report success but are invisible to findClassificationInfo3 reads taken immediately after, and stayed invisible after several minutes and a second write attempt.

Strong evidence the read is backed by a search index that only gets refreshed by smlutility -create_indexing_views, which cannot be triggered without TC-server shell access.

Practical consequence: do not treat the REST path's own "already classified" check as proof of persistence. The plmxml plus CLI path does not have this problem, which is the main reason to prefer it for anything beyond a quick manual fix.

Reference implementation

A standalone, no-Claude-dependency Python toolchain sits at Siemens/f35-notes-classification/lockheed_package/: reconcile.py, build_icos_plmxml.py, apply.py (placements), plus create_requirements.py (creates the source items themselves via tc-object-authoring's createAttachAndSubmitObjects) and build_hierarchy_plmxml.py (creates missing hierarchy nodes via createOrUpdateHierarchyNodes, above). All share tc_client.py, which as of 2026-08-11 also exposes set_current_project() and a TC_PROJECT env var on every script's connect() — see 2c above for why. Environment-variable TC connection config, pure stdlib plus openpyxl. Fork it for the next domain and adapt only the domain-specific bits: spreadsheet sheet names and columns, root class IDs, and the item-ID transform.

Related skills

tc-capture-awc-calls, tc-soa-docs-navigation (how the hierarchy-node op was found — grep every .xsd, don't guess service/domain names), tc-object-authoring, tc-verify-and-cleanup, tc-parameters-units (UMS is also built on Classification).


Generated from skills/tc-classification-bulkload/SKILL.md in the tc-automation-skills library, which is the canonical copy and also serves as the agent skill set for Teamcenter work.