Repository navigation
Server Types and Instantiation
Milo 1.2.0-RC1, Java 17. API baseline 69cb25087804.
The tutorial server builds Thermostat one node at a time. That works for one object, but a server with many objects of the same kind, or one that exposes a standard information model, creates each object from a type instead. The type declares the members once, and Milo copies them into a new instance with the identities you choose.
The alarm builder in Alarms and Conditions already used this engine to instantiate the alarm type. Here you call it directly, from a managed namespace like the one in Address space.
A type is an ObjectType or VariableType plus its declaration hierarchy. Supertypes contribute inherited declarations, and HasModellingRule marks each member as Mandatory, Optional, or a placeholder. Load or construct the type and its declarations first. This API does not import NodeSet2 XML, and a generated Java node class represents model attributes and references, not an application driver.
An instantiation request names the type, the root's identity and parent, the node manager that stores the result, and any choices about optional members and NodeIds. Milo compiles the type into a structural model, plans every node and reference the instance needs, and then stores them. The result records exactly what it created, so you also use it to delete the instance later.
Obtain NodeInstantiator from server.getNodeInstantiator(). This fragment creates a FolderType Object named Devices under Objects, where a server with many thermostats could group them. nodeManager belongs to a started managed namespace, and folderId and folderName use that namespace's registered index. In the tutorial namespace those would come from getNodeManager(), newNodeId("Devices"), and newQualifiedName("Devices"). Imports:
-
InstantiationRequestandInstantiationResultfromorg.eclipse.milo.opcua.sdk.server.nodes.instantiation -
UaObjectNodefromorg.eclipse.milo.opcua.sdk.server.nodes -
NodeIdsfromorg.eclipse.milo.opcua.stack.core -
LocalizedTextfromorg.eclipse.milo.opcua.stack.core.types.builtin
InstantiationRequest<UaObjectNode> request =
InstantiationRequest.of(UaObjectNode.class, NodeIds.FolderType)
.nodeId(folderId)
.browseName(folderName)
.displayName(LocalizedText.english("Devices"))
.parent(NodeIds.ObjectsFolder, NodeIds.Organizes)
.target(nodeManager)
.build();
InstantiationResult<UaObjectNode> result = server.getNodeInstantiator().instantiate(request);InstantiationRequest.of(...) names the type and the Java class you expect for the root, which Milo checks while planning. When instantiate returns, the result's typed root() is already stored with its planned references, so clients can browse to the Devices folder and read its DisplayName. The default conflict policy rejects a NodeId that already exists. Keep result, because it is how you remove this instance later.
The Devices folder needs no member choices, but richer types do. You make them in the request, before any node is created.
| Requirement | Request API and responsibility |
|---|---|
| Include a specific optional |
includeOptional(BrowsePath) takes a full QualifiedName path. Nested selection includes needed ancestors. |
| Include all optionals |
includeAllOptionals() includes optional Methods too. Confirm the resulting public shape. |
| Exclude an optional subtree |
excludeOptional(BrowsePath) prunes its subtree. Mandatory members cannot be omitted. |
| Pin an identity |
assignNodeId(path, id) overrides strategy and default allocation. |
| Derive identities |
nodeIdStrategy(...) receives root, occurrence path, and declaration. Produced IDs are collision-checked. |
| Realize placeholders |
expandPlaceholder(...) describes concrete occurrences. forPlaceholder(...) creates an additional realization against an existing instance. Choose supported concrete subtypes for abstract declarations. |
| Configure behavior |
onNode(...) attaches filters and initial state. bindMethod(...) installs Method handlers. |
Use org.eclipse.milo.opcua.sdk.server.nodes.instantiation.BrowsePath, not the deprecated factory class with the same name. Placeholder occurrences need concrete browse names and unique NodeIds, which you supply, because Milo does not guess them from device inventory.
Methods are copied by default. With shared realization, bind per Object through MethodBindings. Include argument metadata and optional Methods deliberately, since they become part of the instance's public shape.
instantiate(request) plans and applies in one call, and you can also run the steps separately. describe(typeId) returns a compiled structural model, plan(request) resolves selections, identities, collisions, and references without constructing nodes, and apply(plan) materializes the plan. Planning first lets you inspect an instance and its diagnostics before anything changes in the address space. Compilation reports structural problems as diagnostics instead of silently truncating inheritance.
Plans and cached models are snapshots. Coordinate model edits, and invalidate the model cache before expecting a changed declaration tree. A plan made against an older model fails when applied.
Hooks run inside instantiation's staged workflow. Put declarative identity and attribute choices in the request, and use hooks for behavior. Do not rely on rollback to undo arbitrary external side effects a hook causes, because rollback removes only the nodes and references the instantiation recorded.
The result reports diagnostics and the storage guarantee achieved. Built-in managers support atomic storage primitives. Custom managers using emulated primitives give best-effort behavior and require appropriate writer coordination.
| What you see | Meaning and response |
|---|---|
| Error diagnostics on a plan |
plan(...) reports request problems such as NodeId collisions, invalid classes, or omitted Mandatory members on the plan instead of throwing, and apply(...) refuses such a plan. Choose another identity for a collision, or pin one with assignNodeId(...). |
InstantiationException from instantiate(...) or apply(...)
|
The plan had errors or an apply stage failed. The exception names the phase and carries per-path diagnostics. A failed apply removes what it added, except when the rollback itself fails, which carries its own diagnostics, or when a Method binder already changed a reused or shared node. |
UaException from describe(...) or plan(...)
|
The type's model cannot be compiled. Fix the type definition or its declarations. |
| Members you expected are missing | Inspect the result's skipped declarations and diagnostics instead of assuming the whole type was realized. Optional members need explicit selection. |
The nodes.instantiation package is experimental for one minor release, so its API can still change. Check the 1.2 migration guide before changing a persisted model.
Call result.deleteCreated() when this model instance is retired. The result owns only its recorded additions, so it does not delete reused nodes or replacements installed later under the same identifiers. Cleanup and rollback check object identity to leave those replacements in place.
Keep application resources, Method binding lifetimes, and Condition registrations outside node-storage ownership. Stop those users before deleteCreated(), because cleanup removes nodes but does not terminate application threads.
NodeFactory and the legacy factory package remain available, with frozen behavior, during their deprecation period.
Audit every persisted root and member NodeId before switching. The new default derivation escapes backslashes, slashes, and colons in names and String roots. It marks non-String roots and String roots that mimic type markers. These rules can change descendant identities even when the root is unchanged. legacyPathStrings() preserves the old unescaped path-string formula for unpinned, default-derived members. Handle existing pinned identities with assignNodeId(...) or a deliberate strategy.
The new engine copies MinimumSamplingInterval, Historizing, AccessLevelEx, RolePermissions, UserRolePermissions, and AccessRestrictions from declarations. In several paths, the legacy factory left these unset or unrestricted. An existing declaration can therefore change instance sampling or access behavior after migration, so re-test Reads, Writes, Calls, Browse, and subscriptions for every intended role and channel policy.
Map each legacy pattern to its request equivalent:
- Replace post-creation identity changes with request identity and parent placement.
- Replace optional callbacks with path selection, and post-creation deletion with request-time exclusion.
- Compare optional Method shape and placeholder realization, not just the root's class.
- Retain the new result and clean up with
deleteCreated()instead of guessing which nodes were created.
Migrate a test model first, and verify client browsing, calls, persisted identifiers, and cleanup. The 1.2 migration guide records additional behavior changes.
Instances give the server its structure, and their Variables still need values. To control how often the server samples those values for subscribed clients, and to read a device in batches, continue with Sampling.
Related: Server SDK · Address space · Events · Methods
User guide for Milo 1.2.0-RC1 at source 69cb25087, 2026-10-12. Home · Getting started · Examples · Troubleshooting · Release notes
- Home
- Start here
- Client guide
- Server guide
- Feature guides
- Reference
- Operations
- Examples
- Release notes
- Contributing