Intent and Entity Registration Bus Contract

August 13, 2026 · View on GitHub

Spec ID: OVOS-INTENT-4 · Version: 2 · Status: Draft

This document defines the bus messages a skill uses to declare its intents and entities. It is the wire format for intent registration — nothing else. Registrations are broadcast on the bus; pipeline plugins (OVOS-PIPELINE-1) consume what they want; the orchestrator passively indexes everything for introspection. The bus-level utterance lifecycle (matching, dispatch, handler lifecycle, terminal events) is owned by OVOS-PIPELINE-1.

It is the bus-level companion to OVOS-INTENT-3: where that specification defines what an intent is, this one defines how a skill puts that intent on the bus so a plugin can match against it.

It builds on three companion specifications:

  • the Bus Message Specification (OVOS-MSG-1) — the envelope every message defined here travels in, the destination routing key, the session carrier, and the forward / reply / response derivations;
  • the Intent Definition Specification (OVOS-INTENT-3) — the intent concept, identity model, the two definition methods, and the match result that this spec carries on the bus;
  • the Locale Resource Formats Specification (OVOS-INTENT-2) and the Sentence Template Grammar Specification (OVOS-INTENT-1) — the authoring file formats and template grammar a skill loader expands before emitting a registration payload (file paths never cross the bus; see §5.1).

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, RECOMMENDED and MAY are used as in RFC 2119.


1. Scope

This specification defines a small fixed set of registration topics and the orchestrator-provided introspection interface:

  • the two intent registration messages — one per definition method (OVOS-INTENT-3 §2);
  • the entity registration message — the .entity value-set hint (OVOS-INTENT-3 §5.2);
  • the deregistration messages, for one intent, one entity, or a whole skill;
  • the enable and disable messages — temporary suppression of a registered intent without losing its definition;
  • the introspection messages — list and describe registrations, served by the orchestrator's passive registration index (§10);
  • the session-scoped registration model (§11) — how every registration is automatically keyed to the registering session, enabling per-session skill sets and distributed satellite deployments.

It does not define:

  • the intent concept itself (OVOS-INTENT-3) or the handler reference, which never crosses the bus (§9);
  • how plugins implement registration storage, matching, or consumption — black box (OVOS-PIPELINE-1);
  • the utterance lifecycle, dispatch, handler-lifecycle trio, match-result notification, or ovos.intent.unmatched — all owned by OVOS-PIPELINE-1;
  • session lifecyclesession is carried opaquely per OVOS-MSG-1;
  • language fallback — what happens when no registration matches the utterance language exactly is out of scope for this specification.

2. Architectural model — registrations are broadcast

Registration messages defined here are broadcast on the bus. There is no central party that owns, validates, or routes them; whether any loaded pipeline plugin (OVOS-PIPELINE-1) consumes a given registration is a plugin concern, out of scope here. A registration no plugin consumes is silently dropped — the skill's intent will not match, and the deployment is responsible for diagnosing why (typically: wrong plugin loaded for the registration method).

The orchestrator (OVOS-INTENT-3 §6.1) maintains the manifest (§10): a passive index built from observed registrations, observability-only. In processing registrations it does not gate matching, influence consumption, or block re-registration. Other specifications MAY consult the manifest read-only during the utterance lifecycle (for example, OVOS-PIPELINE-1 §6.2's required_slots backstop); such consultation does not make the manifest a gate on registration processing. Plugins are observably pluggable — adding or removing one is a deployment concern; bus traffic and the manifest are unaffected.

Registrations are fire-and-forget: there is no .response reply, no acknowledgement, no error event. A producer that needs to verify a registration landed queries the manifest (§10); manifest presence is the only signal this specification defines.

Manifest presence is not matchability. The orchestrator indexes every non-reserved registration it observes, verbatim, without validating the payload — reserved intent_name values (§3.2) are the single exclusion. A structurally malformed registration therefore still appears in the manifest even though every consuming plugin rejects it under §5.3 / §6.3 / §7.2. A producer reading its own entry back learns that the broadcast reached the orchestrator, not that any plugin will ever match it. The plugin-side WARN log (§5.3) is the signal for that half.


3. Identity

3.1 Skills self-identify on every emission

A skill MUST set Message.context["skill_id"] to its own skill_id (OVOS-INTENT-3 §3) on every Message it originates or mutates before placing it on the bus. This covers fresh emissions (registration messages of §§5–8, ad-hoc skill-defined topics, etc.) and any Message whose context, data, or session the skill modifies before emission.

For a skill handler running under dispatch, conformance is structural: the orchestrator stamps context["skill_id"] on the dispatch Message (OVOS-PIPELINE-1 §7.1), and all Messages the handler derives from it via the OVOS-MSG-1 §5 derivation semantics inherit that value automatically. No extra stamp step is needed on the dispatch path.

Message.context["skill_id"] is the authoritative attribution key for skill-originated bus traffic — observers MUST NOT infer the originating skill from topic names or data fields. A Message arriving without context["skill_id"] is either not skill-originated or is from a non-conformant skill.

Enforcement

On the dispatch path enforcement is structural — the orchestrator stamps context["skill_id"] per OVOS-PIPELINE-1 §7.1 and MSG-1 derivation propagates it to handler-derived Messages. For emissions outside the dispatch path, the component that loads skills SHOULD intercept the emit pathway so non-conformant handler code cannot escape. A Message whose context["skill_id"] disagrees with the <skill_id> of the dispatch it derives from is malformed; the orchestrator MUST log the drift at WARN.

3.2 Identity carried by every registration message

Every registration message carries the identity of what is being registered, inside the Message's data (OVOS-MSG-1 §2.2). The identity fields restate OVOS-INTENT-3 §3 at the bus layer.

For an intent:

FieldTypeRequiredSource
skill_idstringyesINTENT-3 §3 — assistant-unique.
intent_namestringyesINTENT-3 §3 — unique within the skill.
langstringyesBCP-47, compared case-insensitively (INTENT-2 §2). The language of the resource being registered — distinct from session.lang.

The triple (skill_id, intent_name, lang) identifies an intent (INTENT-3 §3). For manifest indexing and replacement (§8.1), the registration key is the quintuple (session_id, skill_id, intent_name, lang, method)method being keyword (§5) or template (§6), and session_id read from the Message context, never from data. The session_id component is owned by §11.1, which also defines the "default" scope; the rest of this section reads as if session_id were fixed. Registering a quintuple that matches an existing entry replaces that entry only; the other-method registration for the same triple is untouched. Replacement is also per-language: other languages of the same (skill_id, intent_name) are unaffected.

For registration and deregistration, the payload skill_id MUST equal context.skill_id. This holds for ovos.intent.register.keyword, ovos.intent.register.template, ovos.entity.register, ovos.intent.deregister, ovos.entity.deregister, and ovos.skill.deregister. A consumer — plugin or orchestrator — MUST NOT index or act on one of these messages whose payload skill_id differs from context.skill_id, and MUST log the mismatch at WARN with both values and the rejecting topic. Without this check a skill could register or deregister another skill's intents; ovos.skill.deregister (§8.4) in particular would be a remote uninstall. The same rule governs fallback registration (OVOS-FALLBACK-1 §3.1).

ovos.intent.enable and ovos.intent.disable are control messages, not ownership claims, and are exempt from the identity check: the payload skill_id names the target — the skill whose intent is being suppressed or re-armed — while context.skill_id names the source requesting it, and the two MAY differ. Cross-skill control is the point of the bus-level surface (§8.5): an admin UI, a conflict-resolving skill, or an A/B harness suppresses another skill's intent without owning it. A consumer SHOULD log source and target at DEBUG when they differ.

Cross-skill control is, deployment-wide, an unsolved trust problem; which sources may modify which targets is a hardening decision, not something this specification can settle. An orchestrator MAY therefore enforce a deployment policy that blocks cross-skill control messages (source ≠ target) — dropping the message and logging the refusal at WARN with both identities and the topic. The policy's shape (allowlist, config flag, anything else) is deployment-defined and out of scope; absent one, cross-skill control is honoured as specified above.

The target session needs no field of its own: a control message affects the scope of the session_id its context carries, like every message of §§5–8 (§11.1). A controller managing another session's registrations declares that session on the message — the ordinary per-message session declaration of OVOS-SESSION-2, not a mechanism of this specification. What bounds an external participant to its own scope is the bridge (OVOS-BRIDGE-1), not this message shape.

A single intent MAY be registered under both methods — they are two training-data representations of the same handler. Different pipeline plugins consume different methods; a match from either dispatches to the same <skill_id>:<intent_name> topic. The wire contract makes no claim about which representation should "win" when both produce a match — that is a pipeline policy concern (OVOS-PIPELINE-1). Producers MAY ship divergent suppression vocabularies between the two methods (different excluded for keyword vs different blacklist for template); each plugin honours only its own method's suppression.

For an entity, intent_name is replaced by entity_name (same uniqueness rule: unique within the skill). Entity registrations have no method axis.

Other specifications MAY reserve specific intent_name values; the authoritative registry is OVOS-PIPELINE-1 §7.3. A registration naming a reserved intent_name is malformed — the orchestrator and every consuming plugin treat it under the malformed-payload rules of §5.3 / §6.3 (log at WARN, do not index).


4. Topics

Topics defined by this specification are lowercase, dot-separated, and namespaced under ovos.intent., ovos.entity., and ovos.skill.. All registration topics are broadcast — any component (typically pipeline plugins) may subscribe. The orchestrator also subscribes to all of them passively, to maintain the introspection index of §10.

TopicDirectionPurpose§
ovos.intent.register.keywordskill → bus (broadcast)Register a keyword intent (INTENT-3 §4).§5
ovos.intent.register.templateskill → bus (broadcast)Register a template intent (INTENT-3 §5).§6
ovos.intent.deregisterskill → bus (broadcast)Remove one intent.§8
ovos.intent.enableskill → bus (broadcast)Re-arm a previously disabled intent.§8.5
ovos.intent.disableskill → bus (broadcast)Suppress an intent without removing its definition.§8.5
ovos.entity.registerskill → bus (broadcast)Register an .entity value-set hint (INTENT-3 §5.2).§7
ovos.entity.deregisterskill → bus (broadcast)Remove one entity.§8
ovos.skill.deregisterskill → bus (broadcast)Remove all intents and entities for one skill_id.§8
ovos.intent.listobserver → orchestratorQuery registered intents (introspection; served by the orchestrator).§10
ovos.intent.describeobserver → orchestratorQuery one registered intent (introspection; served by the orchestrator).§10

Match notification, dispatch, and handler-lifecycle topics live in OVOS-PIPELINE-1 §§7–9, not here.


5. Keyword intent registration

Topic: ovos.intent.register.keyword.

A keyword intent is defined by keyword constraints over vocabularies (INTENT-3 §4). This message carries the constraints and the vocabularies in one payload.

5.1 Vocabulary descriptor

A vocabulary descriptor is a JSON object identifying one vocabulary (INTENT-3 §4.1):

{ "name": "set", "samples": ["set", "change", "adjust"] }

name is the vocabulary name (INTENT-3 §4.1) — this is the key under which the vocabulary's captured phrase appears in the match result (OVOS-PIPELINE-1 Match.slots; INTENT-3 §4.3). samples entries are slot-free OVOS-INTENT-1 templates (INTENT-1 §1.1) and MUST contain at least one entry.

Locale resource files (.voc, .intent, .entity, .blacklist; OVOS-INTENT-2) are a producer-side authoring convenience: a skill loader reads them and inlines their expanded content into the registration payload. File paths never appear on the wire.

5.2 Payload shape

{
  "skill_id": "lighting.skill",
  "intent_name": "set_brightness",
  "lang": "en-US",
  "required": [
    { "name": "set", "samples": ["set", "change", "adjust"] },
    { "name": "brightness", "samples": ["brightness", "light level"] }
  ],
  "optional": [],
  "one_of": [
    [
      { "name": "up",   "samples": ["up",   "higher", "brighter"] },
      { "name": "down", "samples": ["down", "lower",  "dimmer"]  }
    ]
  ],
  "excluded": [
    { "name": "question", "samples": ["what is", "how"] }
  ]
}

Field reference:

FieldTypeRequiredMeaning (per INTENT-3 §4.2)
requiredarray of vocabulary descriptorsno (absent = [])Every required vocabulary MUST occur in the utterance.
optionalarray of vocabulary descriptorsno (absent = [])Captured if it occurs; absence does not prevent a match.
one_ofarray of arrays of vocabulary descriptorsno (absent = [])Each inner array is one group; at least one member of each group MUST occur.
excludedarray of vocabulary descriptorsno (absent = [])If any of these occurs, the intent MUST NOT match.

Empty arrays are permitted, and an absent list-valued key is equivalent to an empty list — a consumer MUST NOT treat a payload as malformed merely because a list-valued key is omitted. Requiring empty keys on the wire would add nothing a consumer can rely on (the §5.3 validity rules operate on the resolved values either way) while turning every producer omission into a spurious rejection.

5.3 Constraint validity

The constraint rules of INTENT-3 §4.2 are restated here as bus-layer malformed-payload rules:

  • The intent_name MUST NOT be one reserved by another spec (§3.2; the authoritative registry is OVOS-PIPELINE-1 §7.3).
  • The combined required and one_of lists MUST NOT both be empty — an intent with only optional and excluded has nothing that must be present and is malformed (INTENT-3 §4.2).
  • A vocabulary MUST NOT appear under more than one role within a single registration (INTENT-3 §4.2). Vocabulary identity for this check is by name.
  • Two descriptors that share a name within the same role are not malformed: a consumer MUST merge them into one vocabulary whose samples is the union of theirs. The union is what the cross-role uniqueness check above sees, and what the match result reports under that name.
  • Every vocabulary descriptor MUST carry a non-empty samples array (§5.1).
  • A vocabulary descriptor's samples MUST include at least one template that expands to a non-empty sample (OVOS-INTENT-1 §3.6). A descriptor that yields zero non-empty samples is malformed.

An absent required, optional, one_of, or excluded key is read as an empty list (§5.2); the validity rules above apply to the resolved values.

Unknown payload fields are not malformed: a consumer MUST ignore fields it does not recognise and MUST NOT reject a registration because of them. This is what lets companion specifications (e.g. OVOS-CONTEXT-1's requires_context / excludes_context) ride on the registration payload as additional fields; the orchestrator's manifest preserves them (§10.2).

A consuming plugin MUST NOT index a registration that violates these rules. The rejecting plugin MUST log the rejection at WARN, including skill_id, intent_name, lang, the rejecting topic, and a one-line reason — this is the only debugging signal a producer receives, since the bus is fire-and-forget (§2). The topic is part of the actionable signal because the same (skill_id, intent_name, lang) may be valid as keyword and malformed as template (or vice versa, §3.2). Structured logging is RECOMMENDED.

Within a vocabulary descriptor, an individual sample that is not parsable as OVOS-INTENT-1 §3 grammar, or that expands to zero non-empty samples, does not malform the registration. A consuming plugin MUST NOT reject the registration on its account: it MUST skip the offending sample, MUST log each skipped sample at WARN with the fields above plus the sample itself, and MUST index the remaining valid samples. Only a descriptor in which no sample expands to a non-empty sample is malformed (the zero-yield rule above), and only then is the registration rejected.

5.4 No intent-suppression .blacklist

The intent-suppression .blacklist (INTENT-2 §4.3, INTENT-3 §5.5) is not used with keyword intents; it is a template-intent artifact and travels on the §6 payload's blacklist field. The excluded role (§5.2) is the keyword-intent suppression mechanism (INTENT-3 §4.2).

The slot-value-exclusion role of a .blacklist paired with an .entity (INTENT-2 §4.3) is a different role again, and this specification defines no wire carrier for it.


6. Template intent registration

Topic: ovos.intent.register.template.

A template intent is defined by example sentence templates (INTENT-3 §5, INTENT-1 §3).

6.1 Payload shape

{
  "skill_id": "music.skill",
  "intent_name": "play_music",
  "lang": "en-US",
  "samples": [
    "(play|put on) {query}",
    "(play|put on) {query} (on|using) {engine}",
    "i want to listen to {query}"
  ],
  "blacklist": ["trailer", "music video"],
  "required_slots": ["query"]
}

Field reference:

FieldTypeRequiredMeaning
samplesarray of stringsyesOVOS-INTENT-1 templates with named slots (INTENT-1 §3, §5).
blacklistarray of stringsnoSlot-free phrases (INTENT-2 §4.3) whose occurrence suppresses the match (INTENT-3 §5.5).
required_slotsarray of stringsnoSlot names the engine MUST extract for a match to be valid (INTENT-3 §5.3).

As in §5.2, an absent list-valued key (blacklist, required_slots) is equivalent to an empty list; samples is the one list a producer must supply, and it must be non-empty (§6.3).

6.2 Slot sets

Templates in samples MAY declare different sets of named slots; the engine extracts only the slots declared by the template that best matches (INTENT-1 §5.5, INTENT-3 §5.1). A consuming plugin MUST accept registrations with differing slot sets across templates.

6.3 Malformed payloads

A consuming plugin MUST NOT index a template registration in which:

  • the intent_name is reserved by another spec (§3.2);
  • samples is missing or empty;
  • no template in samples is both parsable as OVOS-INTENT-1 §3 grammar and expands to at least one non-empty sample (OVOS-INTENT-1 §3.6);
  • required_slots names a slot that is not declared by any valid template in samples (INTENT-3 §5.3).

An individual template that is not parsable as OVOS-INTENT-1 §3 grammar, or that expands to zero non-empty samples, does not malform the registration by itself. A consuming plugin MUST NOT reject the registration on its account: it MUST skip that template, MUST log each skipped template at WARN with the §5.3 fields plus the template itself, and MUST index the remaining valid templates. The registration is rejected only when no valid template remains (third bullet above).

The §5.3 WARN-log rule applies: the rejecting plugin MUST log the rejection with skill_id, intent_name, lang, and a one-line reason. The §5.3 unknown-field rule also applies: unknown payload fields MUST be ignored, not treated as malformed.


7. Entity registration

Topic: ovos.entity.register.

An entity is an optional value-set hint for a template-intent slot (INTENT-3 §5.2, INTENT-1 §5.4, INTENT-2 §4.3). Registering an entity is never a precondition for an intent that references the slot name; a slot with no entity still fills normally.

7.1 Payload shape

{
  "skill_id": "music.skill",
  "entity_name": "engine",
  "lang": "en-US",
  "samples": ["spotify", "youtube music", "the radio"]
}

Field reference:

FieldTypeRequiredMeaning
entity_namestringyesUnique within the skill. By convention matches the slot name a template intent references.
samplesarray of stringsyesSlot-free value-set entries (INTENT-1 §5.4).

7.2 Malformed payloads

A consuming plugin MUST NOT index an entity registration whose samples is missing or empty, or in which no entry yields a non-empty value. The §5.3 WARN-log rule applies: the rejecting plugin MUST log the rejection with skill_id, entity_name, lang, and a one-line reason.

An individual entry that is not parsable as OVOS-INTENT-1 §3 grammar, or that yields no non-empty value, does not malform the registration by itself. A consuming plugin MUST NOT reject the registration on its account: it MUST skip the offending entry, MUST log each skipped entry at WARN with the §5.3 fields plus the entry itself, and MUST index the remaining valid entries.


8. Deregistration, enable, disable, and replacement

8.1 Replacement is implicit

Registering an intent whose (session_id, skill_id, intent_name, lang, method) quintuple matches an existing registration replaces it (INTENT-3 §6.1) — no prior deregister needed. Replacement preserves enabled/disabled state (§8.5); a producer that wants to reset that state deregisters first. The same rule applies to entities, keyed on the quadruple (session_id, skill_id, entity_name, lang) — entities have no method axis. The session_id is read from context.session.session_id (§11.1) — never from Message.data.

8.2 ovos.intent.deregister

Removes one intent. Payload:

{ "skill_id": "music.skill", "intent_name": "play_music", "lang": "en-US" }

If lang is omitted, every language registered for that (skill_id, intent_name) pair is removed. Deregistration targets the (skill_id, intent_name, lang) triple and removes all methods under it — both the keyword and template registrations of the same intent (§3.2), if both exist. There is no per-method deregistration; a skill that wants to remove only one method re-registers the other.

8.3 ovos.entity.deregister

Removes one entity. Payload:

{ "skill_id": "music.skill", "entity_name": "engine", "lang": "en-US" }

If lang is omitted, every language registered for that (skill_id, entity_name) pair is removed.

8.4 ovos.skill.deregister

Removes everything owned by a skill — every intent and every entity registered under that skill_id. Payload:

{ "skill_id": "music.skill" }

The removal is scoped to the session_id read from context.session.session_id of the Message (§11.1) — never from Message.data. A deregistration arriving under the default session removes the "default"-scoped registrations; one arriving under a satellite's session removes only that session's registrations (§11.3).

This message is broadcast, like every other registration message (§2) — it is not addressed to the orchestrator. It is emitted when a skill is unloaded (INTENT-3 §6.1), by the skill itself or by whatever component unloads it, and every subscriber that holds registrations for that (session_id, skill_id) pair drops them independently. A bridge SHOULD emit ovos.skill.deregister carrying the satellite's session in context for every skill the satellite registered when the satellite disconnects (OVOS-BRIDGE-1 §3).

Deregistering an intent, entity, or skill that is not currently registered is a no-op: registrations are fire-and-forget, every plugin processes the message independently, and any plugin without a matching record simply has nothing to remove. This makes the shutdown sequence — where every plugin the skill ever talked to receives every deregistration — naturally idempotent.

Races between a deregistration and an in-flight match (a match emitted before the deregister was processed, dispatched after) are the responsibility of the utterance lifecycle owner — see OVOS-PIPELINE-1.

8.5 ovos.intent.enable and ovos.intent.disable

A registered intent is, by default, enabled — eligible for matching. A skill MAY temporarily disable an intent without removing it; the orchestrator retains the definition in the manifest but marks it disabled, and plugins exclude it from match candidacy until it is re-enabled. The bus-level surface (rather than skill-side gating) lets external tooling — admin UIs, A/B experiments, conflict resolution — suppress an intent without modifying skill code. Both topics share the same payload as ovos.intent.deregister (§8.2), and lang semantics:

{ "skill_id": "music.skill", "intent_name": "play_music", "lang": "en-US" }

Here skill_id is the target of the operation, not the sender: unlike registration (§3.2), enable/disable are legitimately cross-skill, and context.skill_id — the source — MAY name a different skill. The target session is the context session, as for every message here (§3.2, §11.3): a controller reaches another session's scope by declaring that session on the message, not through any payload field.

If lang is omitted, every language for that (skill_id, intent_name) is affected. Like deregistration, enable/disable target the triple and apply to all methods of the intent — there is no per-method enable/disable. A producer that wants to retain only one method deregisters the triple (§8.2, removes both methods) and re-registers just the desired one.

Enabling an already-enabled intent, or disabling an already-disabled intent, is a no-op. Enabling or disabling an intent that is not currently registered is likewise a no-op — like deregistration (§8.4), each consumer processes the message independently and one without a matching record has nothing to change. Re-registration (§8.1) preserves enabled/disabled state unless the producer deregisters first. Whether a consuming plugin's disabled-state record survives a plugin reload is out of scope: a reloaded plugin that needs the current enabled/disabled state recovers it by querying the manifest (§10.1), whose enabled field reflects the latest state.

Enabled/disabled state does not survive an orchestrator restart. The manifest is rebuilt from observed broadcasts (§2, §10), and ovos.intent.disable is not re-emitted by the re-registering skill — so a restarted orchestrator indexes every re-emitted registration in its default enabled state. The party that disabled an intent is responsible for re-asserting the suppression: it SHOULD re-emit ovos.intent.disable after it observes the deployment's readiness announcement (§10), for every intent it currently holds disabled. A disabling party that does not track its own suppressions cannot recover them; the manifest is not durable state.

Enable and disable are session-scoped like every other message here (§11.3): they affect only registrations under the session_id read from context.session.session_id.


9. The handler reference is not on the bus

Per INTENT-3 §6.1, the handler reference — the code object that runs when the intent matches — never crosses the bus; it is held locally by the skill process. This specification puts only the intent definition (§§5–7) on the wire; the dispatch Message that invokes the handler is defined in OVOS-PIPELINE-1. Together they let a skill in a different process from the orchestrator host its handlers across the bus without serializing them — the contract that makes local and remote skills indistinguishable from outside.


10. Introspection — the orchestrator-owned manifest

Registration broadcasts of §5–§8 are load-time announcements; a consumer that subscribed after the skill loaded has missed them (the bus is async with no catch-up channel). The orchestrator-owned manifest is this specification's answer — the orchestrator indexes every registration it observes and serves queries against it. Skills have no introspection obligation; they emit and move on.

Pull-query is the source of truth. A consumer that needs accurate state MUST issue ovos.intent.list / ovos.intent.describe and MUST NOT rely on having heard the original broadcast. For compiled-plugin state — which intents a particular matcher actually has loaded — query OVOS-PIPELINE-1 §10 instead; the surfaces are distinct (declared vs compiled).

Under a split orchestrator (OVOS-PIPELINE-1 §2), each process answers from its own slice; consumers aggregate.

Cold-start recovery. The asymmetry cuts both ways: a skill that registered before the orchestrator (or a consuming plugin) started has emitted into the void, and because registrations are fire-and-forget (§2) nothing tells it so — the manifest stays permanently empty for that skill. A skill therefore SHOULD re-emit its full registration set when it observes the deployment's readiness announcement — the broadcast by which the orchestrator signals it is up and consuming (the topic is deployment-defined and not owned by this specification). Re-emission is safe by construction: replacement is implicit (§8.1), so a duplicate registration is idempotent.

Re-emission MUST carry the same session context as the original registration. The registration key includes session_id (§11.1), so a re-emission under a different session does not replace the original entry — it creates a second one, under a scope whose owner never registered it and cannot deregister it (§11.3). Idempotence holds only within one session scope.

For a satellite's session-scoped registrations this makes the bridge, not the satellite alone, part of the recovery path: relaying the readiness announcement to the satellite and re-establishing the satellite's registrations is defined in OVOS-BRIDGE-1 §4.4.

Two read-only topics:

10.1 ovos.intent.list

Lists registered intents. Request payload:

{ "skill_id": "music.skill", "lang": "en-US", "session_id": "satellite-abc" }

All fields are optional filters: omitting skill_id returns every skill's intents; omitting lang returns every language; omitting session_id returns intents from all sessions (global view). When session_id is provided the response returns the effective pool for that session: "default" intents plus session-specific intents (§11.2), not the raw index for that session alone. An intent registered under both methods (§3.2) appears as two entries distinguished by method.

Response (ovos.intent.list.response):

{
  "ok": true,
  "intents": [
    {
      "skill_id": "music.skill",
      "intent_name": "play_music",
      "lang": "en-US",
      "method": "template",
      "enabled": true,
      "session_id": "default"
    }
  ]
}

Each entry carries skill_id, intent_name, lang, a method of "keyword" or "template" (INTENT-3 §2), an enabled boolean (§8.5), and the session_id under which the intent was registered. Reserved-name registrations are malformed (§3.2) and do not appear in the manifest.

10.2 ovos.intent.describe

Returns the full definition of one intent. Request payload:

{ "skill_id": "music.skill", "intent_name": "play_music", "lang": "en-US", "method": "template", "session_id": "satellite-abc" }

method is an optional filter: "keyword" or "template". When omitted, the response returns every registered method for the triple.

session_id is likewise an optional filter. When provided, only definitions registered under that session_id are returned. When omitted, definitions from every session_id are returned — the same (skill_id, intent_name, lang, method) may exist under "default" and under one or more sessions (§11.2), and each is a separate entry.

Response (ovos.intent.describe.response):

  • On success, { "ok": true, "definitions": [ { "method": "...", "session_id": "...", "definition": {...} }, ... ] } where each definition is the §5 or §6 payload as it was broadcast. Each entry is self-identifying via its method and session_id fields; consumers MUST key on those, not on array position. The array carries one entry per registered (session_id, method) combination that passes the filters. When more than one entry is returned, ordering them by session_id with "default" first, then by method in the order keyword, template, is RECOMMENDED for stable output.
  • On unknown intent, { "ok": false, "error": "..." }.

The orchestrator MAY restrict access to introspection topics; authorization is out of scope.


11. Session-scoped registration

11.1 Every registration is session-keyed

The orchestrator keys every registration by the session_id it reads from context.session.session_id — the context field of the bus Message envelope, never from Message.data. This is a strict requirement: session_id in data would allow a producer to register intents under an arbitrary session it does not own. Reading from context means the session_id is set by the session the producer is running under, not by anything the producer chooses to assert in its payload.

No change to the registration message shape is required: session is an ordinary context field of the OVOS-MSG-1 envelope. A Message whose context carries no session, or a session with no session_id, is not malformed — OVOS-MSG-1 §4 only SHOULDs the session onto a Message. Such a registration is keyed under "default", exactly as if the producer had run under the default session. This is the same treatment the local device gets, and it keeps a minimal producer (a shell script, a test harness) able to register without constructing a session.

The full registration key becomes the quintuple (session_id, skill_id, intent_name, lang, method); the prior quadruple (skill_id, intent_name, lang, method) is the special case where session_id == "default".

Skills running on the local device register under "default" because the local device uses the default session (OVOS-SESSION-2 §5). Skills running on a remote satellite register under whatever session_id the satellite's session carries. No new message, no new field, no coordination protocol.

11.2 Inheritance — "default" is the global scope

The effective intent pool for a session X is:

pool(X) = { intents registered under "default" }
        ∪ { intents registered under session_id == X }
        − { entries excluded by session X's blacklisted_skills
            and blacklisted_intents }

Every session implicitly inherits the full "default" set. Session-scoped registrations extend the pool — they never narrow it. Narrowing is the job of the blacklisted_skills and blacklisted_intents session fields (OVOS-PIPELINE-1 §5, OVOS-SESSION-1 §3). The blacklisted_pipelines field is not part of this formula: it removes pipeline plugins from the session's pipeline, not entries from the intent pool.

If the same (skill_id, intent_name, lang, method) exists in both "default" and session X, both index entries are retained and both appear in the matching pool. The existing first-match-wins iteration (OVOS-PIPELINE-1 §6) determines which is used; the blacklist is the explicit suppression mechanism if the satellite wants to shadow a default intent.

11.3 Deregistration and session teardown

ovos.intent.deregister removes every entry matching (session_id, skill_id, intent_name, lang) across both methods — the key minus its method component, since there is no per-method deregistration (§8.2). ovos.entity.deregister removes the entry matching (session_id, skill_id, entity_name, lang); entities have no method axis. When lang is omitted, lang drops out of the match too and every language under that session and skill is removed (§8.2, §8.3).

As for every message in this specification, the session_id is read from context.session.session_id (§11.1) — never from Message.data. A deregistration arriving under the default session removes the "default"-scoped entries only — it does not remove session-scoped registrations with the same (skill_id, intent_name, lang).

ovos.intent.enable and ovos.intent.disable (§8.5) are scoped the same way: they change the enabled state of the entries matching (session_id, skill_id, intent_name, lang) across both methods, with session_id read from context.session.session_id. Disabling an intent under a satellite's session does not disable the "default" registration of the same intent for any other session, and a party holding the default session cannot disable a satellite's session-scoped entry.

ovos.skill.deregister (§8.4) removes all registrations for that skill scoped to the session read from context.session.session_id. A bridge SHOULD emit ovos.skill.deregister carrying the satellite's session in context for each satellite skill when the satellite disconnects, to clean up the satellite's session-scoped registrations from the orchestrator's index.

11.4 Pipeline plugin visibility

A pipeline plugin that wishes to support session-scoped matching SHOULD receive the effective pool for the current session's session_id when performing a match, i.e. the union described in §11.2. Plugins that do not implement session-scoped matching continue to operate against the "default" pool only and remain conformant; they simply cannot match session-specific intents.

How the orchestrator communicates the effective pool to a plugin is an implementation concern outside this specification. The normative requirement is that the pool delivered to a matching plugin for session X MUST include all entries satisfying §11.2.

11.5 Dispatch routing for session-scoped skills

When the orchestrator dispatches a session-scoped intent — one registered under a non-default session_id — the dispatch Message is a .reply() of the inbound utterance, which sets context.destination to the originating participant's source. A bridge conformant with OVOS-BRIDGE-1 §3.2 will route that dispatch back to the satellite that owns the session. No special routing protocol is needed; the existing destination-based routing (OVOS-MSG-1 §3, OVOS-BRIDGE-1 §3.2) handles it transparently.


12. Conformance

A skill (producer of registration messages) MUST:

  • emit each registration through the topic that matches its definition method (§5 for keyword, §6 for template); a single intent MAY be registered under both methods if the skill has training data of both kinds (§3.2);
  • include the identity fields of §3.2 in every registration's data;
  • set Message.context["skill_id"] to its own skill_id on every Message it emits, per §3.1;
  • set the payload skill_id equal to context.skill_id on every message of §§5–8, including deregistration, enable, and disable (§3.2) — a skill MUST NOT name another skill in these payloads;
  • conform every registration's payload to §5 (keyword), §6 (template), or §7 (entity), respectively;
  • emit ovos.intent.deregister / ovos.entity.deregister / ovos.skill.deregister to retract its registrations, paired with the local release of the handler (§9, INTENT-3 §6.1);
  • conform its underlying templates, vocabularies, and entities to OVOS-INTENT-1 and OVOS-INTENT-2.

A skill SHOULD query the manifest (§10) to confirm a registration landed; there is no acknowledgement. A skill SHOULD re-emit its registrations on observing the deployment's readiness announcement (§10) — cold-start recovery for a late-starting orchestrator or consumer — carrying the same session context as the original registration (§10). A party holding intents disabled SHOULD re-emit ovos.intent.disable at the same point (§8.5).

A pipeline plugin (consumer) MAY:

  • subscribe to any subset of the registration topics and consume what fits its matching strategy — a plugin that consumes none and matches by internal rules (e.g. an LLM persona) is also conformant.

A plugin MUST NOT index a malformed registration (§§5.3, 6.3, 7.2 — including registrations whose intent_name is reserved, §3.2) and MUST log every such rejection at WARN with skill_id, intent_name/entity_name, lang, the rejecting topic, and a one-line reason — fire-and-forget means this log is the producer's only debugging signal. An individual malformed template, sample, or entity entry within an otherwise valid registration is skipped and logged, never grounds for rejecting the registration (§§5.3, 6.3, 7.2). Matching behaviour beyond that is OVOS-PIPELINE-1's concern.

A plugin MUST NOT index or act on any registration or deregistration message (§§5–7, §§8.2–8.4) whose payload skill_id differs from context.skill_id, and MUST log the mismatch at WARN (§3.2). ovos.intent.enable / ovos.intent.disable are exempt: their payload skill_id is the target, not the sender (§3.2, §8.5).

The orchestrator MUST:

  • subscribe to every registration topic (§§5–8) and maintain the manifest — a passive index built from observed broadcasts;
  • key every manifest entry by the quintuple (session_id, skill_id, intent_name, lang, method), reading session_id from context.session.session_id of the registration message (§11.1);
  • serve ovos.intent.list and ovos.intent.describe queries against the manifest, returning the shape of §10.1 / §10.2; when the query includes a session_id, return the effective pool for that session per §11.2;
  • treat a re-registration with the same quintuple as replacement of the prior manifest entry (§8.1); other session_ids, languages, and methods for the same intent are unaffected;
  • honour ovos.intent.enable / ovos.intent.disable in the manifest (§8.5), scoped to the session_id read from context.session.session_id and applied across both methods (§11.3) — the enabled field of §10.1 reflects the latest state, which does not survive an orchestrator restart (§8.5);
  • index every non-reserved registration verbatim, without validating the payload — manifest presence records that the broadcast was observed, not that any plugin will match it (§2);
  • ignore any message of §§5–8 whose payload skill_id differs from context.skill_id, logging the mismatch at WARN (§3.2);
  • on receiving ovos.skill.deregister, remove all manifest entries for the (session_id, skill_id) pair, with session_id read from context.session.session_id (§8.4, §11.1, §11.3);
  • NOT validate, reject, route, or gate any registration message beyond the reserved-intent_name exclusion and the payload-vs-context skill_id check, both of §3.2. The orchestrator is a passive listener for the manifest, not a routing party.

The orchestrator's other responsibilities — matching, dispatch, handler lifecycle, utterance lifecycle — live in OVOS-PIPELINE-1.


See also

  • Bus Message Specification (OVOS-MSG-1) — the envelope every message here travels in, the shared identifier-component rule (§2.1.1) bounding skill_id / intent_name, the destination and session keys used throughout, and the forward / reply / response derivations.
  • Session Specification (OVOS-SESSION-1) — the wire shape of session carried on every registration broadcast.
  • Utterance Lifecycle and Pipeline Specification (OVOS-PIPELINE-1) — the orchestrator's contract: pipeline-plugin model, utterance lifecycle, match-result notification, dispatch, handler-lifecycle trio, terminal events. This spec sits next to PIPELINE-1; together they cover the full skill ↔ orchestrator ↔ plugin path.
  • Intent Definition Specification (OVOS-INTENT-3) — the intent concept, identity, definition methods, and match result that this specification carries on the bus.
  • Locale Resource Formats Specification (OVOS-INTENT-2) — the authoring file formats a skill loader expands into inline samples before emitting a registration.
  • Sentence Template Grammar Specification (OVOS-INTENT-1) — the grammar of the samples strings carried in every registration payload.
  • Bridge Specification (OVOS-BRIDGE-1) — satellite skill registration, relay of the readiness announcement, and disconnect cleanup for session-scoped registrations (§4.4).
  • Fallback Specification (OVOS-FALLBACK-1) — the same payload-skill_id-equals-context.skill_id identity rule applied to fallback registration (§3.1).