Skip to content

Registration Metadata Contract

At a glance — Functions, entry points, websocket services, Event Script plugins and graph fetch features all register declaratively: metadata attached at the definition site, discovered and resolved by the engine at boot. This page fixes the metadata model and its semantics across languages. How each language attaches the metadata — a Java annotation, a Rust attribute macro, a Python or TypeScript decorator — is an idiom; everything else on this page is the contract, and the golden vectors prove an engine honors it.

The Java engine is the reference implementation; the Annotations Reference documents its carrier surface in full. The Rust engine implements this contract today (its Macros Reference is the carrier twin of the annotations page). Python and Node.js ports implement it next.

One model, many carriers

The same declaration in the four languages:

// Java - runtime classpath scan
@PreLoad(route = "hello.world", instances = 10)
public class HelloWorld implements TypedLambdaFunction<Map<String, Object>, Object> { ... }
// Rust - link-time inventory
#[preload(route = "hello.world", instances = 10, typed)]
struct HelloWorld;   // + impl TypedFunction<I, O>
# Python (future) - decorator + explicit package walk
@pre_load(route="hello.world", instances=10)
class HelloWorld(TypedLambdaFunction): ...
// Node/TypeScript (future) - decorator + explicit module glob
@PreLoad({ route: "hello.world", instances: 10 })
class HelloWorld implements TypedLambdaFunction<EventEnvelope, unknown> { ... }

The discovery mechanics differ by necessity — only the JVM scans a classpath at runtime — but the developer-visible style, the metadata model and the boot-time semantics must not.

The canonical metadata model

Every registration resolves to one canonical record. Wire form for tooling and conformance: JSON, camelCase keys, enums as strings (never ordinals).

{
  "kind": "function | websocket | entrypoint-before | entrypoint-main | plugin | feature",
  "routes": ["array of route names - function kind; comma-separated aliases in the carrier"],
  "name": "string - websocket / plugin / feature kinds",
  "namespace": "string - websocket kind, default \"ws\"",
  "sequence": "integer - entrypoint kinds, default 10 (0 is framework-reserved)",
  "declaredInstances": "integer, default 1",
  "envInstances": "string configuration KEY, default \"\" - carried for audit",
  "resolvedInstances": "integer - the boot-resolved effective value, clamped 1..1000",
  "isPrivate": "boolean, default true",
  "zeroTracing": "boolean marker, default false",
  "eventInterceptor": "boolean marker, default false",
  "optionalService": "string | null - an OR-list of key, !key, key=value conditions"
}

Capability fields (per-language applicability)

Some Java @PreLoad fields are capabilities of a runtime family, not universal contract:

Field Java Rust Python / Node guidance
customSerializer runtime ObjectMapper swap per route N/A — serde is the single compile-time serializer; per-type attributes cover the use cases applicable — both have runtime serialization hooks; carry as a type-name string
inputPojoClass defeats JVM type erasure; enables List<PoJo> N/ATypedFunction<I, O> is monomorphized; body_as::<Vec<T>>() carries the element type Python: type hints suffice; Node: applicable as a class reference
inputStrategy / outputStrategy SNAKE / CAMEL / DEFAULT, flippable at runtime (snake.case.serialization) N/A at runtime#[serde(rename_all)] fixes case at compile time (the one genuine capability difference) applicable — runtime case mapping is natural in both
executionHint (reserved) @KernelThreadRunner → kernel thread pool reserved, unimplemented — every function is a tokio task; revisit if a field workload needs spawn_blocking Python: relevant (GIL / thread pool); Node: worker_threads — design when porting
string length/index semantics (plugin catalog: length, substring) UTF-16 code units (String.length() — JVM legacy, retained) Unicode scalar values (chars()) — the ports rule Python len() and Go RuneCountInString are scalar-native; Node uses [...str].length, NOT the UTF-16 .length — never retrofit the JVM legacy

A port documents each N/A explicitly (the Rust docs' !!! note "Rust port" convention); silence is not a disposition. The string-semantics row is a bounded, deliberate divergence (maintainer ruling, 2026-07-26): identical for all Basic-Multilingual-Plane text — English, Chinese, JSON keys, typical enterprise payloads — and differing only for supplementary-plane characters (emoji, historical scripts), which Java counts as 2 code units and every port counts as 1 scalar value.

Fixed semantics — every port MUST

  1. Attach at definition, resolve at boot. envInstances names a configuration KEY (which may itself hold ${ENV_VAR:default}); the value is read at framework boot — never at decoration, macro-expansion or import time. A numeric value wins; anything else falls back to declaredInstances. Effective instances clamp to 1..1000.
  2. Optional-service grammar is exactly the Java Feature evaluator: comma-separated conditions are OR-ed; ! negates; key=value matches case-insensitively; a bare key means key=true; no annotation means always required. Evaluated at boot against the application configuration; a false condition skips registration with a "Skip optional" log line.
  3. Marker stacking is order-free. @ZeroTracing / @EventInterceptor / @OptionalService combine with the primary annotation in any order, as Java annotations always have. Where a language's mechanism is order-sensitive by nature (Rust attribute macros expand outside-in; Python decorators apply bottom-up), the port owes the engineering to hide it — the Rust engine's markers re-attach themselves so either order works.
  4. One conflict policy. Explicit programmatic registration wins over declarative (it runs later and replaces). Within any registry, a duplicate name is a WARNING ("Reloading ... please check duplicated ... name") and the later registration wins — never a silent replace, never an error. Registration order among declaratively discovered items is not guaranteed (classpath scan order, link order, import order); do not rely on it.
  5. Extension-point naming. A plugin or feature name is the positional string on the carrier — @FetchFeature("log-request-headers"), #[simple_plugin("getFirst")], @simple_plugin("getFirst"). For plugins only, omitting the name derives it from the declaration: Java lowercases the first letter of the class simple name; Rust camelCases the snake_case fn name — idiomatic declarations in every language yield the same registered name. Plugins are Event Script capabilities (flow vocabulary): they take no optional-service gating and are never conditionally on/off. Features are runtime behaviors: they honor optional-service gating.
  6. Boot sequence: discover → register → override (yaml.preload.override: configuration-driven route rename / fan-out / instance re-tuning, applied to the collected function set before registration) → resolve (envInstances) → validate (route uniqueness warnings, non-empty registries where built-ins are expected) → route table. Lifecycle anchors: plugins load before flow compilation; features load before graph execution; entry points run in ascending sequence with 0 reserved for the framework.
  7. Discovery is explicit and fails loudly. Java: classpath scan over the base packages plus web.component.scan (free). Rust: link-time inventory collection — plus a startup assertion on expected built-in counts, because a crate that is never linked contributes nothing, silently. Python: a package walk (pkgutil.walk_packages) importing every module under the services packages before resolution — an unimported module's decorator never runs. Node: a directory glob + dynamic import() with the same failure mode. Both future ports must fail loudly on an empty registry that should not be empty.
  8. Misuse is a first-class contract. Invalid declarations fail at the earliest possible stage with a helpful message — unknown parameters name the valid set, a marker without a primary points at the correct form. Where the failure is compile-time (Rust), compile-fail tests guard the messages (trybuild); where it is import/boot-time (Java, Python, Node), unit tests do.

Conformance — the golden vectors

The contract is proven the same way the event envelope wire format is: golden vectors shared verbatim between repositories. Three files, one per metadata-rich kind:

Vectors file Kind Java module Rust crate
registration-vectors/core.json function platform-core platform-core
registration-vectors/plugin.json plugin event-script-engine event-script
registration-vectors/feature.json feature minigraph-playground-engine knowledge-graph

Each engine declares the same small fixture set through its own carrier (annotation / macro / decorator), boots, and asserts that the resolved registrations match the golden entries exactly — including boot-time envInstances resolution against the vectors' assumedConfig, marker effects, name derivation, and the absence of every gatedOut fixture. A new port passes the contract when all three vector suites pass against files byte-identical to these.

The websocket and entry-point kinds carry little metadata beyond a name/namespace or a sequence; they are pinned by each engine's own registration tests rather than shared vectors.

Porting order (the playbook)

For a new language port, in order: (1) implement the carrier + local registry + boot resolver for the function kind; (2) pass core.json; (3) add the extension points and pass plugin.json / feature.json; (4) document every capability-field disposition and intentionally unported carrier; (5) adopt the discovery-failure assertions. The future-ports playbook covers the runtime side (wire format, telemetry parity) — this page covers the declaration side; together they are the port-acceptance bar.

See also