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:
- A block in
Module/conf/translator.xmlwithisactive="true". - A launch script under
Module/Translators/<Service>/. Prepare/Loadbindings inDispatcherClient/conf/Service.properties, plus its name appended to that file'simport=list.- 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>.login the task'sresultdirectory, matching<provider>_<translator>.logas documented for theTranslator.<p>.<s>.Logproperty. - Staging layout is
<TC_ROOT>\staging\DC\<taskId>\with aresult\subdirectory. Input files land in the task directory, output goes inresult. Standard Error : Processing queueis 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 translator —
OutputNeeded="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_exportruns 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
- 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.
- "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.
- 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.
- 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