Skip to content

Build a managed library

This guide is a worked, end-to-end tutorial for building a managed library: a Teamcenter server-side shared library whose registration machinery tcli generates and keeps wired up for you. We start from an empty, initialized project and grow one library, libsample, through every extension kind, a runtime property, an action handler, a rule handler, an operation extension, and a common module. Then add an Active Workspace kit alongside it.

For the exhaustive flag list, see the add reference.

  • An initialized project (tcli init --tc-version <release>), so a .tcli.yaml manifest exists in the working directory. See the quick start.
  • tcli on your PATH. Every command below is run from the project root.

A simple library (--simple) is just a CMake target you fill in yourself. A managed library additionally ships the registration plumbing Teamcenter customisations need, and marks itself manageable: true in the manifest so tcli can grow it later with add extension.

Concretely, a managed library ships:

  • A single registration entry point. register.cpp exposes <lib>_register_callbacks(), which registers a USER_gs_shell_init_module exit that in turn calls <lib>_register_customisations(). Every extension you add is registered from there.
  • Reusable registration helpers (register_tools.cpp/.hpp). Thin, exception-safe wrappers such as reg::ActionHandler, reg::RuleHandler, and reg::RuntimePropString/Int/Double/Date/Tag (and their ...s array overloads). A failed registration is logged to the Teamcenter syslog instead of crashing the server; the library still loads.
  • A CMake build wired for Teamcenter (tc_target_configure_library, tc_target_link_library, export macros in exports.hpp, per-kind source lists).
  • Empty, ready-to-grow subdirectories for each extension kind.
Terminal window
tcli add library --name libsample --iplib none
  • --name must start with the lowercase prefix lib (e.g. libsample).
  • --iplib selects the tc_configure_library registration mode (none, libuser_exits, or libserver_exits); it defaults to none and is ignored for non-manageable libraries.

This appends libsample to the manifest and writes the tree under customisation/tc/server/.

The library itself lives under customisation/tc/server/libsample/:

customisation/tc/server/libsample/
├── CMakeLists.txt # builds the libsample shared target
├── README.md
└── libsample/
├── constants.hpp # kLibraryName, custom error codes
├── exports.hpp # LIBSAMPLE_API import/export macro
├── register.hpp
├── register.cpp # the single registration entry point
├── register_tools.hpp
├── register_tools.cpp # reg:: registration helpers
├── syslog.hpp # syslog helper functions
├── action_handlers/ # CMakeLists.txt + action_handlers.hpp
├── rule_handlers/ # CMakeLists.txt + rule_handlers.hpp
├── runtime_props/ # CMakeLists.txt + runtime_props.hpp
├── op_extensions/ # CMakeLists.txt + op_extensions.hpp
└── modules/ # CMakeLists.txt

The first library or tool scaffolded into a project also creates the shared build scaffolding at the customisation/tc/server/ root, and registers the new library in it:

customisation/tc/server/
├── CMakeLists.txt # customisation root; add_subdirectory(libsample)
├── CMakePresets.json # Debug and release presets for ninja using MSVC
├── .clang-format # Formatting configuration
├── .clangd # Clangd LSP configuration
└── cmake/ # shipped Tc.cmake helper modules

Understanding the wiring makes the rest of the guide obvious. Files contain marker regions, matched // tcli:begin:<id> / // tcli:end:<id> pairs, that tcli patches when you add an extension:

  • register.cpp has one region per registerable kind, e.g. register:action_handler, register:rule_handler, register:runtime_prop.
  • The shared header in each subdirectory (action_handlers.hpp, rule_handlers.hpp, runtime_props.hpp, op_extensions.hpp) has a declarations region, and each subdirectory CMakeLists.txt has a sources region.

When you run add extension, tcli renders the new source file, inserts its declaration into the shared header, adds it to the CMake source list, and (for kinds that self-register) inserts the reg:: call into register.cpp. Every change is applied in a single transactional plan. It either all lands or nothing does, and it refuses to overwrite files that already exist.

You never edit the marker regions by hand; you edit the generated implementation files, which is where each section below points you.

A runtime property computes a business object property value on the fly.

Terminal window
tcli add extension rtp --library libsample --name my4_custom_prop \
--type-name ItemRevision --prop-name my4CustomProp \
--value-type string

For a multi-valued property, add --array (this selects the ...s registration overload, e.g. RuntimePropStrings). Supported --value-type values are string, int, double, date, and tag.

Files created / patched (under customisation/tc/server/libsample/libsample/):

  • + runtime_props/my4_custom_prop.cpp — the implementation.

  • ~ runtime_props/runtime_props.hpp — declaration inserted.

  • ~ runtime_props/CMakeLists.txt — source added.

  • ~ register.cpp — registration inserted into the register:runtime_prop region:

    libsample::reg::RuntimePropString("ItemRevision", "my4CustomProp", my4_custom_prop);

Where to implement: open runtime_props/my4_custom_prop.cpp and replace the // Implementation goes here body of my4_custom_prop_impl(METHOD_message_t* msg, va_list args).

An EPM action handler runs custom logic at a workflow action.

Terminal window
tcli add extension ah --library libsample \
--name my_release_targets \
--handler-name MY-release-targets
  • --name is the C++ function/file identifier (snake_case).
  • --handler-name is the EPM handler name registered with Teamcenter and referenced by workflows, an uppercase-prefixed, hyphenated name such as MY-release-targets.

Files created / patched:

  • + action_handlers/my_release_targets.cpp — the implementation.

  • ~ action_handlers/action_handlers.hpp — declaration inserted.

  • ~ action_handlers/CMakeLists.txt — source added.

  • ~ register.cpp — registration inserted into the register:action_handler region:

    libsample::reg::ActionHandler("MY-release-targets", "MY-release-targets", emx_release_targets);
  • + configuration/aw_workflow_handlers/MY-release-targets.json — the Active Workspace workflow-handler definition, so the handler is selectable in the AW workflow designer. Deploy it with tcli deploy aw_workflow_handlers.

Where to implement: edit the // Implementation goes here body of my_release_targets_impl(EPM_action_message_t msg).

An EPM rule handler returns a decision that gates a workflow transition. It is the same shape as an action handler, differing only in the registered helper and the EPM_decision_t return type.

Terminal window
tcli add extension rh --library libsample \
--name my_check_maturity \
--handler-name MY-check-maturity

Files created / patched:

  • + rule_handlers/my_check_maturity.cpp — the implementation.

  • ~ rule_handlers/rule_handlers.hpp — declaration inserted.

  • ~ rule_handlers/CMakeLists.txt — source added.

  • ~ register.cpp — registration inserted into the register:rule_handler region:

    libsample::reg::RuleHandler("MY-check-maturity", "MY-check-maturity", emx_check_maturity);
  • + configuration/aw_workflow_handlers/MY-check-maturity.json — the AW workflow-handler definition.

Where to implement: edit my_check_maturity_impl(EPM_rule_message_t msg) and return an EPM_decision_t (e.g. EPM_go / EPM_nogo).

An operation extension customises a business object or property operation via a pre-condition, pre-action or post-action.

Terminal window
tcli add extension boext --library libsample --name my_bo_extenstion \
--type-name ItemRevision --method-name fnd0Save \
--ext-type post-action --framework meta

Files created / patched:

  • + op_extensions/my_bo_extension.cpp — the implementation.
  • ~ op_extensions/op_extensions.hpp — declaration inserted.
  • ~ op_extensions/CMakeLists.txt — source added.

There is no register.cpp change: BMIDE wires the operation to your function.

Where to implement: edit the body of my_bo_extension_impl().

A common module is a self-contained helper (shared utilities, a small class) that other extensions in the library can include. It is not registered with Teamcenter and has no shared header to patch.

Terminal window
tcli add extension module --library libsample \
--name string_utils

Files created / patched:

  • + modules/string_utils.hpp — the module header (fill in your declarations).
  • + modules/string_utils.cpp — the module implementation.
  • ~ modules/CMakeLists.txt — source added.

Where to implement: put your declarations in string_utils.hpp and their definitions in string_utils.cpp, both inside the libsample namespace. Include it from other extensions with #include "../modules/string_utils.hpp".

Server extensions are only half of most customisations. An AW kit holds the client-side Active Workspace contributions and lives in a separate root.

Terminal window
tcli add awkit --name customModule --aw-version 7.0.0
  • --name must be camelCase (e.g. customModule).
  • --aw-version is required and must match your Active Workspace release.

This creates the AW customisation root and the module sources:

customisation/tc/aw/
├── awbuild.cmd # AW build script for your release
└── stage/src/customModule/
├── kit.json
└── module.json

The shared CMake presets are generated at customisation/tc/server/. Configure and build the server customisations from there with your Teamcenter-configured toolchain, for example:

Terminal window
cd customisation/tc/server
cmake --preset debug
cmake --build --preset debug
cmake --install build/debug

Build the AW kit with the generated customisation/tc/aw/awbuild.cmd.

Finally register the library in the TC_customization_libraries site preference.