Add a Skill to a Hub
Open a hub’s Skills tab to install a skill on its attached runtime group. Every hub sharing that group receives the change. Installation normally applies live without restarting the runtime; follow its status to confirm it succeeded.
The same change is one call from the API, all seven managed SDKs, the thalovant CLI, and the MCP server, so an agent or a script can add a skill to a hub too.
At a Glance
Section titled “At a Glance”| Question | Simple answer |
|---|---|
| What is this about? | Installing a skill on the runtime group selected through a hub. |
| Where do I start? | Open the hub → Skills tab → Add skill. |
| What is done? | The row shows installed and the hub answers to the skill’s intents. |
Hub Skills And Skill Sets
Section titled “Hub Skills And Skill Sets”A skill set is a bundle of skills that several hubs share. Change the skill set and every hub on it changes.
A hub-addressed skill call resolves the hub’s attached runtime group and changes that group’s skill attachment. Adding, updating or removing it affects every hub served by the group. Use a separate runtime group when hubs need different installed skills.
A hub can start with no skills at all and gain them one at a time. That makes a new hub a blank device you fill in as you go, rather than one that has to pick a full skill set on day one.
Changes apply live. The hub picks up an added, updated, or removed skill in about 15 seconds and does not restart.
Before You Start
Section titled “Before You Start”Confirm:
- the hub belongs to a runtime group (without one, there is no Skills tab yet);
- your workspace is on a paid plan, because adding, updating, and removing hub skills are paid actions;
- for the API, SDKs, CLI, or MCP, you have an API token with
hubs:write, orhubs:inspectto list only (hubs:readincludes it); a restricted token must cover every hub sharing the affected runtime group; - you know the hub’s id, not its slug, when you work from code.
Add A Skill In The App
Section titled “Add A Skill In The App”- Open the hub from Hubs.
- Open the Skills tab.
- Choose Add skill.
- Pick the skill and a version. Latest is the default.
- Confirm. The row shows the install status until it settles on installed.
Each row has Update and Remove. Update lets you pick a version; remove takes the skill out of the attached skill set, affecting every hub that uses it. Both normally apply while the runtime keeps running and show their progress on the row.
From The API And SDKs
Section titled “From The API And SDKs”Every call addresses the hub by its id. Slugs are not accepted on these routes. Install, update, and remove return HTTP 202 with an operation_id; the change is then applied on the hub while you poll or wait.
Call The API
Section titled “Call The API”List the skills on a hub:
curl --fail-with-body \ -H "Authorization: Bearer $THALOVANT_API_TOKEN" \ "https://api.thalovant.com/v1/hubs/$HUB_ID/skills"{ "hub_id": "8d1c2f4e-5b6a-4c7d-9e8f-0a1b2c3d4e5f", "runtime_group_id": "3f9a7b21-6c4d-4e8f-a1b2-c3d4e5f60718", "observed_at": "2026-09-09T14:02:11Z", "source": "runtime", "data": [ { "skill": "skill-weather", "title": "Weather", "marketplace_skill_id": "b7e2c9d0-1f3a-4b5c-8d6e-7f8091a2b3c4", "package_name": "thalovant-skill-weather", "source_type": "catalog", "install_source": "marketplace", "version": "1.1.0", "version_pin": "1.1.0", "installed_version": "1.1.0", "observed_version": "1.1.0", "previous_version": "1.0.4", "latest_version": "1.2.0", "available_version": "1.2.0", "update_available": true, "changelog": "Adds hourly forecasts.", "active": true, "state": "installed", "last_transition_at": "2026-09-09T14:02:11Z" } ]}The rows sit under data, an empty array when nothing is installed this way. observed_at and source say when and from where the list was last observed. The envelope and each row also carry the runtime’s phase, message, and last-error fields, left out of the sample above; they are null when the runtime has nothing to report. installed_version is what is running, version_pin is what you asked for, and update_available is true when a newer version exists.
Each row’s state is one of:
| State | Meaning |
|---|---|
pending |
A change is in progress. Install, update, and remove all show this until the change lands. |
installed |
The skill runs at the requested version. |
failed |
The runtime could not install or update the skill. Its last-error field says why. |
removing |
The skill is being taken off. |
drifted |
The running version differs from the one requested. |
quarantined |
The runtime disabled the skill after repeated failures. |
unmanaged |
The skill is running but is not managed through this route. |
Install a skill. version is always sent and defaults to latest:
curl --fail-with-body -X POST \ -H "Authorization: Bearer $THALOVANT_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"skill": "skill-weather", "version": "latest"}' \ "https://api.thalovant.com/v1/hubs/$HUB_ID/skills"{ "operation_id": "op_01J9Q0S2M5H8ZC3VQK7T1XB4RD", "hub_id": "8d1c2f4e-5b6a-4c7d-9e8f-0a1b2c3d4e5f", "runtime_group_id": "3f9a7b21-6c4d-4e8f-a1b2-c3d4e5f60718", "skill": "skill-weather", "version": "latest", "previous_version": null, "state": "installing"}Posting a skill that is already installed at another version performs an update, and previous_version then carries the version being replaced. Posting the same version answers HTTP 409.
Update to an explicit version. version is required here:
curl --fail-with-body -X PATCH \ -H "Authorization: Bearer $THALOVANT_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"version": "1.2.0"}' \ "https://api.thalovant.com/v1/hubs/$HUB_ID/skills/skill-weather"Remove the skill:
curl --fail-with-body -X DELETE \ -H "Authorization: Bearer $THALOVANT_API_TOKEN" \ "https://api.thalovant.com/v1/hubs/$HUB_ID/skills/skill-weather"{ "operation_id": "op_01J9Q0T7NA2KX4D9RW6PZ8E3HM", "hub_id": "8d1c2f4e-5b6a-4c7d-9e8f-0a1b2c3d4e5f", "runtime_group_id": "3f9a7b21-6c4d-4e8f-a1b2-c3d4e5f60718", "skill": "skill-weather", "version": null, "previous_version": "1.2.0", "state": "removing"}Update answers with the same body shape and "state": "updating". Each write has its own operation_id. Poll that operation until it is ready, failed, or timed_out:
curl --fail-with-body \ -H "Authorization: Bearer $THALOVANT_API_TOKEN" \ "https://api.thalovant.com/v1/operations/$OPERATION_ID"ready means the hub converged: the skill is installed after an install or update, and gone after a remove. See Operations for the full status sequence.
Use An SDK Or The CLI
Section titled “Use An SDK Or The CLI”All seven managed SDKs support hub skill management and history. Use Python 0.6.0, Node.js 0.4.0, Go and Rust 0.6.0, or Kotlin, Swift and .NET 0.4.0. Pass wait to poll every 2 seconds with a 120-second default polling budget. No new status read starts at or after that deadline; the polling deadline does not cancel a request already in flight. A failed status read retains the accepted operation ID, so resume polling that operation instead of submitting the write again. The thalovant CLI ships with the Python SDK and reads THALOVANT_API_TOKEN.
import os
from thalovant import ThalovantControlPlane
api = ThalovantControlPlane(access_token=os.environ["THALOVANT_API_TOKEN"])
skills = api.list_hub_skills("hub-id") # HubSkillListprint(skills.source, skills.observed_at)for skill in skills.data: print(skill.skill, skill.installed_version, skill.state, skill.update_available)
result = api.install_hub_skill("hub-id", "skill-weather", version="latest", wait=True)print(result.state) # installed
api.update_hub_skill("hub-id", "skill-weather", version="1.2.0", wait=True)api.remove_hub_skill("hub-id", "skill-weather", wait=True)import { ThalovantControlPlane } from "@thalovant/sdk";
const api = new ThalovantControlPlane(undefined, { accessToken: process.env.THALOVANT_API_TOKEN,});
const skills = await api.listHubSkills("hub-id"); // { data, source, observed_at, ... }console.log(skills.source, skills.observed_at);for (const skill of skills.data) { console.log(skill.skill, skill.installed_version, skill.state, skill.update_available);}
const result = await api.installHubSkill("hub-id", "skill-weather", { version: "latest", wait: true });console.log(result.state); // installed
await api.updateHubSkill("hub-id", "skill-weather", { version: "1.2.0", wait: true });await api.removeHubSkill("hub-id", "skill-weather", { wait: true });export THALOVANT_API_TOKEN="tvpat_..."
thalovant skills list --hub <hub-id>thalovant skills add --hub <hub-id> skill-weather --version latest --waitthalovant skills update --hub <hub-id> skill-weather --version 1.2.0thalovant skills remove --hub <hub-id> skill-weather
# Machine-readable output for scripts:thalovant --json skills list --hub <hub-id>The list call returns the same envelope as the route: HubSkillList in Python and { data, source, observed_at, ... } in Node.js, with the rows under data. Each write returns the accepted operation with operation_id, skill, version, previous_version, and state. Without wait, install, update, and remove return at once, and you poll the operation with get_operation or getOperation. Go, Rust, Kotlin, Swift, and .NET provide equivalent calls and resume helpers; see the language guides. Read shared-runtime history with list_hub_skill_history(hub_id, limit=50) in Python or listHubSkillHistory(hubId, { limit: 50 }) in Node. The limit is an integer from 1 to 200.
Use The MCP Server
Section titled “Use The MCP Server”From MCP 0.1.23, the MCP Server exposes the same four actions as tools: thalovant_list_hub_skills, thalovant_install_hub_skill, thalovant_update_hub_skill, and thalovant_remove_hub_skill. All four take hubId; the three write tools also take skill, plus version, wait, and timeoutMs where the action needs them. The three write tools need hubs:write and are hidden in read-only mode. MCP 0.2.0 uses Node SDK 0.4.0 and adds the read-only thalovant_list_hub_skill_history tool with an optional integer limit from 1 to 200. Polling failures preserve the accepted operation ID.
If Something Fails
Section titled “If Something Fails”Errors come back as RFC 7807 problem JSON with code and message at the root of the body. The Python and Node.js SDKs put both into the error text as HTTP <status>: <message> (<code>), for example HTTP 409: Skill version already installed. (skill_version_already_installed); there is no separate code attribute on the error yet.
| Response | What it means | What to do |
|---|---|---|
| HTTP 403 | The token does not carry hubs:write. The scope check runs before the plan check, so a free-plan token always lands here, never on 402. |
Mint a token with hubs:write on the API Tokens page. |
| HTTP 402 | The scope is right but the workspace is on the free plan. | Upgrade the workspace, then retry. Listing still works. |
HTTP 404, hub_without_runtime_group |
The hub is not in a runtime group yet. | Add it to a runtime group, then retry. |
HTTP 404 with no code |
The id is unknown, or the skill is not installed there. | Check you passed the id rather than the slug, then list the installed skills. |
HTTP 409, skill_version_already_installed |
The hub already has that skill at that version. | Nothing to do, or pick a different version. |
| HTTP 422 | latest could not be resolved to a version, or the version string is invalid. |
Pass a version the skill publishes, such as 1.2.0. |
State failed |
The runtime could not install or update the skill. | Read the error on the row, the row’s last-error field, or the operation’s error_message; fix the cause and retry. The rest of the hub keeps running. |
A skill that stays in pending for much longer than 15 seconds usually means the hub itself is not healthy. Check its live state on Hubs before retrying.
The Skill Is Ready When
Section titled “The Skill Is Ready When”- the row shows installed and the version you picked;
GET /v1/hubs/$HUB_ID/skillslists it underdatawith"state": "installed";- the operation reports
ready; - the hub answers to the skill’s intents.
Publish A Version Of Your Skill
Section titled “Publish A Version Of Your Skill”To install a skill you wrote this way, it needs a published version. Skill authors publish one with a multipart POST to /v1/marketplace/skills/{id}/versions, with the fields version and channel, an optional changelog, and the built wheel as the wheel file part:
curl --fail-with-body -X POST \ -H "Authorization: Bearer $THALOVANT_API_TOKEN" \ -F "version=1.2.0" \ -F "channel=stable" \ -F "changelog=Adds hourly forecasts." \ -F "wheel=@dist/thalovant_skill_weather-1.2.0-py3-none-any.whl" \ "https://api.thalovant.com/v1/marketplace/skills/$SKILL_ID/versions"It answers HTTP 201 with skill_id, package, version, channel, sha256, wheel_path, and index_url. Thalovant skill repositories that use the shared release workflow publish this way when a tag such as v1.2.0 is pushed. The SkillKit starter creates a test workflow; add publishing separately when your project is ready.
To write your first skill, follow the Pocket Quiz tutorial. To install it directly from your own Git repository or a Python package, follow Add Your Own Skill.
Next Step
Section titled “Next Step”For creating the hub and its runtime from code, see Provision Hubs, and for how shared skill sets work, see Skills.
Skill attachments belong to the selected hub’s runtime group. Every hub sharing that group receives the change; a restricted token must cover all served hubs.