MADEKnowledge

MADE Knowledge Base > Automation

Running MADe Headless: Automating the Model Without the GUI

MADe 3.9.1 ships no headless mode. There is no command line switch, no scripting console, and no batch runner. Everything the product does, it does through its window.

It can still be automated, because of what MADe is built on. This article records what was proven by doing it, and separates that from what merely looks possible.

Everything below was exercised against MADe 3.9.1 on Windows in September 2026.

What MADe actually provides

Checked by reading the installation rather than by assumption:

  • MADE.ini contains no -application argument.
  • Across every plugin.xml in <install>/plugins, the only application MADe registers is com.phm.made.application, which points at the GUI class. Every other application id belongs to Eclipse itself.
  • com.phm.made.reporting.script sounds like a scripting engine and is not. It contains 18 classes of BIRT report helpers.

What MADe is built on is a standard Eclipse Equinox OSGi container. -application is an Equinox launcher argument, not a vendor feature, which means a new application can be added alongside the GUI one without modifying the product.

The approach

Add a small OSGi bundle that declares its own application, register it by editing a copy of the installation's configuration, and launch the Equinox launcher directly. The vendor installation is never modified.

"<install>\jre\bin\java.exe" -jar "<install>\plugins\org.eclipse.equinox.launcher_*.jar" ^
  -application <your.application.id> ^
  -configuration <copy of configuration> -data <workspace> -consoleLog -nosplash

A useful first step, before writing any code: launch with a deliberately wrong -application id. The framework starts, lists every application it knows about, and exits. That confirms MADe's runtime starts perfectly well with no window.

Licensing is not an obstacle. The com.phm.made bundle activates in about 6.5 seconds with no error and no licence complaint.

What works

Proven, with measurements:

Capability Result
Start the MADe runtime with no window Yes
Open a .made project Yes, 0.6 to 1.2 seconds
Read the item tree Yes
Read reliability and criticality per item Failure rate, MTTR, distribution, LRU, criticality flags
Change a value and persist it Yes, verified by reopening and by comparing the archive
Run MADe's own API export operation Yes, 10 categories including project and RBD, against a live endpoint. Deterministic, but not atomic
Run MADe's own API import operation Yes, applied and persisted, with one trap

Five things that will cost you a day each

0. Start MADe's API services before you open anything, or the repair fails silently

This one is first because it is the cheapest to get wrong and the hardest to attribute. Call MADe's startAllServices once per JVM, before the first project open. Skip it and the parent-link repair described later throws, and nothing tells you:

services started services not started
reparentComponents ok NullPointerException on a null FunctionNode parent
parents restored 7 of 7 1 or 2 of 7
connections after restore 141 54
what the restore call reports ok still ok, HTTP 200

With parents unrestored, a hierarchy walk finds nothing and reads as an empty model rather than an error, so analyses and imports come back cleanly wrong.

The failure information was present the whole time, in a field named after the step that failed. It still read as a healthy restore, because the response is a bag of fields and the caller chooses which to look at. That is worth stating plainly: a report you can accidentally not read is the same defect as one that lies.

⚠ And the reasoning trap that made it expensive. A session saw the degraded numbers, moved to a fresh JVM, a fresh configuration and a fresh workspace, saw them again, and concluded the project file was damaged. It was not. A fresh runtime is less initialised, not more. When a symptom survives a clean restart, suspect missing initialisation before you suspect the data. A pristine copy of the same file failed identically, which was the disproof available all along.

1. MADe exports almost nothing, so normal OSGi wiring does not reach it

com.phm.made exports 5 packages, none of the useful ones. com.phm.made.api exports none at all. Require-Bundle and Import-Package cannot see ProjectManager or MADeProjectService.

Use Bundle.loadClass, which resolves against the bundle's own class loader and is not restricted by Export-Package.

2. "The project is no longer compatible with this version of MADE"

Opening any project fails with IncompatibleProjectTypeException and a message telling you to convert the project to an archive, on a file that is already a perfectly good archive.

The message names the wrong cause. MADe stores projects as TrueVFS archives, and the .made extension is bound to an archive driver in exactly one place: the constructor of TrueZIPIOAccessManager. In the GUI that happens on the way in. Construct one before opening a project, on the same thread, because TrueVFS configuration is per thread.

3. Headless means no window, not no display

ProjectManager.doOpenProject() finishes by calling updateShellText(), which is a Display.syncExec. There is no flag to skip it, and the same call appears in close, new project and save-as. With no event loop, the call never returns and your automation hangs with no error at all.

The fix is not to remove the UI, it is to give MADe the contract it expects: create an SWT Display on the main thread, pump readAndDispatch, and run every MADe call on that thread. No window is ever created.

Once a display is pumping, the same call throws IllegalStateException: Workbench has not been created yet after the work has already succeeded. Treat that as a cosmetic tail, not as the outcome: ask getCurrentProjectOptional() what actually happened.

4. A save can report success and write nothing

Setting a value and saving is not enough. Four things are required, and missing any one produces a clean success with an unchanged file:

  1. set the value,
  2. call setDirty(true) on the element, because a setter does not mark it,
  3. write the element through ServiceManager.getIOAccessManager().saveElement(element, path),
  4. call syncProject() and then close the project, which is what rewrites the archive. That rewrite is asynchronous: see the section on closing below before your process exits.

The path in step 3 cannot simply be read off the element. getRelativeFilePath() is built from getFolderPath(), which walks up through getParent(), and headless every child component reports a null parent. The path collapses to the archive root and the element is written to the wrong entry. Derive it from the system that owns the component instead.

Also worth knowing: MADe's own project save drops externalApplicationData.dat, the record of which external application the project came from. If your project is linked to Teamcenter or another system, prefer writing the individual elements.

5. Loading the taxonomy is a separate step

The GUI splash screen says "Loading Taxonomy" for a reason. Nothing in bundle activation does it, and ServiceManager.getTaxonomyManager().load() has to be called explicitly. It takes about a second.

Skip it and most things still work, which is what makes this one dangerous. Items, RAMS and criticality all export perfectly, and functions alone fail, because a function resolves its own name through the taxonomy. The failure surfaces only as "an unknown internal error occurred in MADE".

Diagnosing "an unknown internal error occurred in MADE"

That message is what MADe shows for almost any internal failure, in the GUI as well as headless, and it never names a cause.

The cause is written to the Eclipse workspace log:

%USERPROFILE%\MADE\<workspace>\.metadata\.log

Every failure met while doing this work was named precisely in that file, usually as a NullPointerException with the exact class and method. If a MADe wizard fails and the dialog tells you nothing, read that log before anything else.

Driving the API integration

MADe's Integration menu connects out to an API endpoint that a system integrator implements. Those operations can be run from code, which means an unattended process can push a model out of MADe on a schedule.

An export is assembled as a list of per-category jobs wrapped in an operation:

  • a resource export job takes (host address, API version, request method, filter, input object),
  • RequestMethod.PUT is the wizard's Snapshot and PATCH is its Delta,
  • the operation is an Eclipse ICoreRunnable, so running it is a single run(monitor) call.

Two details that will bite: the jobs take the host address while the operation takes the host name, both typed String, so a mix-up compiles and then fails inside the HTTP client; and ApiHostsManager.readHosts() must be called first or a configured host will not be found.

A run of items, RAMS, criticality and functions completed in 1.6 seconds and delivered four payloads to the endpoint. Ten categories in one operation took 3.4 seconds.

What the export does and does not guarantee

The export was re-run ten times against a controlled endpoint, checking what the endpoint received rather than what the operation reported. Four things are worth carrying.

It is deterministic. Two exports of an unchanged model differ by exactly one line, the date stamp. If you compare payloads, filter that line out and compare content. Do not compare raw byte counts: the timestamp drops trailing zeros, so an unchanged payload can measure a byte shorter.

Its status is trustworthy about delivery, which is unusual here. A refused port and an endpoint returning HTTP 500 both produce Failed with the cause named, and an unknown host name refuses to run at all. Unlike the import's "approved", this status really does reach its failure value.

But success does not mean everything you asked for was sent. A category whose job cannot be built is skipped, and the operation still reports success. Ask for eight categories, get one, and the status still says Success. Count what was built against what you requested; do not read the status alone.

An export is not atomic. Categories are sent one at a time and the run stops at the first failure, so a failed export can leave the endpoint holding several updated categories and one stale one. Nothing is rolled back. The send order is also not the order you asked for, so you cannot predict which categories made it. MADe keeps no per-job result to consult, either: the job's own send method returns nothing. After a failed export, the endpoint is the only place that knows what landed, so ask it.

Delta does not send a smaller payload

This one is easy to assume and wrong. PUT and PATCH produce payloads that are identical line for line once the timestamp is excluded. RequestMethod chooses the HTTP verb and nothing else: MADe computes no diff on the client. "Delta" describes the merge behaviour the integrator is expected to implement at their end, not a reduced request. Sizing an endpoint on the assumption that PATCH is cheap would be a mistake.

There are more export jobs than the wizard shows

The wizard offers eight categories. The jar carries fifteen job classes, and they do not share a constructor: the project job takes a metadata object instead of the system, and the analysis jobs drop the request method entirely and move the filter. Code that fills constructor arguments by position works for the eight and fails on all of the others; match the parameters by type instead.

Two of the extra seven are reachable with nothing more than an open project. The project job exports project metadata, keyed by the project's own UUID rather than the Teamcenter item id. The RBD analysis job takes the assembly the project already provides, and delivers, though its payload carries an empty result set until an RBD analysis has actually been run.

Exporting an analysis does not require running one

This is worth separating, because "the analysis exports need an analysis to have been run" sounds obvious and is only half true.

A fault tree export needs a descriptor, not a calculation, and descriptors already sit in the project: the system carries a fault tree folder, the folder carries the descriptors, and each one loads its analysis on demand. Resolve it and export what the project stores. Measured on MADe's shipped Quadcopter Demo, that produces a 44 KB payload carrying 56 event probabilities faithful to the model.

The diagnostic export needs only the system, so it always builds, but MADe declines the run with "No data available to send" when the project has no sensor sets. That is the right behaviour: it refuses rather than shipping an empty payload.

FMECA and reliability allocation genuinely do need their own computed objects, and no project examined here had one.

Running an analysis headless completes, and computes nonsense

The fault tree calculation can be invoked with no GUI. MADe ships two factories for the job, and only one takes no window: the one the reporting engine uses. The project's own command stack is unreachable headless, but a plain command stack works in its place. The job then runs in about a tenth of a second and returns a status of OK.

The status is worthless. Sampling the same analysis before and after the run:

before   32 events, 7 of them at probability 1.0, top event 1.8498e-4
after    32 events, 32 of them at probability 1.0, top event 1.0

Corrected again: the result is attached, and the recommended fix is the cause

This section has now been wrong twice, and the second version was refuted by measurement rather than by re-reading.

The probability accessor on a fault tree event resolves like this:

getFaultTreeAnalysis().getCutSetResult()
    .flatMap(result -> result.getProbabilityOfFailureFor(this))
    .orElse(1.0)

The previous version read that as "one is the fallback for an analysis with no cut-set result attached". That is false. The run samples the cut-set result on both sides and it is present both times, carrying twenty four minimal cut sets both times:

before   cutSetResult PRESENT, minimalCutSets 24
after    cutSetResult PRESENT, minimalCutSets 24

The result is attached throughout. What comes back empty is the inner optional: the per-event lookup yields nothing for any event, so the chain collapses to the fallback. That is a different failure at a different level, and it explains the surviving cut sets without needing a separate story. Nothing removed the structure. Only the per-event probabilities were replaced.

The factory that was named here as the likely fix is the thing that does this. The previous version said the calculation job has two factories, one taking a window handle and one not, and that the second had not yet been run. It has now been run. It is the factory behind the before and after above.

The cause is visible in the shipped bytecode. The probability map is built by a method taking the analysis, a reliability result store and a multimap. The engine that calls it supplies both data arguments from lazy accessors that create them empty and cache them, and the only write to either field sits inside its own accessor, so no caller can populate them. The engine is built from a record carrying nothing but the analysis. On this path both data inputs are always empty, every model-based lookup misses, and the fallback is the only value available.

That last link is derived rather than measured, and it cannot be measured through this entry point, because there is no writer to populate the store with. That is itself the finding. An experiment to populate a store externally and re-read the probability was planned and abandoned for this reason: it would have returned a clean negative for a reason unrelated to the question.

The working recipe: export a fault tree headless with its real numbers

This is the positive result, and it is the point of everything above. Proven on 10 September 2026, three times.

GET /start?services=true
GET /open?path=<model>.made
GET /restore
GET /api/export?host=<host>&method=POST&categories=fta

There is no calculation step, and adding one is what breaks it. The real probabilities are already in the stored analysis. Recalculating replaces them with the fallback and returns a status of OK.

Compared leaf by leaf against the reference a person computed in the graphical tool, the headless export differs in exactly one field: the export timestamp. Same size, same fifty six event probabilities, of which forty nine are real and seven are the legitimate intermediate ones, same range from 2.38e-06 to 1.85e-04, same twenty four cut sets.

Compare by walking the object tree rather than by hashing. Every export embeds its own timestamp, so a hash comparison reports a difference between two identical analyses and tells you nothing. This is the same trap as diffing a Teamcenter XML export, which stamps a date and time into every header.

The pipeline does not modify the model. The file was hashed after each of open, restore, read and export and was identical every time.

Process shutdown does rewrite the archive, and the size changes. That was tested rather than assumed: an export taken from the rewritten archive is still identical to the reference apart from its date, so the rewrite is benign. Do not use a byte hash as an integrity check across a restart, because a clean run changes the file.

Read the stored analysis instead of recalculating

On a clean open, twenty five of the thirty two events carry real probabilities and seven sit legitimately at one. Those values are readable headless without running anything. The route added for this reads the three per-event sources the engine consults, plus the current probability.

Resolve those accessors on the public event interface rather than on the object's runtime class. The runtime class is a non-public implementation, so a lookup against it resolves the method and the invocation then fails on access. A probe written that way reported every event as unreadable, which reads exactly like a model that holds no data.

Nothing reports a problem at any point, which is the part that matters for a pipeline.

The damage is to the session rather than to the file, and the distinction matters if you are building a pipeline.

What was measured: an export taken later in the same session ships the saturated numbers. It is a clean 34 KB payload, reported as a success at every stage, that happens to contain the number 1.0 eighty times. That is the real risk, and it is entirely silent.

What was not measured, despite being the obvious next fear: corruption of the project file. Closing after a calculation does rewrite the archive, but reopening it in a fresh process gives the correct values back, matching a copy that was never calculated at all. The saturated state does not survive a reload.

Until the cause is understood, export the stored analysis and do not recalculate it. If you do recalculate, compare the numbers before against after, because neither the job's status nor the export will tell you anything is wrong.

One caution on doing that comparison. An early version of this check searched the exported payload for the text of a probability, and the payload writes that number in a different notation than the model reports it, so the check found nothing and briefly suggested the file had been damaged when it had not. Compare parsed numbers, never their formatting.

Restore the model's hidden links after opening

This is the most useful thing in this article if you automate MADe at all, and it has nothing to do with analyses.

A MADe project is a set of Java-serialised objects, and a component's link back up to its parent is marked transient, which means it is deliberately not saved. So does the list of connections between components. The application rebuilds all of that after loading a project. Nothing does it when you drive the runtime yourself, so every component below the system comes back reporting that it has no parent at all.

MADe provides the repair as ordinary public methods, and calling them takes milliseconds:

MadeProjectUtil.reparentComponents(root);
MadeProjectUtil.restoreModeConnections(root);

Measured across four projects, that moves the count of items with a parent from 1 of 7 to 7 of 7, and from 1 of 14 to 14 of 14.

⚠ Call that pair exactly once per open project. The second method is not idempotent: it adds the same connections again every time it runs, and it reports success every time. Counting the connections on MADe's own demo model:

connections
fresh open 30
after the first restore 141
after a second restore 252, the same 111 added again
after a third 363

Those duplicates are in the model you are about to save, so a second call silently corrupts it. Restoring the links does not damage the file on its own, because the fields being restored were never written to it, but that is a narrower statement than it first appears and it does not make repeat invocation safe. Guard the call, or reopen the project instead of restoring twice.

⚠ And the order matters, silently. The connection restore does nothing at all unless the parent restore has run first. Called on its own it leaves the count at 30 and still reports success, because with every parent link null it cannot find the components whose connections it is meant to restore. It succeeds at doing nothing.

⚠ This is why a headless MADe can look completely healthy and still produce nothing. Exporting items, reliability data or failures reads flat lists and never asks a component who its parent is. Every analysis navigates the hierarchy, and a missing parent yields an empty answer rather than an error. If your reads work and your analyses are empty, check this first.

One connection type decides whether FMECA produces anything

The FMECA export itself works with no wizard and no window. What it delivers on this evidence is a correct, complete configuration, and an empty set of results.

The reason is exact. The analysis builds its propagation matrix from connections of one specific type, and no project examined here has a single connection of that type. That sounds like the models have no failure propagation wired at all, and that reading would be badly wrong: MADe's own Quadcopter demo carries 172 failure elements and 141 connections, under eleven other types, on the very same components. Only the one category the matrix reads is empty. It appears to be derived rather than stored, and whatever derives it does not run outside the application.

This was confirmed rather than assumed. Opening the same project in the application and running its response path analysis produces real results; running the identical analysis headless on the same file produces none. So the model supports it and the product supports it, and the limitation is in driving it without the application.

⚠ One check nearly produced the opposite answer. With nothing selected in the project tree, the analysis menu entries are greyed out, which reads exactly like "this project cannot do this". They enable as soon as the system is selected. A disabled menu item is not evidence that a capability is unsupported.

The cause, for anyone who wants to continue it: the connections of that type are not stored in the file at all. They are held in a field marked transient, whose accessor quietly creates a new empty one when it finds nothing, and they are built by a listener that reacts to connections being made rather than by anything that runs at load time. Loading a project fires no such events, so nothing builds them, and the empty result is indistinguishable from a model that has none. Six plausible ways to rebuild them were tried and none worked, so the honest next step is to watch the application do it under a debugger rather than guess a seventh.

⇒ The general lesson is worth more than the specific one. When a lookup keyed on one category returns nothing, count the other categories before concluding the data is absent. Here the difference between "this model is not wired" and "this model is wired 141 times under other names" was one extra query, and only the second is true.

For completeness, a model that genuinely has no propagation wiring does look different, and you want to be able to tell them apart: the Teamcenter-derived model here has 312 failure elements and zero connections of every type, because nothing in that generation path authors propagation.

Do not trust a readiness flag you did not verify

The FMECA parameters object has an isReady method. It checks the report configuration first and returns early when there is none, so with no report attached it answers yes on an object that has not been configured at all. It reported ready on a setup missing the end effect the analysis requires, and the resulting export was a clean success carrying nothing.

A flag named for readiness is not evidence of readiness. Set the fields the operation needs and check those.

Ask what the project actually contains first

An analysis folder that is empty and a project that never had an analysis are indistinguishable from the export side, since both produce a job that cannot be built. Check the container separately.

Nothing generated out of Teamcenter carries an analysis. Of three projects checked here only MADe's shipped Quadcopter Demo had one, which is also what makes the other two zeros trustworthy: the same probe returns a non-zero somewhere, so a zero means empty rather than unreadable.

Answer 200, never 202

Worth stating on its own because it costs a day and the error blames the wrong thing. MADe rejects any success code it does not recognise, 202 among them, with "Unknown API Response. MADE does not support this HTTP response code", and it retries the identical request once, so the integrator sees the payload twice and the operation still fails. The specification lists 200 beside 202 for every write, so answer 200 synchronously.

Apply it to every write route rather than the one that failed. This bug was fixed once on the resource writes and then found again months later on the analysis route, in the same file, because only the failing handler had been changed.

Pin the loopback address, do not write localhost

Check what else holds your port before assuming the address is cosmetic. On the machine this was built on, port 8080 had three listeners: the integration endpoint on 127.0.0.1, and Docker and a WSL relay on the IPv6 addresses. 127.0.0.1:8080 reached the endpoint and localhost:8080 did not reach anything. Configuring the host as localhost would have sent the model somewhere else entirely, and reported a transport error that named none of this.

Importing updates back into a project

The import direction works, and it has one trap sharp enough to deserve its own section.

Given data on the endpoint, MADe fetches it and computes a correct diff. What it does next is more than you might expect: run() also auto-sets each change's response, executes those as commands, and then performs its own commit walk. That walk locks every change, and MADe's staging model is:

  • a change is staged only if it is not locked and its response is not pending,
  • commit() does nothing unless the change is staged,
  • and there is no unlock.

The consequence is that after the operation has run, every change reports itself as approved while being unstaged, so committing the operation applies nothing and reports success. Approving the changes yourself is a no-op, because approving is itself gated on the lock.

If you are automating this, do not trust "approved". Check staged.

What actually applies an import is calling applyChangeToModel() on each change directly. That method is on the public change interface and is not gated. One more detail: applying a change does not mark the affected element dirty, so a save afterwards will skip it and the work is lost on close. Mark each affected element dirty yourself, then save and close.

Two further points worth knowing:

  • Items is mandatory in every import. The wizard always includes it. Importing RAMS alone produces an empty diff; importing items together with RAMS produces a full one.
  • Verified end to end: a failure rate staged on the endpoint as an MTTF of 12987.01 hours arrived in the model as 77.0 failures per million hours, survived a save, a close, a restart of the process and a reopen, with neighbouring components untouched and exactly one entry in the project archive changed.

Creating a new project by import is a separate path and has always worked; this is about applying an import as an update to items that already exist.

Closing a project is not synchronous with the file

This one is worth knowing whatever you use MADe for from code, because it has nothing to do with importing and it loses work silently.

Closing a project is what rewrites the .made archive, but the close call returns before the rewrite has happened. Measured: the call returns in 0.04 seconds, the file starts changing 2.25 seconds later, and settles about 5 seconds after the call returned.

If your process exits inside that window, the write is simply lost. It is not rolled back and no error is raised: the file is left exactly as it was, and the next time you open the project your changes are not there.

Two runs during this work looked like a broken import for exactly this reason. Isolating it took a 2x2: closing without stopping the process persisted, closing and stopping immediately lost the work, closing and waiting ten seconds before stopping persisted (so the stop does not undo a finished write), and closing then stopping then waiting thirty seconds still lost it (so it does not complete later).

The remedy is simple: after closing, wait until the project file stops changing before you exit. Watching the file size with a short settle period is enough.

A word on verification

Four separate times during this work, MADe reported success for an operation that had done nothing: a save that wrote no bytes, a "wrote" flag on an archive that had not changed, an import whose changes were locked and skipped while reporting themselves approved, and a diff measurement that reported zero because the reflective call had thrown.

None of them were visible in the response. All four were caught the same way: reopen the project in a fresh process, compare the entries of the .made archive before and after, or read the payload back off the endpoint.

The export is the one operation whose status survived this scrutiny, and it is worth being precise about why. Pointed at a refused port it reports Failed, and at an endpoint returning 500 it reports Failed, so the field genuinely reaches its failure value. That was established by running those cases deliberately, not by observing that it said Success when things were working. A status that has never been seen to fail is not evidence of anything.

Even then it is honest about only one question, whether the requests that were sent were accepted. It says nothing about a category that was silently skipped, and nothing about which categories landed before a mid-run failure.

If you automate MADe, check the artifact, not the return value. And when a status does look trustworthy, make it prove it: give it a case that must fail.

Retracted: "the import applies to a detached element"

An earlier version of this page said that applyChangeToModel() returns normally, reports success, and does not change the model, and it presented four controls in support. That claim is withdrawn. If you read it here before and acted on it, this section is the correction.

What was observed is not in doubt. Over several hours, imports against one model repeatedly reported success while the model did not change, and the save that followed reported nothing dirty and nothing written. The project archive still carries the evidence: 42 of that file's 47 .diff records are the small empty class MADe writes for an import that changed nothing.

What was wrong was the conclusion drawn from it, and the reason is worth more than the claim was.

The four controls varied the persistence window, the element, the field and the origin. They never varied the payload source. Every one of them ran through the same bridge and the same store lineage. So they were not four independent controls at all: they were one instrument, measured four times, and their agreement could not detect the failure they were there to rule out.

And the result does not reproduce. Each named variable was isolated and excluded:

variable how it was excluded
the headless build by inspection: the only intervening change touched response construction, not the apply path
the bridge's serve path by reconstruction: the failing run's own store survived as an automatic backup, and served through the exact earlier code it returned the staged value correctly
committing the operation direct test: the model moved without it
the restore state moved both restored and unrestored
a fresh process moved on a brand-new JVM, configuration and workspace

So: the failure was real, it is attested in the file, and it is not reproducible. No cause is claimed. That is the whole honest statement, and anything more confident than it is not supported.

⚠ One methodological note, because it cost a run. A reproduction attempt staged no difference at all: the model and the store already held the same value. It returned a large change count and a verdict of unchanged, and it meant nothing. A comparison with nothing staged is not a negative result, and it looks exactly like one. Check that your before and after actually differ before you read the outcome.

What survives is the diagnostic, which is still worth having: the count of what your code marked dirty and the count of what the project actually wrote are different numbers, and a disagreement between them is worth investigating.

Settled: it does reproduce, the target is not detached, and nothing lands

The functions category reproduces this on demand, and instrumenting it properly answers the part that was previously guesswork. Importing the functions category into a byte-verified clean baseline reports 506 changes, 506 applied, all returned normally, committed, nothing marked dirty, and changes zero bytes of any model file.

The detached-target explanation is wrong. Comparing object identity within a single process, because identity hashes do not survive a restart, the object the import wrote to is the model's own function, matching by both name and identity. What looked like detachment was a property of the diff node rather than of the target.

And nothing lands where the payload's substance actually lives. The connections belong on flow objects, not on functions:

before after 506 applied
functions 11 11
flows visited 34 34
external connections 0 0
internal connections 0 0
flows whose accessors failed 0 0

That last row is the one that makes the zero readable. Without it this is an unread zero rather than a measured one.

So the operation reports success, binds to the correct objects, and leaves the model unchanged in memory and on disk. Whether the caller invokes it wrongly or it does nothing is still unseparated.

Two earlier probes behind this were themselves broken, and both failed in the flattering direction. A functions-only probe cannot see the level where the changes must land, and it happened to report "nothing changed" correctly by accident. A second printed a null type for every function, because five of the seven accessors do not exist and a generic reflective helper returns null for a missing method, so a blind field rendered exactly like an empty one. Resolve accessors explicitly and render an absent one as absent.

The most expensive mistake: an unrun analysis looks exactly like an empty one

Exporting a reliability allocation without running it publishes a small record with an empty result set. That is indistinguishable from an analysis that ran and found nothing, and two people classified it independently, from different evidence, agreed with each other, and were both wrong about the cause.

Set the analysis target and run the job, and the same kind produces correct output: the record grows from an empty configuration to seven result rows. The arithmetic checks out on the published artifact, not merely on the status. Mean time between failures equals one million divided by the failure rate on every row, the system failure rate equals the sum of its children exactly, and each child's weighting equals its share of the total, summing to one.

Before calling any analysis kind empty, establish that it was actually run. Independent agreement confirms the observation and offers no protection at all to the inference drawn from it.

Two caveats on that result, because "it produces correct output" is not the same as "the output is usable". It ran against a default requirement of one hour, so the allocated figures are shares rather than engineering targets, and a motor with a measured mean time between failures of twenty-five thousand receives an allocated figure of about twelve. The allocated rate field also carries a unit of hours, which is a unit of time and not of a rate.

An export that fails with "no input of type X" means you never built X

This shape appeared twice, and there are fifteen export jobs, so expect it again. The category is mapped, the job class exists, and the export fails naming a missing input type. It reads as though the category were unsupported. It means the object the export wraps was never constructed.

The propagation table export takes the perturbation job rather than a propagation table, which means a table built through the registry cannot be exported at all. The reliability allocation export takes an allocation analysis, which nothing creates until you create one.

Guard the export in the expensive direction

Every other failure described on this page lets a failure look like a success. Two guards here do the opposite deliberately. The export refuses to publish a fault tree whose every event has collapsed to a probability of one unless the caller explicitly says to, and refuses an allocation analysis that reports no results, which the product otherwise surfaces as an unnamed internal error. A legitimate action rendering as a refusal until intent is stated is the right trade when the cheap direction is a question and the expensive direction is a bad number published silently.

If you build an endpoint over this, report the artifact and not the call

The most expensive habit in this work is reporting what the API call did instead of what the model did. One endpoint here reported applied: 0 while the model had changed, was corrected, and then reported applied: 1 while it had not, inside one day. Both versions counted whether the call threw. The question is whether the value moved.

Fingerprint the model before and after, and let the verdict come from the re-read. Keep call-level counters if they are useful, but do not let them be the answer.

One design idea worth stealing: emit counters that must sum. A response showing one change seen, zero attempted and zero ignored is arithmetically impossible, and that impossibility is what exposed a real bug no single number could have shown. A check the instrument runs against itself is worth more than any one value it reports.

Smaller traps, all measured

  • A service's own index is a claim, not a measurement. A status endpoint here listed 6 routes while 23 were live, and a reader filed "the build is out of date" as a blocker while every route they believed missing answered normally. Probe the route.
  • MADe dials a configured host. The address lives in <user>/MADe/environment/apihosts.properties and the caller does not choose it. That file is re-read per request, so it can be changed without a restart.
  • Importing one category can run another. A rams import also runs an items job, and a 404 there aborts the whole import. Stage both under the same resource id.
  • A record keyed to an element that does not exist imports as a success, with diff nodes, zero changes, nothing created and no error. Without a before-and-after on the model it files as a working import.
  • Closing rewrites the archive even when nothing was saved, adding diff records per import. Growth is expected. Verify by content, never by file size.

Source: Original research, Siemens Xcelerator workspace. Not a Siemens Support Center article. · retrieved 2026-09-09