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
destinationrouting key, thesessioncarrier, and theforward/reply/responsederivations; - 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
.entityvalue-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 lifecycle —
sessionis 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:
| Field | Type | Required | Source |
|---|---|---|---|
skill_id | string | yes | INTENT-3 §3 — assistant-unique. |
intent_name | string | yes | INTENT-3 §3 — unique within the skill. |
lang | string | yes | BCP-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.
| Topic | Direction | Purpose | § |
|---|---|---|---|
ovos.intent.register.keyword | skill → bus (broadcast) | Register a keyword intent (INTENT-3 §4). | §5 |
ovos.intent.register.template | skill → bus (broadcast) | Register a template intent (INTENT-3 §5). | §6 |
ovos.intent.deregister | skill → bus (broadcast) | Remove one intent. | §8 |
ovos.intent.enable | skill → bus (broadcast) | Re-arm a previously disabled intent. | §8.5 |
ovos.intent.disable | skill → bus (broadcast) | Suppress an intent without removing its definition. | §8.5 |
ovos.entity.register | skill → bus (broadcast) | Register an .entity value-set hint (INTENT-3 §5.2). | §7 |
ovos.entity.deregister | skill → bus (broadcast) | Remove one entity. | §8 |
ovos.skill.deregister | skill → bus (broadcast) | Remove all intents and entities for one skill_id. | §8 |
ovos.intent.list | observer → orchestrator | Query registered intents (introspection; served by the orchestrator). | §10 |
ovos.intent.describe | observer → orchestrator | Query 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:
| Field | Type | Required | Meaning (per INTENT-3 §4.2) |
|---|---|---|---|
required | array of vocabulary descriptors | no (absent = []) | Every required vocabulary MUST occur in the utterance. |
optional | array of vocabulary descriptors | no (absent = []) | Captured if it occurs; absence does not prevent a match. |
one_of | array of arrays of vocabulary descriptors | no (absent = []) | Each inner array is one group; at least one member of each group MUST occur. |
excluded | array of vocabulary descriptors | no (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_nameMUST NOT be one reserved by another spec (§3.2; the authoritative registry is OVOS-PIPELINE-1 §7.3). - The combined
requiredandone_oflists MUST NOT both be empty — an intent with onlyoptionalandexcludedhas 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
namewithin the same role are not malformed: a consumer MUST merge them into one vocabulary whosesamplesis the union of theirs. The union is what the cross-role uniqueness check above sees, and what the match result reports under thatname. - Every vocabulary descriptor MUST carry a non-empty
samplesarray (§5.1). - A vocabulary descriptor's
samplesMUST 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:
| Field | Type | Required | Meaning |
|---|---|---|---|
samples | array of strings | yes | OVOS-INTENT-1 templates with named slots (INTENT-1 §3, §5). |
blacklist | array of strings | no | Slot-free phrases (INTENT-2 §4.3) whose occurrence suppresses the match (INTENT-3 §5.5). |
required_slots | array of strings | no | Slot 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_nameis reserved by another spec (§3.2); samplesis missing or empty;- no template in
samplesis both parsable as OVOS-INTENT-1 §3 grammar and expands to at least one non-empty sample (OVOS-INTENT-1 §3.6); required_slotsnames a slot that is not declared by any valid template insamples(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:
| Field | Type | Required | Meaning |
|---|---|---|---|
entity_name | string | yes | Unique within the skill. By convention matches the slot name a template intent references. |
samples | array of strings | yes | Slot-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 eachdefinitionis the §5 or §6 payload as it was broadcast. Each entry is self-identifying via itsmethodandsession_idfields; 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 bysession_idwith"default"first, then bymethodin the orderkeyword,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 ownskill_idon every Message it emits, per §3.1; - set the payload
skill_idequal tocontext.skill_idon 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.deregisterto 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), readingsession_idfromcontext.session.session_idof the registration message (§11.1); - serve
ovos.intent.listandovos.intent.describequeries against the manifest, returning the shape of §10.1 / §10.2; when the query includes asession_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.disablein the manifest (§8.5), scoped to thesession_idread fromcontext.session.session_idand applied across both methods (§11.3) — theenabledfield 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_iddiffers fromcontext.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, withsession_idread fromcontext.session.session_id(§8.4, §11.1, §11.3); - NOT validate, reject, route, or gate any registration message
beyond the reserved-
intent_nameexclusion and the payload-vs-contextskill_idcheck, 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, thedestinationandsessionkeys used throughout, and theforward/reply/responsederivations. - Session Specification (OVOS-SESSION-1) — the wire shape of
sessioncarried 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
samplesstrings 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_ididentity rule applied to fallback registration (§3.1).