> This page is for NeMo Relay, version v0.10.0.
> For other versions, use one of these documentation indexes:
> - Latest (v0.10.0) (default): https://docs.nvidia.com/nemo/relay/latest/llms.txt
> - dev: https://docs.nvidia.com/nemo/relay/dev/llms.txt
> - v0.10.0: https://docs.nvidia.com/nemo/relay/v0.10.0/llms.txt
> - v0.9.4: https://docs.nvidia.com/nemo/relay/v0.9.4/llms.txt
> - v0.8.4: https://docs.nvidia.com/nemo/relay/v0.8.4/llms.txt
> - v0.7.3: https://docs.nvidia.com/nemo/relay/v0.7.3/llms.txt
> - v0.6.0: https://docs.nvidia.com/nemo/relay/v0.6.0/llms.txt
> - v0.5.0: https://docs.nvidia.com/nemo/relay/v0.5.0/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo/relay/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/relay/_mcp/server.

# Register Behavior

> Install component-owned event, tool, LLM, and stream behavior in each binding.

Registration converts a valid component document into owned runtime behavior. The
example installs only the feature groups whose `enabled` values are true and reads
[priority and `break_chain`](/build-plugins/fundamentals/plugin-context) directly from
configuration. It never calls a process-global middleware registrar from inside the
plugin.

An optional `registration_control` group in each complete example demonstrates an
activation-owned conditional middleware guardrail. It is disabled by default. When
enabled, the context qualifies the gate name and removes the gate automatically during
rollback or teardown. Refer to
[Conditional Middleware Guardrails](/about-nemo-relay/concepts/conditional-middleware-guardrails)
before selecting and storing the target name.

## Register an Activation-Owned Gate

The following examples register the same constant-result gate after configuration has
supplied a discovered effective target name:

#### Python

```python
from nemo_relay.runtime_registrations import RuntimeRegistrationKind

control = settings["registration_control"]
if control["enabled"]:
    context.register_conditional_middleware_guardrail(
        "documentation-registration-control",
        {RuntimeRegistrationKind(kind) for kind in control["kinds"]},
        control["registration_name"],
        lambda _kinds, _name: control["reason"],
    )
```

#### Node.js

```js
const control = settings.registration_control;
if (control.enabled) {
  context.registerConditionalMiddlewareGuardrail(
    'documentation-registration-control',
    control.kinds,
    control.registration_name,
    () => control.reason,
  );
}
```

#### Rust

```rust
if config.registration_control.enabled {
    let reason = config.registration_control.reason.clone();
    ctx.register_conditional_middleware_guardrail(
        "documentation-registration-control",
        config.registration_control.kinds.iter().copied().collect(),
        &config.registration_control.registration_name,
        Arc::new(move |_kinds, _name| Some(reason.clone())),
    )?;
}
```

The component-local gate name does not need a plugin prefix. Relay qualifies the name
and records its rollback operation. The target name is different: it must already be the
effective name that discovery returned for the current runtime activation.

## Register Event Metadata Injectors

An event metadata injector receives an event snapshot and returns flat metadata
additions. Relay validates and inserts accepted additions before event sanitizers run.
Existing metadata values are preserved.

The following examples register a component-owned callback through `PluginContext`:

#### Python

```python
from nemo_relay import EventMetadata, Json
from nemo_relay.plugin import PluginContext

def register(self, plugin_config: dict[str, Json], context: PluginContext) -> None:
    tag = plugin_config["tag"]
    context.register_event_metadata_injector(
        "component-metadata",
        10,
        lambda event: {"example.plugin.tag": tag},
    )
```

Applications can instead use `nemo_relay.event_metadata.register_injector()` for a
global callback or `nemo_relay.scope_local.register_event_metadata_injector()` for a
callback owned by an active scope. The matching deregistration functions remove those
registrations.

#### Node.js

```js
register(pluginConfig, context) {
  context.registerEventMetadataInjector('component-metadata', 10, () => ({
    'example.plugin.tag': pluginConfig.tag,
  }));
}
```

Applications can instead use `registerEventMetadataInjector()` for a global callback
or `scopeRegisterEventMetadataInjector()` for a callback owned by an active scope. The
matching deregistration functions remove those registrations.

#### Rust

```rust
let tag = config.tag.clone();
ctx.register_event_metadata_injector(
    "component-metadata",
    10,
    Arc::new(move |_event| {
        let tag = tag.clone();
        Box::pin(async move {
            Ok(BTreeMap::from([(
                "example.plugin.tag".into(),
                Json::String(tag),
            )]))
        })
    }),
)?;
```

Applications can instead use `register_event_metadata_injector()` for a global
callback or `scope_register_event_metadata_injector()` for a callback owned by
an active scope. The matching deregistration functions remove those registrations.

Python and Node.js callbacks can return additions directly or asynchronously. Rust
callbacks return a future. In every binding, callback failures and invalid return values
omit that callback's additions without dropping the event.

## Register One Equivalent Request Intercept

The following excerpts show the same model-header rewrite. The full checked examples add
event observation, tool policy, execution wrappers, and streaming verification around
this common center.

#### Python

```python
from collections.abc import AsyncIterator, Awaitable, Callable

from nemo_relay import (
    AnnotatedLLMRequest,
    Json,
    LLMRequest,
    LLMRequestInterceptOutcome,
    LlmExecutionContext,
)

settings = normalized_config(config)
tag = settings["tag"]
observe = settings["observe"]
requests = settings["requests"]
execution = settings["execution"]

if observe["enabled"]:
    context.register_subscriber(
        "events", lambda event: self.events.append(event.name)
    )

def tool_policy(name: str, _args: Json) -> str | None:
    if requests["mode"] == "enforce" and name in requests["blocked_tools"]:
        return f"tool '{name}' is blocked"
    return None

context.register_tool_conditional_execution_guardrail(
    "tool-policy", 10, tool_policy
)
context.register_tool_request_intercept(
    "tool-request",
    requests["priority"],
    requests["break_chain"],
    lambda _name, args: {**args, "plugin_tag": tag},
)

def add_header(
    _name: str, request: LLMRequest, annotated: AnnotatedLLMRequest | None
) -> LLMRequestInterceptOutcome:
    headers = dict(request.headers)
    headers[requests["header_name"]] = requests["header_value"]
    return LLMRequestInterceptOutcome(
        request=LLMRequest(headers=headers, content=request.content),
        annotated_request=annotated,
    )

context.register_llm_request_intercept(
    "documentation-header",
    requests["priority"],
    requests["break_chain"],
    add_header,
)

async def stream_request(
    _name: str,
    request: LLMRequest,
    context: LlmExecutionContext,
    next_call: Callable[[LLMRequest], Awaitable[AsyncIterator[Json]]],
) -> AsyncIterator[Json]:
    assert context.response_codec is None
    async for chunk in await next_call(request):
        yield {**chunk, "plugin_stream": True}

context.register_llm_stream_execution_intercept(
    "documentation-stream", execution["priority"], stream_request
)
```

#### Node.js

```js
const settings = normalizedConfig(config);
const { observe, requests, execution } = settings;

if (observe.enabled) {
  context.registerSubscriber(
    'events', (event) => documentationPlugin.events.push(event.name),
  );
}

context.registerToolConditionalExecutionGuardrail('tool-policy', 10, (name) => (
  requests.mode === 'enforce' && requests.blocked_tools.includes(name)
    ? `tool '${name}' is blocked`
    : null
));
context.registerToolRequestIntercept(
  'tool-request', requests.priority, requests.break_chain, (_name, args) => ({
    ...args,
    plugin_tag: settings.tag,
  }),
);

context.registerLlmRequestIntercept(
  'documentation-header',
  requests.priority,
  requests.break_chain,
  ({ request, annotated }) => ({
    request: {
      ...request,
      headers: {
        ...request.headers,
        [requests.header_name]: requests.header_value,
      },
    },
    annotated,
  }),
);

context.registerLlmStreamExecutionIntercept(
  'documentation-stream',
  execution.priority,
  async function* (request, context, next) {
    console.assert(context.responseCodec === null);
    for await (const chunk of await next(request)) {
      yield { ...chunk, plugin_stream: true };
    }
  },
);
```

#### Rust

```rust
let tag = config.tag.clone();
let mode = config.requests.mode.clone();
let blocked_tools = config.requests.blocked_tools.clone();
let header_name = config.requests.header_name.clone();
let header_value = config.requests.header_value.clone();

if config.observe.enabled {
    ctx.register_subscriber(
        "events",
        Arc::new(|event| println!("event: {}", event.name())),
    )?;
}

ctx.register_tool_conditional_execution_guardrail(
    "tool-policy",
    10,
    Arc::new(move |name, _args| {
        let mode = mode.clone();
        let blocked = blocked_tools.clone();
        Box::pin(async move {
            Ok((mode == "enforce" && blocked.contains(&name))
                .then(|| format!("tool '{name}' is blocked")))
        })
    }),
)?;

ctx.register_tool_request_intercept(
    "tool-request",
    config.requests.priority,
    config.requests.break_chain,
    Arc::new(move |_name, mut args| {
        let tag = tag.clone();
        Box::pin(async move {
            if let Some(object) = args.as_object_mut() {
                object.insert("plugin_tag".into(), Json::String(tag));
            }
            Ok(args)
        })
    }),
)?;

ctx.register_llm_request_intercept(
    "documentation-header",
    config.requests.priority,
    config.requests.break_chain,
    Arc::new(move |_name, mut request, annotated| {
        let header_name = header_name.clone();
        let header_value = header_value.clone();
        Box::pin(async move {
            request.headers.insert(header_name, header_value.into());
            Ok(LlmRequestInterceptOutcome::new(request, annotated))
        })
    }),
)?;

ctx.register_llm_stream_execution_intercept(
    "documentation-stream",
    config.execution.priority,
    Arc::new(move |_name, request, context, next| {
        Box::pin(async move {
            debug_assert!(context.response_codec().is_none());
            let downstream = next(request).await?;
            Ok(LlmJsonStream::new(downstream.map(|chunk| {
                chunk.map(|mut value| {
                    if let Some(object) = value.as_object_mut() {
                        object.insert("plugin_stream".into(), Json::Bool(true));
                    }
                    value
                })
            })))
        })
    }),
)?;
```

Execution codec objects are scoped to the callback that received them. Non-streaming
capabilities expire when that callback finishes. A streaming request capability remains
usable while the returned stream is live and expires on completion, error, close, or
drop. Retaining a codec object past that boundary produces an error; it does not extend
the invocation lifetime.

The LLM intercept returns the
[complete outcome](/reference/llm-request-intercept-outcomes) rather than relying on
mutation. In particular, it preserves `annotated`. The Rust request is mutable inside its
owned callback value; Python creates a new typed request, and Node.js creates a new plain
object. Those language differences do not change Relay semantics.

## Initialize and Inspect

Use the following procedure to verify successful activation and transactional rollback:

1. Register the kind, validate the shared component, and stop if the report contains an
   error. Duplicate kind registration is itself an error and should fail the test.
2. Initialize the valid document with Rust `initialize`, Python
   `nemo_relay.plugin.initialize`, or Node.js `plugin.initialize`. Keep the
   returned activation handle alive for the entire host lifetime.
3. Inspect the report on that handle. Rust uses `activation.report()`, while Python and
   Node.js use `activation.report`. It describes that activation without rerunning
   validation.
4. Execute an LLM request and inspect the real callback headers. Then emit an event and
   execute the representative tool and stream paths from the checked example.
5. Force a later registration in the same component to fail. Initialization should
   reject, the new partial registrations should disappear, and Relay should restore the
   previous configuration when it can prove cleanup succeeded.

Success means configuration controls the installed surfaces, names are component-owned,
the activation report matches the runtime effect, and failed registration leaves no
half-active middleware.