TeamcenterKnowledge

Customization

Dispatcher: Anatomy of a Working Custom Translator

Read off a live Dispatcher host: a real custom translator, fully wired across all four surfaces, plus 2,715 real task logs from four months of operation. This is what the pattern looks like when someone has actually built it.

Infrastructure identifiers are omitted deliberately. Everything below is the transferable shape.

The architecture that a real one uses

The translator in question is a level 0 / level 1 hybrid, and it is worth understanding why that combination is the sweet spot.

Teamcenter side: the generic classes, no Java. In DispatcherClient/conf/Service.properties:

# <name> - client-side service registration (mirrors <other> standalone service; generic basic handlers)
Translator.SIEMENS.<Service>.Prepare=com.teamcenter.ets.translator.ugs.basic.TaskPrep
Translator.SIEMENS.<Service>.Load=com.teamcenter.ets.translator.ugs.basic.DatabaseOperation
Translator.SIEMENS.<Service>.Duplicate=false

The author's own comment says "generic basic handlers". This is the zero-Java route, chosen deliberately by someone building a production integration.

Module side: a thin .bat delegating to a modern runtime. The registered executable is a 1.3 KB batch file whose entire job is to set defaults and shell out:

setlocal
if "%NODE_EXE%"=="" set NODE_EXE=node
"%NODE_EXE%" "%APP_HOME%\bin\forward-module.mjs" ^
  --model "%~1" --rev-uid "%~2" --name "%~3" --workdir "%~4" ^
  --config "%APP_HOME%\deploy\dispatcher\adapters.json"
exit /b %ERRORLEVEL%

Note exit /b %ERRORLEVEL%. Propagating the child's exit code is the whole contract between your logic and Dispatcher's success/failure determination.

That is the pattern to copy. Dispatcher gives you queueing, retry, state tracking and an audit trail. The .bat is a 20-line shim. All real logic lives in a language you actually want to write, testable standalone, with the .bat never needing to change again.

Same three surfaces this KB describes

On the host, in adjacent config, the same translator appears as:

  1. A block in Module/conf/translator.xml with isactive="true".
  2. A launch script under Module/Translators/<Service>/.
  3. Prepare / Load bindings in DispatcherClient/conf/Service.properties, plus its name appended to that file's import= list.
  4. Extract/load preferences in Teamcenter.

The adapter config and the Teamcenter preferences agree exactly, which is the check worth running on any deployment. The check-in adapter invokes import_file with -type=Text -ref=text -relationType=IMAN_specification, and the corresponding preferences read back over SOA as _ets_dst_ds_type = Text, _ets_dst_nr_type = text, _ets_dst_relation_type = IMAN_specification. When those two drift apart, the load fails in a way neither file explains on its own.

⚠ The failure this deployment actually demonstrates

The translator has never run. Not once, in 2,715 tasks over four months.

Every piece of plumbing is correct: registered, active, scripted, bound, preferences set. What is missing is the menu preference family: ETS.DATASETTYPES.<PROVIDER>.<SERVICE>, ETS.PRIORITY.*, ETS.TRANSLATOR_ARGS.*, ETS.REPEATING_UI.* all return cannot be found.

Without ETS.DATASETTYPES, the service never appears in the Translation Selection dialog for any dataset type, so no user can create a request for it, so it never runs. Nothing errors. Nothing logs. It simply sits there looking fully configured.

This is the single most expensive Dispatcher failure mode, and it is silent. A half-configured translator is indistinguishable from a working one by inspection of the Module host alone. The two preference families are independent, and you need both.

Verifying you have both takes one SOA call. See Live configuration evidence for the probe.

What a successful run looks like

Dispatcher writes one log per component per task, under Dispatcher/Logs/Dispatcher/task/<taskId>/:

File Written by
<taskId>_dc.log Dispatcher Client
<taskId>_s.log Scheduler
<taskId>_m.log Module

That triplet maps directly onto the state machine, which makes bisecting a stall mechanical: open the log belonging to the component that owns the state you are stuck in.

A complete, successful module log:

Task = <id>, Status = Module started downloading files
Task = <id>, Status = Module completed downloading files
Task = <id>, Status = Started Translation!!!
Command string:
  <ModuleRoot>/Translators/<svc>\<svc>.bat -mode=process -qsmode=RunUpdater -input_uid_list=<staging>\<id>_<svc>.txt
Translator log messages are in: <staging>\<id>\result\SIEMENS_<svc>.log
Standard Error : Processing queue
Task = <id>, Status = The translator completed successfully
Task = <id>, Status = Module started uploading result files
Task = <id>, Status = Module completed uploading result files
Task = <id>, Status = Completed successfully!

Four things this confirms:

  • The Command string: line is the highest-value line in Dispatcher. It is the exact command with all options resolved. Copy it, run it by hand on the Module host, and you have isolated your translator from the whole framework.
  • The result log naming default is real: SIEMENS_<service>.log in the task's result directory, matching <provider>_<translator>.log as documented for the Translator.<p>.<s>.Log property.
  • Staging layout is <TC_ROOT>\staging\DC\<taskId>\ with a result\ subdirectory. Input files land in the task directory, output goes in result.
  • Standard Error : Processing queue is logged and the task still succeeded. Writing to stderr does not fail a task by itself. Only a match against <TransErrorStrings> or a non-zero exit does. Worth remembering before you spend a day silencing a chatty translator.

A multi-artifact direct translator, running

CapitalForward is the one built for this KB. It is a direct translatorOutputNeeded="false" in translator.xml, no .Load binding in Service.properties — because it produces several artifacts with different dataset types and relations and checks them in itself. Dispatcher's generic loader can load exactly one file, with one type and one relation, so binding it would have silently discarded everything except the main XML.

Verified output of one request:

capital.xml     1,094,386     93 components, 43 pathways, 5 functions, 10 signals
exchange.json      84,496     the neutral graph the audit stage consumes
coverage.json      96,009     per-port reconciliation
baseline.json      52,915     written back so the NEXT run has a diff target
rollup.json           215     the flat numbers a saved query turns into a report

rollup.json is kept deliberately tiny and flat so a tier adapter can map it straight onto item-revision properties:

{ "model": "040808", "coverage_run": "2026-08-03T18:24:07.381Z",
  "coverage_scope": "all", "coverage_subjects": 213, "coverage_blocking": 213,
  "coverage_regressions": null, "coverage_compared": false }

compared: false with regressions: null is the honest first-run answer. No prior baseline existed, so the tool reports that it could not compare rather than reporting "no changes" — which would have been indistinguishable from a clean run and is the failure mode every reporting stage should be designed against.

The baseline has to outlive the task

compared: false is not a bug on the first run, but it stays false forever unless something carries the baseline between runs. Every Dispatcher task gets a fresh staging directory, so an artifact written into result\ dies with the task. A translator that diffs against its own previous output needs an explicit store.

The cheap first deployment is a working copy on the Module host keyed by item id, with the Teamcenter datasets as the versioned archive rather than the working copy. A host rebuild then costs exactly one run of "could not compare" and nothing else. Promote the new baseline last, and only if it exists, so a failed run cannot poison the store and quietly invalidate the next comparison.

⚠⚠⚠ A gate that cried regression on a model nobody had touched

Wiring that store through immediately produced 55 regressions between two runs of an unchanged model. A non-zero delta is not evidence that a gate works, so this is worth reading before trusting your own:

The ledger keyed a cell by component + port + branch. A component name does not identify anything in an occurrence tree — the same part appears at many points in the structure — so 213 cells collapsed onto 110 distinct keys. The differ loads the baseline into a map and deletes each entry as it matches, so for every duplicated key the first cell found its prior and every one after it found nothing and was reported as newly appeared. Newly-appeared-and-blocking counts as a regression, so the gate produced dozens of false accusations on every run, forever.

The fix was to put the occurrence id in the key. Two things made it safe, and both were checked rather than assumed:

  • The occurrence paths are stable across exports — verified identical across two consecutive plmxml_export runs of the same revision, 86 ids and the same set both times. A key built on values that are regenerated per export would have produced exactly the same symptom.
  • A cell with no occurrence id falls back to the old key, so a baseline written before the fix still compares instead of reporting every cell as new once during the changeover.

The generalisable lesson is about diagnosis, not keys. Two plausible theories (unstable export ids, then a direction field present in one structure and not the other) were both wrong, and both were killed in about a minute by measuring instead of reasoning: diffing the two baselines directly showed zero keys differed, and grouping by direction showed all 213 values were empty. Read the differ rather than theorising about it, and never report a delta as a working gate before checking what the delta consists of.

Two design points worth stealing:

  • The gate fires last, after the evidence is already checked into Teamcenter, so a blocked request is actionable rather than merely failed.
  • Regression-blocking is opt-in (--fail-on-regression, off by default). Standing a gate up on a real program should not begin by failing every request over pre-existing debt.

⚠⚠⚠ Check the output against the SOURCE, not against your own intermediate

The most important thing on this page, and the one that cost the most to learn.

A translator that reports its own quality is grading its own homework. Ours had a reconciliation stage comparing the final artifact against the neutral bundle it had produced a step earlier. That stage passed, every time, while the translator silently discarded a third of the model.

What happened. Two modules were updated on the Module host to close a version skew. The pipeline stayed green. The artifact went from 1,094,386 bytes to 797,101, and the component count went from 93 to 50, because a deduplication step merged components on their name. The source model legitimately contained three Thrusters, three Heaters, three Thermistors, three Antennas, three Valves and three watchdogs. Two of every three were destroyed. Nothing errored.

Why the existing gate could not see it. It compared the final artifact against the intermediate bundle. Both are produced by the same transform, so anything the transform drops is missing from both sides of the comparison. The ledger went from 213 subjects to 110 and reported every one of them as blocking, exactly as it had before. A smaller number looked like a smaller model, not a broken one.

The check that works starts from the extract — what Teamcenter actually said — because the translator did not write it, and follows the counts forward one hop at a time:

  extract.json  -->  intermediate bundle  -->  final artifact
  (Teamcenter)       (your mapping)            (target tool)
  extract   block occurrences            93
            distinct occurrence path     93
            distinct name                69   <- NOT an identity
  bundle    components                   93
            reported as merged            0
  target    devices                      93

  PASS - every block occurrence Teamcenter reported reached the target.

Checking each hop separately is what makes it useful. "93 became 50" is a bug report. "93 survived into the bundle and 43 vanished at the emitter" names the file to open.

Four rules this produced

  1. Count identities, never names. The extract carried 93 occurrences at 93 distinct occurrence paths under only 69 names. A check that counted names would have agreed with the bug and reported everything fine. Print the gap between paths and names on every run, even when nothing is lost — it is the trap.
  2. "Explained" is not "correct". The deduplication step recorded exactly how many components it had merged, in a field that had been populated the whole time and that nothing ever read. Surface the number, and make the gate refuse a reduction whether or not the translator claims it was deliberate. A contract of "nothing may vanish" catches this; "nothing may vanish unexplained" does not, and that distinction is the entire failure.
  3. Separate "wrong" from "incomplete". Pre-existing gaps are debt and can be ratcheted. An artifact that lost content the model had is simply wrong. They deserve different switches, because a site may reasonably block on the second long before it can afford to block on the first.
  4. Legitimate reductions still get named. Some drops are correct — an endpoint that resolves to nothing is a documented skip. Report those too, just without failing. Silence is what let this ship.

The general form, for any translator: whatever your translator claims to have produced, count it in the thing Teamcenter handed you and count it again in the thing you handed the target tool. If those two numbers can drift without anybody being told, they eventually will.

Real-world translator.xml attributes

The shipped file uses almost none of the available attributes. A tuned production file uses many. Observed live:

<ToZipfile isactive="true" provider="SIEMENS" service="tozipfile"
           wrapperclass="com.teamcenter.tstk.translator.DefaultTranslator"
           maxlimit="1" MaximumProgress="100" NoOfTries="1"
           ExclExitVal="1" OutputNeeded="true"
           WaitTimeBetTrans="0" WaitTimeForReTries="0">
Attribute Effect
maxlimit Concurrent instances of this translator on this Module
MaximumProgress Per-translator override of the watchdog ceiling
NoOfTries Retry count
WaitTimeForReTries Delay between retries
WaitTimeBetTrans Delay between successive translations
ExclExitVal Exit value to treat as success rather than failure
OutputNeeded Whether an output file is required to declare success

ExclExitVal is the useful discovery: it is the clean way to handle a tool that returns a non-zero code you know is benign, rather than wrapping it in a script that swallows the code.

Also note wrapperclass="com.teamcenter.tstk.translator.DefaultTranslator" — naming the base class directly, with no subclass, purely to get the default wrapper behavior.

Direct translators carry OutputNeeded="false" consistently (nxtopvdirect, nxtransdirect, nxtocgmdirect, copyugrelstatus, jttobboxandtso, qsearchprocessqueue), because they write into Teamcenter themselves and produce no file for Dispatcher to map.

Two operational cautions

"Installed" is not "running". On this host all three Dispatcher services exist with StartMode: Auto and State: Stopped. A service list proves deployment; only -ping or fresh log timestamps prove operation.

Config files can be individually unreadable. translator.xml on this host denies read to a normal user while every sibling file in the same directory, including its own .bak, reads fine under a folder ACL granting Everyone read. Expect to need elevation for spot files, and check for a .bak before assuming you are blocked: backups left by integration installers are frequently readable and nearly identical.

Source: Live Dispatcher host, read 2026-08-03 · retrieved 2026-08-03