Layer 2 of the 4-layer pipeline handles plugin dispatch. When a message arrives:
- Layer 0 assembles context (speaker, room, time)
- Layer 1 checks for instant answers (date, math, greetings)
- Layer 2 tries each registered plugin in order:
- Calls
match(message, context)→ returnsCommandMatch - If
matched=True, callshandle(message, match, context)→ returnsCommandResult - First match wins — no further plugins are tried
- Calls
- Layer 3 falls through to the LLM if no plugin matches
| Plugin | ID | Needs Setup | Description |
|---|---|---|---|
| Home Assistant | home_assistant |
HA_URL + HA_TOKEN | Smart home device control |
| Plugin | ID | Needs Setup | Description |
|---|---|---|---|
| Dictionary | dictionary |
No | Word definitions, synonyms, etymology |
| Wikipedia | wikipedia |
No | Quick encyclopedia lookups |
| Conversions | conversions |
No | Unit/currency conversions |
| Cooking | cooking |
No | Recipes, substitutions, meal planning |
| Translation | translation |
No | Language translation |
| Plugin | ID | Needs Setup | Description |
|---|---|---|---|
| Weather | weather |
WEATHER_API_KEY |
Forecasts and conditions |
| News | news |
API key | Headlines and summaries |
| Stocks | stocks |
API key | Market data and quotes |
| Sports | sports |
API key | Scores and schedules |
| Movie | movie |
API key | Movie info, ratings, recommendations |
| Plugin | ID | Needs Setup | Description |
|---|---|---|---|
| Scheduling | scheduling |
No | Alarms, timers, reminders |
| Routines | routines |
No | Automation routines |
| Daily Briefing | daily_briefing |
No | Personalized morning summary |
| Plugin | ID | Needs Setup | Description |
|---|---|---|---|
| Media | media |
Provider auth | YouTube Music, Plex, Audiobookshelf |
| Stories | stories |
No | Interactive story generator |
| Sound Library | sound_library |
No | Sound effects and ambient sounds |
| Plugin | ID | Needs Setup | Description |
|---|---|---|---|
| Number Quest | number_quest |
No | Math STEM game |
| Science Safari | science_safari |
No | Science STEM game |
| Word Wizard | word_wizard |
No | Language STEM game |
| Intercom | intercom |
Satellites | Announce, broadcast, calling |
| Plugin | ID | Needs Setup | Description |
|---|---|---|---|
| Knowledge | knowledge |
Optional (WebDAV) | Document search, knowledge base |
| Lists | lists |
Optional (HA) | Shopping lists, to-do lists |
In the admin panel (Plugins page):
- Each plugin shows a toggle to enable/disable
- Changes take effect on the next server restart
- Disabled plugins are skipped during Layer 2 dispatch
Via API:
# Enable
curl -X POST http://localhost:5100/admin/plugins/weather/enable \
-H "Authorization: Bearer $TOKEN"
# Disable
curl -X POST http://localhost:5100/admin/plugins/weather/disable \
-H "Authorization: Bearer $TOKEN"The admin panel provides form-based configuration for each plugin using the ConfigField system. Each plugin declares its config fields with types, labels, and validation.
Supported field types:
| Type | Renders As |
|---|---|
text |
Text input |
password |
Password input (masked) |
url |
URL input with validation |
toggle |
On/off switch |
select |
Dropdown with options |
number |
Numeric input |
Via API:
curl -X PATCH http://localhost:5100/admin/plugins/weather/config \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"api_key": "your-openweathermap-key", "location": "New York"}'Plugins that need external API keys:
| Plugin | Key Variable | Free Tier |
|---|---|---|
| Weather | WEATHER_API_KEY (OpenWeatherMap) |
Yes — 1,000 calls/day |
| Movie | TMDB or OMDB API key | Yes |
| News | NewsAPI key | Yes — 100 req/day |
| Stocks | Alpha Vantage or similar | Yes — 5 req/min |
| Sports | Sports API key | Varies |
What happens without an API key? The plugin reports health() → False with status "Needs Setup". Messages that would match the plugin fall through to Layer 3 (LLM), which answers from its training data — less accurate but functional.
| Plugin | Required Service | Configuration |
|---|---|---|
| Home Assistant | HA instance | HA_URL + HA_TOKEN env vars |
| Media (YouTube) | YouTube Music | OAuth device flow via admin panel |
| Media (Plex) | Plex server | PLEX_URL + PLEX_TOKEN env vars |
| Media (ABS) | Audiobookshelf | ABS_URL + ABS_TOKEN env vars |
| Knowledge | WebDAV/Nextcloud | CalDAV/WebDAV URL + credentials |
| Intercom | Satellite speakers | At least one provisioned satellite |
These plugins require no configuration — they work immediately:
- Dictionary, Wikipedia, Conversions, Cooking, Translation
- Scheduling (alarms, timers, reminders)
- Routines (automation builder)
- Daily Briefing
- Stories (interactive story generator)
- Sound Library
- STEM Games (Number Quest, Science Safari, Word Wizard)
- Lists (local backend; HA integration optional)
- Knowledge (local search; WebDAV optional)
Each plugin reports a health status visible in the admin panel:
| Status | Meaning |
|---|---|
| 🟢 Ready | Plugin is configured and backend is reachable |
| 🟡 Needs Setup | Missing required configuration (API key, URL, etc.) |
| 🔴 Error | Configuration exists but backend is unreachable |
The health_message property provides a human-readable explanation (e.g., "Weather API key not configured").
Every plugin extends CortexPlugin from cortex/plugins/base.py:
from __future__ import annotations
from cortex.plugins.base import CortexPlugin, CommandMatch, CommandResult, ConfigField
class MyPlugin(CortexPlugin):
plugin_id = "my_plugin"
display_name = "My Plugin"
plugin_type = "action"
version = "1.0.0"
author = "Your Name"
config_fields = [
ConfigField(
key="api_key",
label="API Key",
field_type="password",
required=True,
placeholder="Enter your API key",
help_text="Get a key from https://example.com",
),
ConfigField(
key="enabled_feature",
label="Enable Feature X",
field_type="toggle",
default=True,
),
]
@property
def health_message(self) -> str:
if not self._config.get("api_key"):
return "API key not configured"
return "Ready"
async def setup(self, config: dict) -> bool:
self._config = config
return bool(config.get("api_key"))
async def health(self) -> bool:
return bool(self._config.get("api_key"))
async def match(self, message: str, context: dict) -> CommandMatch:
if "my keyword" in message.lower():
return CommandMatch(matched=True, intent="my_action", confidence=0.9)
return CommandMatch(matched=False)
async def handle(self, message: str, match: CommandMatch, context: dict) -> CommandResult:
# Do your thing
return CommandResult(success=True, response="Here's your answer!")@dataclass
class ConfigField:
key: str # Config dict key
label: str # Human-readable label
field_type: str = "text" # text | password | url | toggle | select | number
required: bool = False # Must be filled before plugin activates
placeholder: str = "" # Input placeholder text
help_text: str = "" # Help text shown below input
default: Any = None # Default value
options: list[dict[str, str]] = [] # For "select": [{"value": "a", "label": "Option A"}]Plugins are discovered automatically by the PluginRegistry when placed in the cortex/plugins/ or cortex/integrations/ directories.
- Return
CommandMatch(matched=False)quickly —match()is called for every message - Use
confidencescores — helps with disambiguation when multiple plugins could match - Implement
health()honestly — users rely on the admin panel status - Declare
config_fields— enables the form-based UI in the admin panel - Set
health_message— provides actionable feedback when setup is needed - Handle failures gracefully — return
CommandResult(success=False, response="...")rather than raising