Skills
Cameo Headless Automation
Skill
cameo-headless-automation. Run scripts inside Cameo / MagicDraw without touching the GUI - a custom CommandLineAction plugin driven by CommandLineActionLauncher, or a persistent worker that serves HTTP so calls cost 0.4s instead of 40s. Covers the three things that make an unattended run fail while looking healthy - batch mode still needs a desktop, the FL_* licence properties suppress the FlexNet dialog in BATCH but not in GUI, and a batch process discards an unsaved edit while the create still reports success. Use for any headless/batch/unattended Cameo work, macro automation without clicking Tools > Macros, or a "the action ran and nothing happened" investigation.
Verified live 2026-08-12 on Cameo Enterprise Architecture 2024x Refresh3
(2024.3.0-55-260588bf), bundled Temurin JDK 17.0.14, Open API version 1.2,
FlexNet floating licence 1101@localhost, edition Enterprise. Vendor source:
the Open API Developer Guide shipped in the install at
<install>\openapi\guide\guide\ (extract openapi\guide.zip if absent) and the
worked example at <install>\openapi\examples\commandlineplugin.
Working implementation: cameo-mcp in this workspace (plugin + MCP server + tests).
The mechanism
Cameo runs a batch program through com.nomagic.magicdraw.commandline.CommandLineActionLauncher.
Which code it runs is chosen by a system property naming a class that a loaded plugin registered:
-Dcom.nomagic.magicdraw.commandline.action=<your.FQCN>
com.nomagic.magicdraw.commandline.CommandLineActionLauncher arg1=v1 arg2=v2
Your plugin registers it in init():
CommandLineActionManager.getInstance().addAction(new MyAction());
Two base types, both verified present on this build with javap:
| Type | Use |
|---|---|
CommandLineAction (interface, byte execute(String[])) |
you manage projects yourself, or need none |
ProjectCommandLineAction (abstract, byte execute(String[], Properties, Project)) |
the launcher opens the project(s) and calls you once per project |
ProjectCommandLineAction is the one to reach for: it gives you project=,
server=, username=, password=, encryptPassword=, serverType=,
enableSSL=, projectPassword=, version=, branch=, properties= for free,
so the same action reaches a Teamwork Cloud or 3DEXPERIENCE project and not just
a local .mdzip. Passwords are expected encrypted; generate one by running
any launcher with the single argument generateServerPassword=yourPassword.
Make the action generic once. Rather than a plugin per job, write one action
that evaluates a script file and stop recompiling: the Automaton plugin puts
Jython 2.7.4, Groovy 4, Rhino and BeanShell on the shared plugin classloader, so
new ScriptEngineManager(getClass().getClassLoader()).getEngineByName("python")
resolves — provided your plugin.xml declares
<required-plugin id="AutomatonPlugin" name="Automaton"/> so load order is right.
Verified engine list from a live instance: jython 2.7.4 [python, jython],
Groovy Scripting Engine 2.0, JavaScript Rhino 1.7.13, BeanShell Engine 2.1.7.
Classpath and launch
Compile with the tool's own bundled JDK (<install>\jre\bin\javac.exe)
against <install>\lib\* plus every subdirectory of lib — the Developer Guide's
Compilation classpath page, with brand.jar / brand_api.jar first when they
exist. Then the bytecode level cannot drift from the JVM that loads it.
At run time the app's own classpath is one manifest jar:
-cp <install>\lib\classpath.jar
-Desi.system.config=<install>\data\application.conf (mandatory)
@<install>\bin\vm.options (the --add-opens set)
-Dfile.encoding=UTF-8
Deploying without installing: -Dmd.plugins.dir="<install>\plugins;<your dir>"
adds an extra plugins directory (each holding <plugin-id>\{plugin.xml,*.jar}).
This is how you test against an install you do not want to modify. Plugins also
load from <config location>\plugins, but that is not equivalent for batch:
the interactive launcher sets -DLOCALCONFIG, a bare java ... Launcher command
line does not, so a plugin living only there loads in the GUI and silently does
not load in batch.
The three things that make it fail while looking fine
1. Batch mode is NOT headless
"The application cannot be run on the headless device even in a batch mode. The graphical environment is required." — Developer Guide, Running programs in batch mode
It runs with no window and no user, but it needs a logged-in desktop session. It will not run as a Windows service, nor over an RDP session that was disconnected rather than left open. Do not promise unattended-server operation. (Whether it survives a locked workstation is untested here.)
2. The FL_* licence properties work in BATCH but NOT in GUI
-DFL_FORCE_USAGE=true -DFL_SERVER_ADDRESS=localhost -DFL_SERVER_PORT=1101 -DFL_EDITION=Enterprise
EXERCISED both ways in one sitting: under CommandLineActionLauncher the licence
is acquired silently, zero dialogs. An interactive com.nomagic.magicdraw.Main
start with the identical properties sat on a License Server Connection
dialog and never finished loading — the plugin descriptor was parsed and init()
never ran. In an unattended run that is indistinguishable from a hang.
⇒ You cannot start an interactive Cameo unattended this way. Get the real server
from Creating FlexNet Client: <port>@<host> in %LOCALAPPDATA%\.cameoea\2024x\cea.log.
The Siemens licence server on 29000 is a different product and a documented red
herring. Interactive starts also raise two further modals (Select Edition, Merge
Plugin "not available"), which get misdiagnosed as "won't license on relaunch".
3. A batch process discards an unsaved edit, and the create still says ok
Each one-shot run is its own process. Create an element, return success, exit —
and the change never existed. Caught for real: a package created and
verifiedByReread: true was simply gone in the next run.
⇒ Save inside the same run, and assert on the artifact:
before = os.path.getmtime(src)
saved = pm.saveProject(ProjectDescriptorsFactory.getDescriptorForProject(p), True)
after = os.path.getmtime(src)
persisted = bool(saved) and after > before # saveProject can return true and change nothing
The session must be closed before the save — a save inside an open edit
session is not valid. If a Java wrapper opened the session for you, close it from
the script and make the wrapper's own close conditional on isSessionCreated.
The worker: pay startup once
A one-shot batch call costs ~15 s of startup and 35-48 s wall. Register a third
action that starts a loopback HTTP server and blocks, and the same work costs
0.4-0.8 s per call (measured: project_info 0.75 s, diagrams 0.44 s,
find 0.38 s, against a 51 s one-time start). The project stays open between
calls, so element ids stay live and nothing is re-saved and re-opened between two
edits.
This is also the only unattended route to the live HTTP surface, precisely because of the GUI licence dialog above.
Build it with com.sun.net.httpserver (in the JDK, no extra jar to conflict on
the shared classloader), bound to 127.0.0.1, gated on a per-launch random token
written to a handshake file in the config directory. Put parameters in the query
string and the script in the raw body — then you need a JSON writer and no parser.
Non-obvious requirements, each of which is a real failure otherwise:
- Serialize requests.
Executors.newSingleThreadExecutor(). The UML model is thread safe for reading only; writing is not. - Marshal to the AWT event thread. "The majority of the application code (including OpenAPI) is not thread safe" — Developer Guide, Multi-threading. An HTTP handler thread is not the EDT.
- Use
invokeLater+FutureTask.get(timeout), neverinvokeAndWait.invokeAndWaitcannot be interrupted, so one modal dialog hangs the bridge forever. Time out the wait and return a distincttimeouterror kind — the EDT may still be stuck, and saying so is the honest answer. - Expire the worker. Idle timeout and total lifetime. It holds a floating licence seat and an open project until it stops.
- Remove the handshake file on shutdown. A stale one pointing at a dead port makes "not running" look like "running but broken".
- Refuse
/shutdownfrom an interactive session — that would close an application a person has open.
Session and verification discipline
if not SessionManager.getInstance().isSessionCreated(project):
SessionManager.getInstance().createSession(project, "My edit")
# ... edits ...
SessionManager.getInstance().closeSession(project) # cancelSession(project) on error
createSession throws IllegalStateException if one is already open, and only
one can be active. Cancel on exception or a raise leaves a dangling session —
and verify the cancel with a control: a script that edits the model and then
raises should leave zero survivors on a follow-up search. That control passed
here; without it, a half-applied edit is indistinguishable from a clean run.
Two verification traps found the same night:
- Tagged values are coerced to the tag's declared type. Writing
"true"reads back"True". Comparing raw strings makes a successful write look like a failure — and since a failure cancels the session, the over-strict check was itself throwing away good writes. Normalise before comparing. - Sanitised filenames collide. Exporting diagrams by name reported 8 written with 7 on disk: two diagrams shared a name and one silently overwrote the other. De-duplicate, and report distinct files found on disk next to the count.
Errors that name the wrong thing
| Symptom | Real cause |
|---|---|
Commandline action <FQCN> not found. |
the plugin did not load. Names the class, never the missing plugin directory. Check -Dmd.plugins.dir, and that plugin.xml + jar are in a <plugins-dir>\<id>\ folder. |
error: Invalid filename: <BOM>C:\... from javac |
a PowerShell 5.1 Set-Content -Encoding utf8 argfile. Use [System.IO.File]::WriteAllLines with UTF8Encoding($false). |
Could not find or load main class Enterprise |
Start-Process -ArgumentList joined an unquoted -D...=D:\Cameo Enterprise Architecture\... on its space. Quote each member, or use an argv list (Python subprocess handles it). |
plugin descriptor logged, init() never runs, no error |
a startup modal is blocking. Check the process's MainWindowTitle. |
| a run that "worked" but the model is unchanged | no save, or a save inside an open session. See §3. |
Pure ASCII, when it applies
Jython source that must also run as a registered GUI macro has to be pure
ASCII with no encoding declaration: non-ASCII fails under execfile
("Non-ASCII character ... but no encoding declared"), and adding
# -*- coding: utf-8 -*- then fails the macro engine with
org.python.antlr.ParseException: encoding declaration in Unicode string. No
single file satisfies both paths. A script delivered over HTTP or read as UTF-8
by your own action is not subject to this — but keeping to ASCII costs nothing
and keeps a script runnable either way.
Generated from skills/cameo-headless-automation/SKILL.md in the tc-automation-skills library, which is the canonical copy and also serves as the agent skill set for Teamcenter work.