TeamcenterKnowledge

Skills

TC AWC Custom Tab

Skill tc-awc-custom-tab. Add a whole TAB (an XRT ) to the summary panel of one Teamcenter business object type in Active Workspace - the top-level inject pattern OOTB uses for Weight and Balance / Cost / Parameters, how tab order and the overflow chevron decide whether anyone ever sees it, why type scoping belongs in the REGISTRATION rather than in visibleWhen (an occurrence sheet is not what renders for a block), gating a tab on a site preference, and the verification traps where a screenshot lies in both directions. Use for "put a custom tab on this object type", scoping a tab that appears on too many objects, a per-type dashboard or report tab, or a "my tab does not appear" investigation.

A tab in the summary panel is an XRT <page>. Nothing more. Adding one needs no WAR rebuild, no kit build and no BMIDE: it is two datasets and one preference, all read from the database on every render.

Read tc-awc-stylesheets first if you have not. It owns the layer beneath this one: which style sheet a surface actually resolves, the AWC_<Type>.<...>RENDERING precedence chain, the SWF module that backs an htmlPanel, and the two silent-failure traps on the preference write. This skill only covers the part that is specific to contributing a page rather than a section.

Proven end to end on TC 2606 (TC2606 VM) on 2026-08-07: a Port Audit tab on Awb0DesignElement, carrying live per-object properties plus a module-backed panel, rendered in Active Workspace and captured in pixels.

The contribution pattern

A tab is contributed by injecting a <subRendering> that contains one <page>, at TOP LEVEL of the host style sheet - as a sibling of its own <page> elements, not inside one.

This is not a trick. It is exactly how the OOTB tab row is assembled. The tail of Pmg1CPGAwb0DesignElementSummary.xml is nothing but this pattern repeated:

<rendering>
    <header>...</header>
    <content visibleWhen="...">
        <page titleKey="tc_xrt_Overview">...</page>
        <page titleKey="tc_xrt_Markup">...</page>
    </content>
    <content visibleWhen="..."><inject type="dataset" src="Wnb1WeightandBalanceSummary"/></content>
    <content visibleWhen="..."><inject type="dataset" src="Ct1CostSummary"/></content>
    <content visibleWhen="..."><inject type="dataset" src="Nxs0AttributesTableSummaryAwb0Element"/></content>
    <content visibleWhen="awb0UnderlyingObjectType != null"><inject type="evaluate" src="awb0UnderlyingObject"/></content>
</rendering>

Weight and Balance, Cost and Parameters are each a whole tab arriving through one <inject>. Copy that, and your tab is indistinguishable from theirs.

The smallest OOTB example of an injected tab is Ct1CostSummary - a tab whose entire body is one declarative module view. It is the template worth starting from:

<subRendering>
    <page titleKey="ct1Cost">
        <section titleKey="tc_xrt_CostDetails">
            <htmlPanel declarativeKey="Ct0CostDetailsTable"/>
        </section>
    </page>
</subRendering>

⚠ An injected file MUST be rooted in <subRendering>, never <rendering>. Same rule as for sections.

title is the plain-string fallback for titleKey. A tab titled purely with title="Port Audit" and no titleKey renders literally, verified in pixels. You do not need a localization key to get a working tab, and inventing an unresolvable titleKey is how you get a tab labelled with the key itself.

Tab ORDER is document order, and the row overflows

★★ Where you put the <inject> decides where the tab lands, and the row is not infinite. On a vanilla 2606 tier the Awb0DesignElement tab row already carries Overview, Markup, Weight and Balance, Cost, Parameters, Changes, Finishes and Partners before you add anything, and everything past that collapses behind an overflow chevron (DIV.sw-tab-overflowContainer). The full page list on this tier runs past twenty entries: Classification, Baselines, Where Used, Attachments, Materials, History, Characteristics, Relations, Participants, Simulation, Physical Test, Reports, Solution Variants.

Append at the end and the tab is genuinely contributed, genuinely resolvable, present in document.body.innerText - and invisible on screen. That reads exactly like a failed registration and sends you back to debug something that already works.

Anchor on the FIRST top-level element that contributes a tab of its own, and insert immediately before it. That puts the new tab early, in the visible run, right after Overview. The anchor differs per host, so read the file rather than assuming: Pmg1CPGAwb0DesignElementSummary wraps its tab injects in <content visibleWhen="..."> (anchor on the Weight and Balance block), Awp0Fnd0LogicalBlockRevisionSummary uses bare injects (anchor on Fs0FMEDAInputTab). Match whichever form the host uses so the diff stays minimal.

Scoping the tab: the registration picks the TYPE, visibleWhen picks the CONDITION

Without a condition a tab appears on every object of the registered type site-wide. Registering on Awb0DesignElement therefore puts it on every BOM occurrence on the tier: a fastener, a document, a system block, all of them.

★★★ The fix is NOT a visibleWhen on the underlying type. Move the REGISTRATION. Both the book and the tier agree:

  • The Customization book, under inject: "Do not use the visibleWhen attribute with the inject element to check the object type. Use multiple style sheets, each registered to a different object instead. Do not attempt to create a single, over-arching XRT for all object types."
  • Measured on TC2606, and this is the part that would have cost a day: selecting a logical block in a structure and switching the secondary area to Details does not render the OCCURRENCE style sheet at all. It renders the underlying revision's type summary, via AWC_Fnd0LogicalBlockRevision.SUMMARYRENDERING = Awp0Fnd0LogicalBlockRevisionSummary. The tab row proves it: Overview / Diagrams / Requirements / Parameters / Interfaces / Where Used / Documents / Test Coverage / Test Results, with none of the occurrence sheet's Markup / Weight and Balance / Cost.

So an occurrence-registered tab gated with visibleWhen="awb0UnderlyingObjectType == Fnd0LogicalBlockRevision" would have rendered precisely nowhere: hidden on every surface that reads the occurrence sheet, and absent from the surface that actually shows blocks. It would have looked like a broken condition and it would have been a wrong home.

Verified end to end, all three legs:

Change Surface Result
register AWC_Fnd0LogicalBlockRevision.SUMMARYRENDERING fuelControlUnit, mainFuelTank *Port Audit present, second after Overview
revert AWC_Awb0DesignElement / AWC_Awb0Element to OOTB a plain item occurrence tab gone, stock row restored
Xc0PortAuditEnabled = false fuelControlUnit tab gone, rest of the row intact

★★ Moving a tab between an occurrence sheet and a revision sheet means rewriting its properties. The two surfaces have different vocabularies and there is no overlap to coast on: the occurrence carries awb0ArchetypeId, awb0ArchetypeRevId, awb0OccName, awb0Quantity; the revision carries item_id, item_revision_id, object_name, object_type, owning_user, last_mod_date. A straight copy renders a section of blank labels, which reads as a broken panel rather than as the wrong property names.

visibleWhen is supported on exactly two elements: <content> and <page>. Not <section>, not <property>, not <htmlPanel> (Customization book, Conditional content). Once the type is handled by the registration, use it for the condition within the type. A site preference is the most useful one, because it lets an administrator retire the tab without touching a style sheet:

<page title="Port Audit" visibleWhen="{pref:Xc0PortAuditEnabled}==true">

Proven both ways on 2606: true renders the tab, false removes it and leaves every other tab in place.

★★ Creating that preference over SOA does not work. Administration-2012-09-PreferenceManagement/setPreferencesDefinition returns HTTP 200 with empty ServiceData and creates nothing - the same silent shape as the setPreferenceIn trap. Use the utility, and mind that the flag is -file for import where export wants -out_file:

preferences_manager -u=infodba -p=infodba -g=dba -mode=import -scope=SITE -action=OVERRIDE -file=<pref.xml>

The XML is the same shape the export emits: `

true . Write it **without a BOM** ([System.IO.File]::WriteAllTextwith a no-BOM encoder), and re-export to confirm it exists: a missing preference makes{pref:X}==true` silently false, so the tab just never appears.

What visibleWhen compares (all from the Customization book, The visibleWhen attribute):

Form Example
property on the selected object awb0ServiceAdapter == 4G, awb0Parent != null
property on a related object (DCP) REF(awb0UnderlyingObject,ItemRevision).awb0IsDiscoveryIndexed == true
preference {pref:AWB_ShowMarkup} == true
BMIDE global / type / property constant {const:ContextType:Att0EnableComplexValue} == true
location / sublocation ActiveWorkspace:SubLocation != com.siemens.splm.client.occmgmt:OccurrenceManagementSubLocation
XRT context ActiveWorkspace:xrtContext=={"interfaceTable":"visible"}

and is supported. or is not. Date and reference properties can only be tested for null / not-null. Array properties match if the value is in the array; you cannot compare against a list.

Two kinds of page, and the 2606 work-mode change

The Customization book distinguishes:

  • XRT pages - layout defined in the style sheet itself. This is what you are authoring.
  • PNT pages - <page titleKey="..." pageNameToken="AwDetailsSublocation"/>, where the style sheet only contributes the tab and the client owns the view.

On 2606 the showObject location has primary work modes, and the book's migration note matters if you inherit custom location-level style sheets: a location style sheet containing only XRT pages should have its preference deleted, because the type Summary already carries those pages. A combination of XRT and PNT pages should be reduced to the single Details PNT entry. Getting this wrong produces duplicated or missing tabs that look like an authoring bug.

Binding the tab to the selected object

A <property> in your page is bound to the selection, and this is the cheapest way to prove it. The Port Audit tab's "Audited Block" section renders the selected block's real item_id, item_revision_id, object_name, object_type, seg0Kind, owning_user and last_mod_date; selecting a different block in the tree changes them.

An htmlPanel module view is NOT automatically bound - see tc-awc-stylesheets. To bind one, the documented mechanics are:

<htmlPanel declarativeKey="MyPanel">
    <property name="item_id"/>
    <property name="object_name"/>
</htmlPanel>

The <property> children exist to force those Teamcenter properties to be loaded into memory for the panel; without them the view model may find nothing. Inside the view, the selection is reachable as subPanelContext.selected.properties.<name>.dbValue, and a fixed string can be passed with context="..." and read as subPanelContext.declarativeKeyContext. A view model can also call a real service with "actionType": "TcSoaService".

★ Until a panel is genuinely bound, say so on the panel itself. A tab that silently shows another object's numbers is worse than no tab.

Install and register

Create the datasets with the shipped utility, not createDatasets over SOA:

install_xml_stylesheet_datasets -u=infodba -p=infodba -g=dba -input=<manifest.txt> -filepath=<dir> -replace

Manifest lines are datasetName, file.xml. Run it inside the TC environment shell (tc_root\tc_menu\tc_Vanilla_Env.bat) or it exits 0xC0000135 having done nothing.

★★ -replace is silent about replacing. A first install logs Successfully created dataset "<name>"; re-running with -replace on an existing dataset logs the type/format lines, Process file, Finished processing file, and no per-dataset line at all, exit 0. That is success, not a no-op - but the log cannot tell you which, so verify a content change by rendering it, never by reading the utility output.

★★★ -replace does not overwrite in place: it adds a dataset VERSION each run, and the row whose object_string has no semicolon is NOT the newest. Measured by a parallel session on this tier, which reached ;5. Consequences:

  • A saved-query check against the bare-name row compares against a stale version and reads exactly like "-replace silently did nothing". Select the highest version.
  • The renderer serves the newest version, which is why verifying by rendering works while verifying by name does not. That asymmetry is the whole reason the render check is the reliable one.
  • Repeated iteration accumulates versions. Harmless, but do not read a high version count as corruption.

Then register, as one preference write per type (tc-awc-stylesheets has the precedence chain, the setPreferenceIn argument-name trap and the session-cache trap):

AWC_Awb0DesignElement.SUMMARYRENDERING = <your patched host summary>
AWC_Awb0Element.SUMMARYRENDERING       = <same>

Read the preference back from a FRESH PROCESS. preferences_manager -mode=export -scope=SITE -out_file=<f> then grep, because SOA preference reads are cached per session and will hand you the old value on the connection that just wrote it.

Patch the host summary textually, never through an XML round trip, so the administrator's diff is the block you added and not 240 reformatted lines.

Verifying: the screenshot lies in both directions

This is where the time goes. Four failure modes, all hit while proving the Port Audit tab.

★★★ captureBeyondViewport fabricates skeletons

Page.captureScreenshot({captureBeyondViewport: true}) re-renders the page into an off-screen surface sized to the whole document. Active Workspace's panels are virtualized and do not paint into that surface, so the capture comes back as tidy grey placeholder bars over a DOM that is fully populated.

It is indistinguishable from a tier stuck on skeletons, and it survives --headed, which is otherwise the tie-breaker for exactly this doubt. Measured: the same page that screenshotted as grey bars returned, at those exact coordinates, DIV.sw-row.sw-sectionTitleContainer ... text="Audited Element" and text="FAIL - 6 of 34 port/branch cells", background transparent, animationName: none.

Capture fromSurface: true with captureBeyondViewport off, and make the viewport tall enough instead. bin/shoot-awc.mjs now defaults to this; --beyond-viewport opts back in.

★★ Text present is not painted, and guessed selectors are worse than nothing

--wait-for matches on document.body.innerText, which populates well before the panel paints - the run that first proved this tab matched in 2 seconds and screenshotted nothing. Allow a dwell (--dwell) after the match.

When a capture and the DOM disagree, sample document.elementFromPoint(x, y) at the disputed coordinates and report the real class chain, text and computed background. Do not count [class*="skeleton"]: those elements do not carry that class, so the probe returns 0 on a visibly grey page and confirms whatever you already believed.

Useful assertions that are cheap and unambiguous:

[...document.querySelectorAll('[role="tab"]')]
  .map(e => (e.getAttribute('aria-selected') === 'true' ? '*' : '') + e.textContent.trim())
// -> ["Overview","Markup","*Port Audit","Weight and Balance","Cost", ...]

That single line answers "is my tab contributed", "is it in the visible run", and "is it the active tab" at once.

★★ Clicking a tab: poll, and prefer [role="tab"]

The tab is A[role="tab"].sw-tab-title wrapped in DIV.sw-tab, with the label in DIV.tab-text.sw-tab-truncateText.

  • querySelectorAll returns DOCUMENT order, not selector order. A combined selector like '[role="tab"],button,a,div' hands back DIV.sw-tab, the container, whose click does nothing. Query [role="tab"] in its own call first and only then fall back.
  • Poll for the target instead of sleeping a fixed time. The tab strip appears ~12 s in on this tier. A click fired at 8 s reports "not found", which is indistinguishable from a tab that was never contributed.
  • Icon-only controls carry no text. The summary-panel toggle is BUTTON[aria-label="Information"] with empty textContent, so a matcher that only reads text can never reach it and reports a missing control rather than a missing selector. Match textContent || aria-label || title.
  • Disambiguate by scope when the same string appears twice. "Details" is both a top-level tab and an item in the view-switcher menu; clicking the wrong one navigates away from the object. Preferring [role="menuitem"] when a popup is open helps but is a race, because the popup can close between the click that opened it and the poll that looks inside it. An explicit --click-text "menuitem:Details" scope is the reliable form.

★★ Switching the secondary view destroys the JS execution context

A CDP Runtime.evaluate issued against the old context after a view switch never answers: no error, no response. The run ends with Detected unsettled top-level await and no screenshot, after everything had actually worked.

Give every CDP call a deadline and treat a timeout as retryable rather than fatal. Keep the screenshot's deadline generous and separate (encoding a 1700x1500 surface routinely passes 20 s); a short global timeout throws away a run that had already succeeded.

★★ The summary panel is not always the secondary area, and "Information" is a different sheet

Which surface you are looking at decides which preference is read, so this is not a UI detail.

  • For a plain item occurrence the Content sublocation's secondary area is the summary panel, and it renders the occurrence sheet.
  • For a Fnd0LogicalBlockRevision occurrence it is the Architecture diagram. The summary is behind the view switcher at the top of that pane: Architecture / Relations Graph / Relations Tree / Details, and choosing Details renders the underlying revision's type summary.
  • The Information button in the main toolbar (BUTTON[aria-label="Information"]) opens a different panel entirely, driven by INFORENDERING, not SUMMARYRENDERING. It has no tabs. Reaching for it while hunting a missing tab wastes a cycle and proves nothing.

★★ A session with nothing selected skeletons EVERY tab, including OOTB ones

The summary panel renders the selected occurrence. Active Workspace restores a prior session with a Restore from where you left off? banner and the tree unselected, and until something is selected every panel on every tab - yours and Siemens' - shows placeholders.

Always run the OOTB control. If Overview skeletons too, the problem is not your style sheet. Click Restore, or select the element row, before judging anything.

The tier's own health comes first

Before any of the above, confirm the tier is not starved: MemoryDemand below MemoryAssigned on the VM, and a tcserver count well under PROCESS_MAX. See tc-vm-operations. Measured healthy during this work: 17 tcservers of 30, 6.3 GB free in the guest.

★ If the SOA cookie transplant stops producing a session (on this tier after a reboot, GET / returned 200 with no Set-Cookie at all, so there was no XSRF-TOKEN to carry and the SPA rejected the bare JSESSIONID), fall back to driving Active Workspace's own login form. Set the field values through the prototype's value setter and dispatch input/change, or the framework never sees them and the form submits empty.

Element support, measured on 2606

Both rows below were measured on the same tab, one variable apart, with the tab itself as the control:

Inside an injected <page> Result
<section> + <property> renders
<htmlPanel declarativeKey="..."> (SWF module view) renders
<label text="..."/> renders
<break/> renders
two <column width=".."> accepted; collapses to stacked when the panel is narrow

This positively retires a claim that is still repeated in older notes: that <label> hangs the render while <property> is fine. It does not. The original table was measured against a preference the client was never reading, so every variant "failed" for the same unrelated reason. Version 1 of the Port Audit tab (property + htmlPanel) rendered; version 2, differing only by a <label> and a <break>, rendered identically. Do not re-derive this.

Rollback

One preference write per registration, back to the OOTB value. The datasets can stay - they are inert until a preference names them.

AWC_Fnd0LogicalBlockRevision.SUMMARYRENDERING -> Awp0Fnd0LogicalBlockRevisionSummary
AWC_Awb0DesignElement.SUMMARYRENDERING        -> Pmg1CPGAwb0DesignElementSummary
AWC_Awb0Element.SUMMARYRENDERING              -> Awb0ElementSummary

★★★ READ THE PREFERENCE BEFORE YOU REVERT IT. A host XRT is a SHARED resource and the last writer wins silently. Two teams adding a tab to the same type both patch the same registration. On TC2606 that happened within a day: a second session generated Xc0LogicalBlockRevSummaryXc0Tabs from OOTB carrying BOTH its inject and the Port Audit one, and repointed AWC_Fnd0LogicalBlockRevision.SUMMARYRENDERING at it. Following the rollback line above verbatim would have deleted the other team's tab, and re-running the original patcher would have deleted it too. Neither produces an error; the tab just stops appearing.

So, every time:

  1. Read what the preference points at now (fresh process, preferences_manager -mode=export).
  2. If it is not the host you created, open it and see whose injects it carries.
  3. Revert to OOTB only if yours is the only inject. Otherwise remove your <inject> line from the shared host and leave the registration alone.

Same rule when re-running a patcher: regenerate from the CURRENT host, not from OOTB, or you drop every inject added since.

A tab gated on a site preference has a softer rollback that needs no style-sheet knowledge at all: set the preference false. Worth preferring for anything an administrator may want to switch off in a hurry.

Enumerate before you declare yourself clean. Export site preferences and grep for every registration whose value is your prefix, not just the one you meant to set. This session found a diagnostic left behind by an earlier one (AWC_ItemRevision.showObjectLocation.OccurrenceManagementSubLocation.SUMMARYRENDERING = Xc0ItemRevSummaryV3) that a written-up "reverted" state did not mention.

Worked example: the Port Audit tab

Registered on AWC_Fnd0LogicalBlockRevision.SUMMARYRENDERING, so it is on system blocks and nothing else. Xc0PortAuditTab.xml:

<subRendering>
    <page title="Port Audit" visibleWhen="{pref:Xc0PortAuditEnabled}==true">
        <column width="40%">
            <section title="Audited Block">
                <property name="item_id"/>
                <property name="item_revision_id"/>
                <property name="object_name"/>
                <property name="object_type"/>
                <property name="seg0Kind"/>
                <property name="owning_user"/>
                <property name="last_mod_date"/>
            </section>
        </column>
        <column width="60%">
            <section title="Port Coverage Ledger">
                <label text="Scored against the Teamcenter master port set. Snapshot, not live."/>
                <break/>
                <htmlPanel declarativeKey="Xc0PortCoverage"/>
            </section>
        </column>
    </page>
</subRendering>

seg0Kind is worth knowing about: it is a stock TC attribute that the OOTB Cameo mapping already populates with the applied SysML stereotype on eleven object types, so a domain-aware column costs nothing and needs no BMIDE deploy. It renders here as Kind: Block.

Xc0LogicalBlockRevSummaryPortAudit.xml - the OOTB Awp0Fnd0LogicalBlockRevisionSummary with exactly one two-line block added, immediately before the first tab-contributing inject:

    <!-- Xc0: Port Audit tab. One inject, contributing a page. Revert by deleting this block. -->
    <inject type="dataset" src="Xc0PortAuditTab"/>

Note there is no wrapping <content> here: the host's own tab injects are bare, so matching them keeps the diff to two lines. Wrap in <content visibleWhen="..."> only when the host does.

Generator, deployer and capture tool live in Capital_TC_Integration: out/awc/tab/patch-summary.mjs (textual patch, asserts the inject count), bin/shoot-awc.mjs (login, click, wait, capture, DOM probes).

★ A trap in the patcher itself, worth copying rather than rediscovering: when the anchor is a substring in the middle of a line, taking "everything before the match" as the indent splices half a tag into the output and produces nested unterminated attributes. Take the line's leading whitespace, and validate the result parses before installing it.

Documentation

2606 set, D:\Siemens\Help Server\collections\documentation\external\PL20251212545240207\en-US\tc_help\:

Topic Where
XRT element reference (page, inject, htmlPanel, label, column, section) Active Workspace Customization, "XRT element reference"
visibleWhen and the comparison table same, "Conditional content" / "The visibleWhen attribute"
XRT vs PNT pages, primary work modes same, "Configuring primary work modes for the show object location"
Passing properties and context to an htmlPanel, calling a service from one same, "Specifying HTML content"
install_xml_stylesheet_datasets Teamcenter Utilities

★ The 2506 set under PL20241125556497283 is a different release. pypdf extracts these cleanly, which is how to check a flag or an element attribute rather than trusting a write-up.

Related skills

tc-awc-stylesheets (the layer beneath: resolution chain, registration, SWF modules), tc-capture-awc-calls, tc-vm-operations, tc-soa-payload-shapes, tc-verify-and-cleanup.


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