# What Can My Hub Be Asked?

> List every intent a hub answers, with the sentences a person says, from a connected client and without an API token.

Source: https://docs.thalovant.com/concepts/hub-intents/

---
A connected client can ask its hub what it can be asked. The hub answers with every intent its skills registered, per language, and the sentences a person says to reach each one.

The answer travels over the client's own session. No API token, sign-in, or workspace role is involved. A voice client, a kiosk, an installer, or an agent can show a person what to say, or confirm that a new skill answers, with nothing more than its connection.

## What You Get Back

An intent is one thing a skill can do, `current.weather` for instance. Ask a hub and it returns every intent it has, grouped by skill, with the sentences that reach each one, in each language. Any connected client can ask, as long as its connection may publish `ovos.intent.list` (and `ovos.intent.describe`, when the sentences have to be fetched separately). It costs nothing beyond the connection itself. A refusal is a policy error naming the message type, and it comes back at once rather than as a timeout; the fix is the connection's message types in the dashboard.

## What the Hub Returns

The inventory groups intents by skill. Each intent carries:

| Field | Meaning |
| --- | --- |
| `skill_id` | The skill that registered the intent, such as `thalovant-skill-weather.thalovant`. |
| `name` | The intent name, such as `current.weather`. The full id is `skill_id:name`. |
| `engine` | How the hub matches it. `padatious` matches whole sentences; `adapt` matches keyword sets. |
| `enabled` | Whether the hub answers to it right now. |
| `phrases` | The sentences that reach it, keyed by language. |

Sentences appear as the skill wrote them. A word in braces is a slot the person fills in: `what is the weather in {location}` matches "what is the weather in Montréal". Keyword intents carry keyword sets rather than sentences, so they list without phrases.

A weather skill that speaks English and French looks like this in the JSON form the SDKs and the `thalovant intents --json` command print:

```json
{
  "languages": ["en-us", "fr-fr"],
  "source": "intent-manifest",
  "denied": [],
  "skills": [
    {
      "skill_id": "thalovant-skill-weather.thalovant",
      "languages": ["en-us", "fr-fr"],
      "intents": [
        {
          "id": "thalovant-skill-weather.thalovant:current.weather",
          "skill_id": "thalovant-skill-weather.thalovant",
          "name": "current.weather",
          "engine": "padatious",
          "enabled": true,
          "phrases": {
            "en-us": ["what is the weather", "what is the weather in {location}", "how is it outside"],
            "fr-fr": ["quel temps fait-il", "quelle est la météo à {location}", "quelle est la météo"]
          }
        }
      ]
    }
  ]
}
```

`source` says how the inventory was read. `intent-manifest` carries sentences per language. `engine-manifests` is the names-only fallback described below, and `denied` names the query that triggered fallback.

## Languages

Ask for each language you want to show. Language tags compare without regard to case, and `_` and `-` count as the same character: `fr-fr`, `fr_FR`, and `fr-FR` name one language. The hub answers with the tag it stores, so a request for `fr-fr` can come back labeled `fr-FR`.

A skill lists only in the languages it ships. A skill with English sentences and no French ones appears in the English list and has no French phrases. The hub registers only the languages its runtime loads. If a whole language is missing, check the primary and secondary languages in [Runtime](/concepts/runtime/).

## What the Connection Needs

The hub answers two queries, and the connection needs only the ones its client sends:

| Message type | What it does | When the client sends it |
| --- | --- | --- |
| `ovos.intent.list` | Lists every registered intent for one language. | Always. |
| `ovos.intent.describe` | Returns one intent's registration, including its sentences. | Only when the client has to ask for the sentences separately. |

A client that only wants to know which intents a hub registered needs `ovos.intent.list` alone: it gets the names, engines, and enabled state, and no sentences. Every SDK asks for the sentences unless you turn that off, and in Python the switch is `client.intents(["en-us"], describe=False)`.

Asking for the sentences doesn't always mean sending `ovos.intent.describe`. The SDKs ask the hub to attach each intent's definition to the listing itself, with `include_definitions`. A hub whose runtime honors that answers the listing with the sentences already in it, no describe is ever sent, and the connection never needs that second type. The SDK falls back to describing intent by intent only when the listing came back without definitions, and that's when the connection needs `ovos.intent.describe` too.

Which of the two happens depends on the runtime, and today no shipping runtime attaches them: OpenVoiceOS keeps its listing free of definitions by design, and Thalovant hubs run a runtime built from those same sources. A runtime that doesn't recognize `include_definitions` ignores it and answers the plain listing, and the SDK then describes intent by intent. So in practice the connection needs both types. Allow both unless you've measured otherwise; that's what Thalovant's own connections do.

Both are read-only. They reveal only intents the connection can already trigger, so allowing them adds no privilege. Connections that Thalovant creates for SDK clients and Thalovant Voice include both by default.

To check or change them, open **Connections** in the dashboard, select the connection, and open its access settings. The list of message types the connection may publish (`allowedTypes` in the client spec) must contain each type the client sends. Add them when a connection was created before this feature shipped, or when its access was narrowed by hand, then reconnect the client.

  Access rules linked to the client can also allow or block message types. If the connection lists both types and the hub still refuses, review the [access rules](/concepts/access-rules/) linked to that client for a blocked action that names them.

## What a Refusal Looks Like

A connection that may not publish one of the types gets no reply. Instead the hub answers `hive.policy.denied` at once, naming the message type and the types the connection may publish:

```json
{
  "denied_type": "ovos.intent.list",
  "code": "acl_disallowed_type",
  "reason": "ovos.intent.list not in allowed_types",
  "data": {
    "msg_type": "ovos.intent.list",
    "allowed": ["recognizer_loop:utterance", "speak"]
  }
}
```

The SDKs turn that answer into a typed policy error the moment it arrives, rather than waiting for a timeout. The error names the refused type and carries `code`, `reason`, and the `allowed` list. In Python it's `ThalovantPolicyDeniedError`, and its message tells you where to fix it:

```text
The hub refused 'ovos.intent.list': ovos.intent.list not in allowed_types.
Allow this connection to publish 'ovos.intent.list' in the dashboard's connection settings.
```

## When the Hub Answers With An Error

A refusal isn't the only unhappy answer. The hub can also answer a query it accepted with `ok: false` and an error of its own, and the two queries don't mean the same thing by it.

When `ovos.intent.list` answers with an error, the listing failed. The hub hasn't said what it registered, so the SDKs raise their runtime error carrying the hub's error text. A listing that failed isn't an empty hub, and reporting it as no intents would show a person a device that can do nothing. In Python it reads:

```text
ovos.intent.list failed: intent manifest unavailable
```

Ask again once the hub is healthy. If it keeps failing, check the hub's live state and its runtime on [Hubs](/concepts/hubs/).

When `ovos.intent.describe` answers with an error, that's a normal, empty answer: the hub doesn't know that registration. The intent stays in the inventory with its name, engine, and enabled state, and has no sentences. Nothing is raised, and the rest of the inventory is unaffected.

## Names Only Fallback

When the hub refuses `ovos.intent.list` or doesn't answer before its timeout, the SDKs fall back by default to two older queries, `intent.service.adapt.manifest.get` and `intent.service.padatious.manifest.get`. These return intent names only, in the form `skill_id:intent_name`, with no language and no sentences.

A names-only result says so. `source` is `engine-manifests`, `denied` lists `ovos.intent.list`, and no intent carries phrases. It's enough to confirm that a skill registered; it isn't enough to show people what to say. When the fallback queries are refused as well, the policy error is raised.

The [current high-level SDKs](/developers/sdks/#supported-versions) all use this fallback for a silent listing. The legacy `denied` field names the unavailable query; it doesn't prove a policy refusal. If the engine queries also time out, the operation raises the SDK timeout error. Replies and refusals carrying another request ID can't complete this request.

Turn the fallback off when your client should fail rather than degrade. In Python that's `client.intents(fallback=False)`.

## Fallback Handlers And Language Availability

Intent phrases don't describe every way a hub can answer. The SDKs can add an optional inventory probe using `ovos.skills.fallback.list` for fallback handlers and read `ovos.skills.fallback.list.response`. Allow that query on the connection when you want this information. The probe uses at most 1.5 seconds across connection, send, and reply wait; caller cancellation still stops the operation.

`fallbacks_known` (or its language-native name) distinguishes a confirmed empty list from unknown support. Refusal, silence, malformed reply envelopes, and an explicit failed response leave support unknown. An accepted fallback array produces a known list of usable handlers; invalid rows are skipped. `may_answer(lang)` is conservative: it's false only when fallback discovery succeeded with no handlers and no enabled intent has phrases for that language. Unknown support isn't evidence that the hub can't answer.

Python, Node, Kotlin, Swift, and .NET add this information to their inventory. Go and Rust preserve their existing inventory type and expose an additive capability wrapper through `IntentsWithCapabilities` and `intents_with_capabilities`. Embedded C supplies fallback request frames and response classification; the caller implements the probe, result collection, and availability decision.

## Where It Helps

  
    Show a "what can I say" screen in the person's language, built from the hub rather than typed by hand.
  
  
    Confirm that a newly installed skill answers, in every language the runtime loads, before handing the hub over.
  
  
    Let an agent read what the hub can do before it chooses a phrase, instead of guessing.
  
  
    Assert that the hub registered the intents your skill ships. See [Add Your Own Skill](/manage/add-your-own-skill/).
  

## Read It From Code

The Python SDK 0.4.36 and newer reads the inventory with `client.intents([...])`, and the `thalovant intents` command prints it:

```bash
thalovant --identity _identity.json intents --lang en-us --lang fr-fr
```

See [List What the Hub Can Be Asked](/developers/sdks/python/#list-what-the-hub-can-be-asked) in the Python SDK guide. The Node.js, Go, Rust, Kotlin, Swift, and .NET SDKs have the same three calls under their own spelling, and the embedded C library builds and reads the two frames. Each [SDK page](/developers/sdks/) names them and gives a working example.

## Show Examples As Sentences

The current managed SDKs and MCP can render raw intent patterns as sentences.
Enable the sentence option on the example helper to remove optional wording,
choose alternatives, insert locale sample slots, and add capitalization and
punctuation. Your explicit slot values override the samples. The original intent
patterns remain available.

Examples prefer complete phrases over prefixes and slot patterns. Rendered limits
count unique nonempty results. Regional matching can use a nearby registered
locale; it doesn't translate a skill into a language it doesn't support.
Unknown locales retain bare text. See the [SDK guides](/developers/sdks/) for
versions and examples; Python requires its optional listing extra.

## If The Listing Is Refused

| Symptom | Check first |
| --- | --- |
| A policy error names `ovos.intent.list` or `ovos.intent.describe` | Add that message type to the connection's message types in the dashboard, then reconnect the client. |
| Names but no sentences, and `source` is `engine-manifests` | The connection may allow only fallback queries, or the listing received no reply. Check that the runtime implements the listing and allows `ovos.intent.list`; allow `ovos.intent.describe` when sentences are not attached. |
| Listing failed with an error from the hub | The hub could not read its intent manifest. It is not an empty hub. Check its live state on [Hubs](/concepts/hubs/), then ask again. |
| One intent has no sentences | It is a keyword intent, the hub does not know that registration, or its description did not arrive in time. Raise the timeout or describe that intent on its own. |
| A whole language is missing | Check the runtime language configuration and fallback discovery. Missing phrases alone do not rule out an answer. |
| A skill is missing | Check its install state and live status on [Skills](/concepts/skill-sets/). |
| The call times out with no policy error | Check authenticated connection readiness, whether the runtime implements the query, and its response budget. See [Connections](/concepts/clients/). |

## The Inventory Looks Right When

The inventory is ready to show people when:

- every installed skill appears under the `skill_id` you expect;
- each intent has sentences in every language the runtime loads;
- `source` is `intent-manifest` and `denied` is empty;
- the example sentences read like something a person would say.

Read it with the Python SDK
Review access rules
