Introduction
Custom Abilities are the cornerstone of extending OpenHome’s functionality. They allow developers to:- Add personalized features to AI agents.
- Integrate third-party APIs for dynamic interactions.
- Customize logic for enhanced user engagement.
- Structuring and registering an Ability.
- Using
CapabilityWorkerfor seamless I/O management. - Examples showcasing how to create powerful custom Abilities.
Adding an Ability
File Structure
Each Ability resides in its folder and requires amain.py file to define the logic.
Example File: main.py
Here’s a basic template for building a new Ability:
Key Components
#{{register_capability}}: is essential.call: Executes the Ability’s logic when triggered.
Custom API Keys (Third-Party Services)
If your Ability needs credentials for external services (for example OpenAI, SendGrid, or Twilio), configure those keys in the dashboard and read them at runtime usingget_api_keys("key_name").
Setup Flow
For Developers
Step 1 — Declare keys You can declare a custom API key in either of two ways:-
While creating/editing the Ability: Under Ability Behavior → API Keys, add each key by name and include a provider URL (required). The key value is not set here; values are always managed from Settings → API Keys.

-
From Settings: Go to Settings → API Keys → Third-party Keys and create the key directly, then link it to an Ability by creating or editing one in the Ability editor.


Important: Untagged keys will not trigger an install-time prompt for users.Step 3 — Read values at runtime
For Users
When installing an Ability from the marketplace, a pop-up lists all required keys with direct links to each provider. Users can enter values immediately or skip and add them later from Settings → API Keys. The Ability will not work until all required keys have values set.
Security note: Never hardcode secrets in your Ability code. Always read keys at runtime.
Reading Linked Account Tokens with get_token()
For providers that support OAuth-based account linking, OpenHome stores the user’s access token on the user’s behalf when they connect the account from Settings → Linked Accounts in the Dashboard. Inside an Ability, use get_token() to retrieve that token and call the provider’s API on behalf of the user without handling the OAuth flow yourself.
This is different from Custom API Keys above: API keys are values the user pastes in for arbitrary third-party services, while get_token() returns an OAuth access token managed by OpenHome for a supported provider.
Signature
get_token() is synchronous — do not await it.
Parameters
Returns
- A non-empty
straccess token if the user has linked the requested provider. Noneor an empty value if the provider is not linked for the user. Always guard the return value before using it.
Example: token lookup with a connect-account fallback
The recommended pattern is to read the token at the start ofrun() and, if it is missing, speak a clear instruction telling the user where to connect the account, then exit cleanly.
"slack", "discord", "microsoft", "tesla") and the spoken instruction to match the account your Ability needs.
Reference Abilities Using get_token()
The following community Abilities use get_token("google") in production. Review them for end-to-end patterns covering token retrieval, API calls, and error handling:
- Morning Brief — reads Gmail and Google Calendar to generate a daily summary.
- Gmail Voice Assistant — voice-driven Gmail inbox management.
- Google Calendar — view, create, and update calendar events by voice.
- Google Tasks — manage Google Tasks lists by voice.
Making HTTP Requests
When your Ability calls an API, always route the request throughself.worker.session_tasks. This lets the SDK manage the request for you: it paces outbound calls and smooths out latency, so a slow or heavy API doesn’t block your Agent or affect other Abilities.
Do not call requests, httpx, or aiohttp directly — use the matching session_tasks helper instead. This applies to code you write by hand and to code generated by an AI assistant.
The simplest option is get(). It works just like the requests library: give it a URL and read the response with .json().
get() accepts the usual arguments (params, headers, timeout, auth) and returns a standard requests.Response.
Which helper should I use?
Most Abilities can just useget(). You only need the others if you want an async call or prefer a different library. When you do, the choice depends on how your method is written:
- Normal
defmethod → use a sync helper and call it directly, with noawait:get()orhttpx_get(). async defmethod (for example, a background-daemon loop) → use an async helper andawaitit:get_async(),httpx_get_async(), oraiohttp_get_async(). Async helpers don’t freeze the rest of your Ability while the request is running, so prefer them whenever your method is already async.
requests has no async version of its own, so get_async() is there to give you a requests-style request that you can await.
Every library has the same helpers, so pick the one you’re comfortable with:
Examples
With
requests and httpx, you read the response directly with .json(), .text, and .status_code. With aiohttp, reading the body is also asynchronous, so use await resp.json() and await resp.text().Understanding CapabilityWorker
The CapabilityWorker class simplifies I/O interactions, enabling:
- Speech synthesis: Using text-to-speech (TTS).
- Listening for user input: Capturing and processing responses.
- Running interaction loops: Supporting conversational flows.
CapabilityWorker Quick Reference
Use these functions directly onself.capability_worker.
Conversation
Speak text to the user using the configured TTS.True or False.
city, region, country, postal, timezone, ip, loc, and more.
Text Generation
Return plain text from the model (no speech).File Helpers
Read a file.Note: Storage Scope Usage
- Use
in_ability_directory=Falsefor persistent user-level storage shared across abilities.- Use
in_ability_directory=Truefor ability-scoped data that should remain isolated within the ability session.
Context Storage (Key-Value)
Audio and Streaming
WebSocket / Device Actions
Using Specific Voice IDs for Text-to-Speech
The CapabilityWorker class supports the use of specific Voice IDs for text-to-speech (TTS) functionality. This allows you to customize the voice used for speech synthesis by specifying a Voice ID from the provided list.Available Voice IDs
You can use any of the following Voice IDs for TTS:text_to_speech Function
Thetext_to_speech function converts the provided text into speech using the specified Voice ID and streams it to the user via WebSocket.
Parameters
text (str): The text to be converted into speech.voice_id (str): The Voice ID to be used for speech synthesis.
Exiting an Ability
An Ability returns control to the Agent when it finishes its work by callingresume_normal_flow(). Beyond that normal completion, there are two ways to leave a Skill Ability on demand: the built-in exit phrases the SDK recognizes, and exit handling you add in your own code.
Built-in exit phrases
While a Skill Ability is active, the SDK recognizes a fixed set of spoken exit phrases. Speaking any of them ends the current Ability and returns control to the Agent’s conversation flow. This is the same outcome as callingresume_normal_flow(), but initiated by the user rather than by the Ability. This is built in and requires no additional code, so a user always has a way out of an Ability, including one that is mid-conversation or waiting for input.
The recognized exit phrases are:
- “open home exit”
- “openhome exit”
- “open home quit”
- “openhome quit”
- “open home, quit”
- “open home, exit”
- “exit open home”
- “quit open home”
- “exit, open home”
- “quit, open home”
Note: Built-in exit phrases apply to Skill Abilities.
Handling exit in your own code
You can also end an Ability from your own code. Detect an exit condition in the user’s input, such as your own stop words, and callresume_normal_flow() to return control to the Agent.
Advanced CapabilityWorker Functions
Audio Processing Functions
TheCapabilityWorker provides comprehensive audio handling capabilities:
play_audio: Play audio content directly or file objectsplay_from_audio_file: Play audio files stored in the capability directorysend_audio_data_in_stream: Stream processed audio data over WebSocket
Text Generation Functions
Multiple options for text generation:text_to_text_response: Standard text generation with history and system promptsgenerate_ttt_using_openrouter: Alternate text generation using OpenRouterllm_search: Web-search-backed short answerllm_tools: Tool-calling with the model
Streaming and Communication
Advanced communication features:stream_initandstream_end: Manage audio streaming sessionssend_data_over_websocket: Send custom data over WebSocketsend_interrupt_signal: Interrupt ongoing output and hand control back to user inputsend_agent_message_without_audio: Send a text reply without TTSsend_devkit_action: Trigger a Devkit actionsend_devkit_mqtt_action: Trigger a DevKit MQTT action to control a smart device. See Controlling MQTT Devices for the full reference and an example Ability.
Context and Session Helpers
get_timezone: Read the current user’s timezone for local-time-aware behaviorget_token: Read the access token of a user-linked provider account ("google","slack","discord","microsoft","tesla"). See Reading Linked Account Tokens withget_token()for the full reference and arun()example with a fallback message when the account isn’t connected.get_api_keys: Read custom API key values from Settings → API Keys by key nameget_full_message_history: Read full session message history for context-aware responsesupdate_personality_agent_prompt: Append context/instructions to the Agent personality promptcreate_key/update_key/delete_key: Manage structured key-value context storageget_single_key/get_all_keys: Read one or all stored context entries
Recording and Local Audio
get_audio_recording: Load the latest user recording bytesget_audio_recording_length: Duration in seconds for the latest recordingflush_audio_recording: Clear the current recording before a new captureplay_from_audio_file: Play an audio file stored in the Ability directory
Session Task Utilities (replace raw asyncio usage)
To ensure Abilities run within the agent’s managed lifecycle, avoid using rawasyncio helpers directly.
- Use
self.worker.session_tasks.sleep(seconds: float)instead ofasyncio.sleep(...):
Background Daemon Entry Point (background.py)
Background daemons run automatically when a session starts. Use a separate background.py file with this entry signature:
- Keep daemon logic inside a continuous
while Trueloop. - Use
await self.worker.session_tasks.sleep(...)between cycles. - Do not call
resume_normal_flow()inside daemon loops. - Call
await self.capability_worker.send_interrupt_signal()before daemon speech/audio.
Example 1: Basic Capability
This Ability creates a daily life advisor that:- Asks the user for a problem: Initiates a conversation to gather user input.
- Provides advice: Offers a solution based on user input.
- Collects feedback: Captures user satisfaction with the advice.
Code
Key Functions
speak: Introduces the advisor and provides the solution.user_response: Captures user input (e.g., their problem).run_io_loop: Combines speaking the solution and listening for feedback.resume_normal_flow: Resumes the agent’s default workflow after interaction.
Example 2: Weather Capability
This Ability integrates a weather API to fetch and share weather updates based on user-provided locations.Code
Key Features
- External API Call: Fetches real-time weather data.
- Geolocation: Validates and processes user-provided locations.
- Error Handling: Provides meaningful feedback for invalid inputs.
Allowed/Disallowed Libraries and Patterns
The following imports, keywords, and patterns are not allowed in Abilities. Use the safe alternatives.Blocked Imports and Keywords
Guidance:
- Avoid direct storage/infra access. Use platform-provided helpers within
CapabilityWorker/workeror request an API if needed. - Use the provided logging (
editor_logging_handler) instead of prints. - For files, prefer platform abstractions and per-user capability folders; ask for an approved helper if you need persistent storage.
- For HTTP GET requests, always use the
self.worker.session_taskshelpers (get,get_async,httpx_get,httpx_get_async,aiohttp_get_async). Never callrequests,httpx, oraiohttpdirectly. See Making HTTP Requests.
Security Guidance
Avoid insecure or unsafe patterns such as runtimeassert checks, exec() of dynamic code, binding servers to all interfaces, hardcoded secrets, swallowing exceptions, insecure deserialization (pickle/dill/shelve/marshal), weak hashes like MD5, or weak cipher modes (e.g., ECB). If you have a special case, request approval and an approved wrapper/utility.
Conclusion
Building Abilities in OpenHome empowers developers to create custom functionalities for AI agents. With the examples like the Basic Advisor and Weather Capability, you can:- Core Communication: Use
speak,run_io_loop, anduser_responsefor basic interactions. - Advanced Audio: Play custom audio files, and stream audio data.
- Text Generation: Leverage multiple text-to-text options with history and system prompts.
- Voice Customization: Use specific voice IDs for varied and engaging responses.
- External APIs: Integrate third-party services for dynamic functionality.
CapabilityWorker provides all the tools needed to create sophisticated, interactive Abilities.
Start creating innovative Abilities and push the boundaries of voice AI with OpenHome! 🎉
Note: For GET requests to third-party APIs, useself.worker.session_tasks.get()(see Making HTTP Requests). It takes the same arguments as therequestsmodule’sget()and returns the same response. For other request types, use therequestsmodule and avoid other libraries; if you need a library that isn’t available, you can request that we add it.

