Plugin Manifest Guide
The manifest.json file is the heart of your Lumia Stream plugin. It defines metadata, configuration options, and capabilities.
Basic Structure
{
"id": "unique_plugin_id",
"name": "Human Readable Name",
"version": "1.0.0",
"author": "Your Name",
"description": "Brief description",
"lumiaVersion": "^9.0.0",
"category": "utilities",
"config": {
"settings": [],
"actions": [],
"variables": [],
"alerts": [],
"translations": {}
}
}
Required Fields
Basic Information
id(string): Unique identifier for your plugin. Use letters, numbers, or underscores, no spaces or hyphens.name(string): Human-readable plugin name.version(string): Semantic version (e.g., "1.0.0").author(string): Plugin author name.description(string): Brief description of what the plugin does.lumiaVersion(string): Compatible Lumia Stream version (semver range).category(string): Plugin category (see categories below).
Optional Fields
Extended Information
email(string): Author's email address.website(string): Plugin or author website URL.repository(string): Source code repository URL.license(string): License type (e.g., "MIT", "GPL-3.0").keywords(string): Comma separated list of keywords for search.icon(string): Plugin icon filename (PNG recommended).changelog(string): Markdown changelog content or a relative path to a.mdfile in the plugin package.bundle(object): Optional install-time bundle for commands and overlays.
Bundle Content (Optional)
Use bundle when you want a plugin to install ready-made commands or overlays along with the plugin itself.
{
"id": "my_plugin",
"name": "My Plugin",
"version": "1.0.0",
"author": "You",
"description": "Example",
"lumiaVersion": "^9.0.0",
"category": "utilities",
"config": { "settings": [] },
"bundle": {
"commands": [
"bundle/commands/hype_command.lumia",
"bundle/commands/wow_command.lumia"
],
"overlays": ["overlay-marketplace-upload-id", "another-overlay-upload-id"]
}
}
bundle.commands must be an array of exported command file paths (.lumia / .lumiastream).
bundle.overlays must be an array of marketplace upload IDs or shared download URLs.
Example URL format: https://api.lumiastream.com/shared/download/<overlay-id>.
Each ID must reference a public + approved marketplace overlay.
Plugin Categories
The Lumia marketplace recognises the following categories (strings are case-sensitive):
system: Core Lumia features and internal toolingplatforms: Streaming platform integrations (Twitch, YouTube, Kick, etc.)apps: Third-party app integrationslights: General lighting control providersswitch: Smart switch and relay integrationsdeck: Control deck hardware and softwareprotocols: Network or automation protocols (OSC, MIDI, etc.)keylight: Key light devicesdevices: Miscellaneous hardware integrationsutilities: General purpose utilities and helpersoverlays: Overlay and visual experiencesaudio: Audio processing and sound integrationschat: Chat interaction toolsdevelopment: Development, testing, and debugging utilities
Lights configuration (for lights category)
If your plugin provides lights, add a config.lights block so the PluginAuth UI can render discovery/manual-add controls and a selection list. Lights are saved by the Lumia UI; plugins should not mutate light state directly.
{
"id": "my_lights_plugin",
"name": "My Lights",
"category": "lights",
"config": {
"settings": [],
"lights": {
"search": {
"buttonLabel": "Discover lights",
"helperText": "Runs your searchLights hook"
},
"manualAdd": {
"buttonLabel": "Add light",
"helperText": "Enter details for a device",
"fields": [
{ "key": "name", "label": "Name", "type": "text", "required": true },
{ "key": "id", "label": "ID (optional)", "type": "text" },
{ "key": "ip", "label": "IP (optional)", "type": "text" }
]
},
"displayFields": [
{ "key": "name", "label": "Name" },
{ "key": "ip", "label": "IP", "fallback": "No IP" }
],
"emptyStateText": "No lights yet. Discover or add one."
}
}
}
Runtime hooks for lights plugins:
- Implement
searchLightsto return an array of discovered lights for the UI to save. - Implement
addLightto handle manual-add requests and return the updated array. - Implement
onLightChangeto receive color/brightness/power updates for your lights. - Implement
searchThemesto return Studio theme options (for example effects/modes).
Note: Selection and persistence are handled by the PluginAuth UI; plugins should not call registerLights/updateChosenLights.
Plugs configuration (optional for plug/accessory integrations)
If your plugin provides plug/accessory control, add a config.plugs block so the PluginAuth UI can render discovery/manual-add controls and a selection list.
{
"id": "my_plug_plugin",
"name": "My Plugs",
"category": "devices",
"config": {
"settings": [],
"plugs": {
"search": {
"buttonLabel": "Discover plugs",
"helperText": "Runs your searchPlugs hook"
},
"manualAdd": {
"buttonLabel": "Add plug",
"helperText": "Enter details for a plug",
"fields": [
{
"key": "name",
"label": "Alias Name",
"type": "text",
"required": true
},
{
"key": "mac",
"label": "MAC Address",
"type": "text",
"required": true
},
{ "key": "model", "label": "Model (optional)", "type": "text" }
]
},
"displayFields": [
{ "key": "name", "label": "Name" },
{ "key": "model", "label": "Model", "fallback": "Unknown model" }
],
"emptyStateText": "No plugs yet. Discover or add one."
}
}
}
Runtime hooks for plugs/accessories:
- Implement
searchPlugsto return an array of discovered plugs for the UI to save. - Implement
addPlugto handle manual-add requests and return the updated array. - Implement
onPlugChangeto receive runtime state updates (state: true/false) for selected plugs.
Note: Selection and persistence are handled by the PluginAuth UI; plugins should not call registerAccessories/updateChosenAccessories.
Studio Theme Configuration (optional for lights plugins)
Use config.themeConfig when your lights plugin supports Studio theme scenes/effects/presets.
{
"config": {
"themeConfig": {
"keyForThemes": "effects",
"hasEffects": true,
"sceneType": "light-theme",
"displayKey": "name",
"showIndividualLights": true
}
}
}
Theme runtime contract:
searchThemesshould return either:- an array (stored under
themeConfig.keyForThemes, defaultscenes) - or an object containing one or more of
scenes,effects,presets
- an array (stored under
- when a Studio theme is executed, selected theme values are provided to
onLightChangeinconfig.rawConfig.theme
Configuration
The config object defines your plugin's interactive elements:
AI Provider Support
If your plugin should appear as an AI provider (like ChatGPT/DeepSeek) in Lumia features that support AI routing, declare:
{
"config": {
"hasAI": true
}
}
When hasAI is enabled, implement:
aiPrompt(config)to process prompt requests and return text.aiModels(config?)to return available model options for dropdowns.
Settings
Settings create a configuration UI for users:
{
"config": {
"settings": [
{
"key": "apiKey",
"label": "API Key",
"type": "password",
"placeholder": "Enter your API key",
"helperText": "Get this from your service dashboard",
"required": true
},
{
"key": "pollInterval",
"label": "Poll Interval (seconds)",
"type": "number",
"defaultValue": 30,
"validation": {
"min": 5,
"max": 300
}
},
{
"key": "notifications",
"label": "Enable Notifications",
"type": "toggle",
"defaultValue": true
}
]
}
}
Organizing Settings with Sections and Groups
Use these layout properties to keep plugin settings easy to navigate:
section: creates top-level settings tabssectionOrder: controls tab order (ascending)group: creates a grouped container inside a section for related fields
Quick guidance:
- Use only
sectionwhen you just need tabs. - Add
groupwhen a tab has multiple clusters (for example, one cluster per game).
UI rendering shape:
Plugin Settings
├─ Tab: Connection (section="Connection")
│ ├─ Field: API Key
│ └─ Field: Poll Interval
└─ Tab: Detection (section="Detection")
├─ Group: Valorant Rules (group="valorant_rules")
│ ├─ Field: Kill Template
│ └─ Field: ROI
└─ Group: Rocket Rules (group="rocket_rules")
├─ Field: Goal Template
└─ Field: ROI
Example:
{
"config": {
"settings": [
{
"key": "enabledGames",
"label": "Enabled Games",
"type": "select",
"multiple": true,
"section": "Detection",
"sectionOrder": 2,
"options": [
{ "label": "Valorant", "value": "valorant" },
{ "label": "Overwatch", "value": "overwatch" }
]
},
{
"key": "valorantTemplate",
"label": "Valorant Kill Template",
"type": "file",
"section": "Detection",
"group": {
"key": "valorant_rules",
"label": "Valorant Rules",
"visibleIf": {
"key": "enabledGames",
"equals": "valorant"
}
}
},
{
"key": "valorantRoi",
"label": "Valorant ROI",
"type": "roi",
"section": "Detection",
"group": "valorant_rules"
}
]
}
}
For complete behavior details (including visibleIf, fallback rules, and shorthand/object group forms), see docs/field-types-reference.md.
Avoid adding settings that exist only for testing or debugging. Keep settings user-facing and essential.
If you provide settings_tutorial (markdown or a relative .md file path), it renders as a setup guide in the auth screen.
To provide a tutorial that is specific to actions, use actions_tutorial (markdown or a relative .md file path). When present, it renders in the Actions editor.
Populate both tutorials with clear, step-by-step instructions that a first-time user can follow without guesswork.
Tutorials can include local images bundled with the plugin. Reference them with a relative path and they will resolve from the plugin root at runtime:
### Setup


Tutorials can also embed playable media directly in markdown-supported HTML tags.
Tutorial Media Embeds
Use raw HTML tags inside settings_tutorial or actions_tutorial for embedded media:
### Video Walkthrough
<iframe
width="100%"
height="315"
src="https://www.youtube.com/watch?v=VCd0kYWLvMQ"
title="Setup walkthrough"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
### Local Video
<video controls preload="metadata">
<source src="./setup-demo.mp4" type="video/mp4" />
</video>
### Audio Instructions
<audio controls preload="metadata">
<source src="./voiceover.mp3" type="audio/mpeg" />
</audio>
Media behavior and safety rules:
iframeembeds are restricted to YouTube (youtube.com,m.youtube.com,youtu.be).- YouTube watch/share URLs are normalized to embed URLs automatically.
video,audio, andsourcesrcvalues allowhttp,https,file,blob, anddata:audio/*/data:video/*.- Relative media paths are resolved from the plugin root, so include those files in your plugin package.
- Keep a plain fallback link near embeds so users can still open media externally if playback is blocked on their system.
Plugin Translations
Plugins can provide translation bundles under config.translations. These bundles are loaded into i18next under the plugin namespace (the plugin id) when the plugin loads.
config.translations supports two shapes:
- a language-map object (
{ en: {...}, es: {...} }) - a relative
.jsonfile path that contains that language-map object
{
"id": "clickup",
"name": "ClickUp",
"version": "1.0.0",
"author": "Lumia Stream",
"description": "ClickUp integration",
"lumiaVersion": "^9.0.0",
"category": "apps",
"config": {
"translations": "./translations.json"
}
}
Example translations.json:
{
"en": {
"task_due_date": "Task Due Date",
"clickup_task_due_date": "Task Due Date"
},
"de": {
"task_due_date": "Fälligkeitsdatum",
"clickup_task_due_date": "Fälligkeitsdatum"
}
}
You can also inline the language map directly in manifest.json:
{
"config": {
"translations": {
"en": {
"task_due_date": "Task Due Date"
}
}
}
}
Use keys from supported Lumia languages (en, es, fr, ar, nl, sv, de, tr, zh, pt). Regional tags (for example en-US) fall back to their base language when available.
Multi-Value Settings
Use select with multiple: true when you need users to select or enter multiple values.
{
"config": {
"settings": [
{
"key": "lightIds",
"label": "Light IDs",
"type": "select",
"multiple": true,
"allowTyping": true,
"helperText": "Add one or more light identifiers or choose from suggestions",
"options": [
{ "label": "Keylight 1", "value": "keylight-1" },
{ "label": "Keylight 2", "value": "keylight-2" }
],
"defaultValue": ["keylight-1", "keylight-2"]
}
]
}
}
OAuth (Lumia-managed)
Plugins can request OAuth via Lumia's auth service so users can authorize in-app and store tokens in plugin settings. To enable this, add an oauth block under config.
UI rendering shape:
Plugin Auth Screen
├─ OAuth Card
│ ├─ Title: OAuth
│ ├─ Helper Text (oauth.helperText)
│ ├─ Button: Authorize (oauth.buttonLabel)
│ ├─ Button: Copy auth link
│ └─ Status / Error Text
└─ Settings Form
└─ Regular plugin settings fields (config.settings)
OAuth flow:
- User clicks Authorize.
- Lumia opens the OAuth URL (in-app or browser based on
openInBrowser). - Provider redirects back to Lumia.
- Lumia maps returned tokens to plugin settings via
tokenKeys. - Tokens are available in runtime as
this.settings.<tokenKey>.
{
"config": {
"oauth": {
"buttonLabel": "Authorize Fitbit",
"helperText": "Connect your Fitbit account to pull health metrics.",
"openInBrowser": false,
"scopes": ["activity", "heartrate"],
"extraParams": "external=true",
"tokenKeys": {
"accessToken": "accessToken",
"refreshToken": "refreshToken",
"tokenSecret": "tokenSecret"
}
}
}
}
Notes:
- The OAuth provider is always the plugin ID within Lumia. If you need a new OAuth 2.0 integration, contact Lumia Stream on Discord or email dev@lumiastream.com so we can enable it for your plugin.
serviceUrlcan be provided to override the default auth URL entirely.scopesare provider-specific. When set, they are sent to Lumia's OAuth service as a comma-separated list.- Tokens are stored into your plugin settings using the
tokenKeysmapping and are available viathis.settingsin your plugin.
Custom Auth Display (PluginAuth Embedded UI)
If your plugin needs a custom setup flow (for example, patching fixtures or testing hardware output), you can render your own HTML/CSS/JS inside PluginAuth.
Manifest shape:
{
"config": {
"custom_auth_display": {
"entry": "./auth/index.html",
"autoAutoOpen": true,
"authButtonLabel": "Open Device Setup",
"title": "Device Setup"
}
}
}
Field behavior:
entry(required): relative path to your HTML file from plugin root.title(required): modal title shown in PluginAuth.autoAutoOpen(optional boolean): auto-opens the modal when PluginAuth loads.authButtonLabel(optional string): label for the manual open button in PluginAuth.
Runtime hooks:
onCustomAuthDisplaySignal(config)receives generic signals from your embedded page.onCustomAuthDisplayClose(config)runs when the modal closes.
In your embedded page, Lumia injects window.customAuthDisplay:
window.customAuthDisplay.signal(type, payload)sends a generic signal toonCustomAuthDisplaySignal.window.customAuthDisplay.close(reason?)closes the modal and triggersonCustomAuthDisplayClose.window.customAuthDisplay.signal('close')is also supported as a close mode.
Lumia PluginAuth Styling Standards (Recommended)
To keep custom auth pages visually consistent with Lumia Stream, use Lumia's base theme colors and component conventions.
Suggested palette:
:root {
--background: #191743;
--containerbackground: #1f1f3a;
--cardbackground: #15142b;
--cardborder: #393853;
--primary: #ff4076;
--secondary: #535395;
--white: #ffffff;
--white2: #cac9d5;
--success: #5dda6c;
--warning: #dcc984;
--error: #fd5454;
--transwhite: rgba(255, 255, 255, 0.05);
}
Suggested component rules:
- Font:
Roboto, sans-serif. - Cards/panels: radius
16px, border1px solid #393853, dark panel fill (#1f1f3a/#15142b). - Inputs/selects/textareas:
- border
1px solid #393853 - background
rgba(255,255,255,0.05)or#1f1f3a - text
#ffffff, secondary/help text#cac9d5 - focus border
#cac9d5(or#ff4076for primary emphasis)
- border
- Buttons:
- primary actions use
#ff4076tint + stronger accent border - secondary actions use neutral dark fill + subtle border/hover
- status actions use
#5dda6c,#dcc984,#fd5454tinted variants
- primary actions use
- Spacing: use
8pxscale (8/12/16/24) for consistent density.
For AI-assisted UI generation, include these tokens and rules in your prompt so generated custom auth pages match Lumia out of the box.
Native chatbot support
If your plugin can post chat messages on its own platform, declare chatbot support in the manifest:
{
"config": {
"hasChatbot": true
}
}
config.hasChatbot is a boolean flag. Set it to true to enable plugin-native chatbot routing.
Runtime routing behavior:
- Lumia calls
chatbot(config)on your plugin when implemented. - Lumia does not fall back to
actions()for chatbot routing.
Native moderation command support
If your plugin supports chat moderation actions in Dashboard Chatbox / API, declare supported commands:
{
"config": {
"modcommandOptions": ["delete", "ban", "timeout"]
}
}
Allowed values are:
deletecopytranslateshoutoutbanunbantimeoutadd-vipremove-vipadd-moderatorremove-moderator
When declared, Lumia routes these commands to modCommand(type, value) in your plugin runtime.
Heart rate source support
If your plugin reads a live heart rate (BPM), from a wearable, a Bluetooth heart-rate monitor, or a third-party API, declare it as a heart-rate source:
{
"config": {
"hasHeartrate": true
}
}
config.hasHeartrate is a boolean flag. When true, Lumia treats your plugin like the built-in HypeRate/Pulsoid integrations and shows the heart-rate zone Alerts UI (Pulse levels) for it instead of plain alerts.
The flag is only the UI half. To feed the shared heart-rate system, call updateHeartRate(bpm) from your runtime whenever a new reading arrives:
await this.lumia.updateHeartRate(bpm);
That single call drives the {{heart_rate}} overlay variable, the Pulse heart-rate zone alerts, calorie tracking, and Studio heart-rate light reactions, the same pipeline the native heart-rate integrations use. See updateHeartRate in the API Reference.
Settings support the same field types as action fields, plus three settings-only structured types: named_map (labeled name-to-value rows), json (raw JSON editor), and roi (region-of-interest coordinates). Settings also support disabled: true to render a field read-only in PluginAuth.
Use named_map instead of a freeform textarea when users should define multiple named entries (for example sound name -> file, feed name -> URL).
For all field types, properties, validation options, and examples see Field Types Reference.
Actions
Actions allow users to trigger plugin functionality manually:
{
"config": {
"actions": [
{
"type": "send_message",
"label": "Send Custom Message",
"description": "Send a custom message to the service",
"fields": [
{
"key": "message",
"label": "Message",
"type": "textarea",
"required": true
},
{
"key": "channel",
"label": "Channel",
"type": "select",
"options": [
{ "label": "General", "value": "general" },
{ "label": "Alerts", "value": "alerts" }
]
}
]
},
{
"type": "update_status",
"label": "Update Status",
"description": "Update the current status message",
"fields": [
{
"key": "status",
"label": "Status",
"type": "text",
"required": true
}
]
}
]
}
}
Avoid test-only actions (for example, test connection or refetch). Actions should map to real user workflows.
To auto-refresh dynamic options when a user selects the action type, set refreshOnChange on the action.
{
"type": "send_message",
"label": "Send Custom Message",
"refreshOnChange": true,
"fields": []
}
acceptedVariables on actions is optional, but when used each key must be prefixed with your plugin id (for example, myplugin_song_name). Unprefixed keys are ignored by Lumia.
Use action acceptedVariables for action-specific result data. Do not mirror one-off action payloads into global config.variables unless that value must persist outside the action result.
Variable Functions
Variable functions let plugins return values inline in templates (similar to {{ai_prompt=...}}).
Define them under config.variableFunctions:
{
"config": {
"variableFunctions": [
{
"key": "my_function",
"label": "My Function",
"description": "Use {{my_function=value}} to return a value."
}
]
}
}
Usage example:
{{my_function=message|thread|model}}
- The text after
=is passed to the plugin as a raw string (value). - A convenience
argsarray is provided by splitting the raw string on|. - Your plugin handles the logic in
variableFunction()and returns a string.
Guidelines:
- Keep variable functions fast. They run during template resolution.
- Do not log excessively or retry aggressively.
- Return a plain string or an object with
{ value, variables }.
Logging Guidance
Avoid excessive logging. High-frequency logs can quickly fill the user's Logs dashboard. Prefer concise logs and only emit details for errors or explicit user actions. Avoid per-plugin log wrappers unless they add real value.
Action fields support: text, datetime, email, url, number, textarea, color, select, checkbox, toggle, slider, file, media. Note: password, named_map, json, and roi are not available in action fields.
Key action-field behaviors:
- Set
allowVariables: trueto enable{{variable}}insertion on a field. When omitted, variables are disabled, including onselectfields withallowTyping. - Set
allowTyping: trueonselectto allow custom typed values alongside dropdown options. - Set
multiple: trueonselectto allow multi-value selection (value becomes an array). allowTypingandmultiplecan be combined on the sameselectfield.
For full property lists and examples see Field Types Reference.
Variables
Variables define data that your plugin provides to Lumia Stream:
Variable names in config.variables can be unprefixed. Lumia namespaces them internally.
When you reference variables in action acceptedVariables or return them from actions() via newlyPassedVariables, keys must be fully prefixed with your plugin id (<pluginId>_key). Unprefixed keys are ignored.
Global variable display text is resolved from your plugin translations (config.translations) using your plugin namespace.
Best practice: keep config.variables minimal and durable. Reserve globals for long-lived state (for example totals, latest known status, profile metadata). For action outputs and request-specific values, use action acceptedVariables / newlyPassedVariables and alert extraSettings payloads instead of creating many global variables.
{
"config": {
"variables": [
{
"name": "follower_count",
"system": true,
"origin": "twitch",
"value": 0,
"example": "follower_count"
},
{
"name": "last_follower",
"system": true,
"origin": "twitch",
"value": "",
"example": "last_follower"
}
]
}
}
Alerts
Alerts define events that your plugin can trigger:
{
"config": {
"alerts": [
{
"title": "New Follower",
"key": "follow",
"acceptedVariables": ["follower_count", "last_follower"],
"defaultMessage": "{{last_follower}} just followed! Welcome!",
"variationConditions": []
},
{
"title": "Stream Started",
"key": "streamStart",
"acceptedVariables": ["stream_title", "game"],
"defaultMessage": "Stream is now live! Playing {{game}}"
}
]
}
}
Optional alert defaults can be provided under defaults to control how Lumia initializes the base alert state. For example, disableBaseAlert can enforce variations-only behavior.
{
"title": "High CPU Usage",
"key": "cpu_high",
"defaultMessage": "",
"defaults": {
"disableBaseAlert": true
}
}
Alert Variations (variationConditions)
Use variationConditions when an alert can fire with multiple sub-types (for example, different tiers of a subscription or thresholds of a donation) and you want creators to configure each variation independently.
type: One of the condition identifiers exposed byLumiaVariationConditions(seelumia-types/src/alert.types.ts:6). Examples:EQUAL_SELECTION,GIFT_SUB_EQUAL,GREATER_NUMBER,RANDOM.description(optional): Helper text shown in the Lumia UI.dynamicOptions(optional,EQUAL_SELECTIONonly): Whentrue, Lumia shows dropdown suggestions but also allows typed custom values.selections(optional): Only used withEQUAL_SELECTION; supplies the dropdown values the creator can pick from.label: How the option appears in the Lumia UI.value: The literal tier/value you expect to receive at runtime (compared againstdynamic.value).message(optional): Override fordefaultMessagewhen this variation is active.
Example manifest entry with variations:
{
"title": "Gifted Membership",
"key": "giftedMembership",
"defaultMessage": "{{gifter}} gifted a membership!",
"acceptedVariables": ["gifter", "recipient", "gift_count"],
"variationConditions": [
{
"type": "EQUAL_SELECTION",
"description": "Tier level (compares against dynamic.value).",
"dynamicOptions": true,
"selections": [
{
"label": "Single gift",
"value": "Tier1"
},
{
"label": "Premium tier",
"value": "Tier2",
"message": "{{gifter}} just unlocked the premium tier!"
}
]
},
{
"type": "GIFT_SUB_EQUAL",
"description": "Exact gift bundle size (compares against dynamic.giftAmount)."
},
{
"type": "GIFT_SUB_GREATER",
"description": "Gift bundle size or larger (compares against dynamic.giftAmount)."
}
]
}
At runtime, trigger the alert with variation data in dynamic and alert variables in extraSettings:
await this.lumia.triggerAlert({
alert: "giftedMembership",
dynamic: {
value: "Tier2", // value compared by EQUAL_SELECTION
},
extraSettings: {
gifter: "StreamerFan42",
recipient: "LuckyViewer",
gift_count: 5,
},
});
Important:
dynamicis only forvariationConditions.- Use
dynamic.valuefor standard comparisons. - For specialized comparisons, pass direct fields like
giftAmount,subMonths,currency, orisGift. - Plugin-triggered alerts do not accept
dynamic.name; plugin runtime strips it automatically. extraSettingscan contain any key/value pairs and is the payload used for alert variables/templates.
If you need multiple variation comparisons, trigger separate alerts with the appropriate dynamic payload for each event.
Variation Generation
Use variation generation when Lumia should build many alert variations from plugin-managed data instead of having the creator add them one by one.
variationGeneration(optional): Configures the modal shown before generation runs.title(optional): Modal title.description(optional): Helper text shown above the form.buttonLabel(optional): Label used for the alert-page button.generateLabel(optional): Label used for the modal submit button.fields(optional): Uses the samePluginFormField[]schema as other plugin forms, so you can declare checkboxes, inputs, selects, sliders, and other supported field types.
Example manifest entry:
{
"title": "Gift Event",
"key": "gift",
"defaultMessage": "{{username}} sent {{giftName}}",
"variationGeneration": {
"title": "Generate Gift Variations",
"description": "Create one variation for each supported gift.",
"buttonLabel": "Generate Gifts",
"generateLabel": "Generate",
"fields": [
{
"key": "minimumCost",
"label": "Minimum cost",
"type": "number",
"defaultValue": 1,
"min": 0
},
{
"key": "includeImages",
"label": "Include thumbnails",
"type": "checkbox",
"defaultValue": true
}
]
}
}
When the creator clicks the button, Lumia calls your runtime generateVariations() method:
async generateVariations(config) {
const { alertKey, options, existingVariations } = config;
if (alertKey !== 'gift') return [];
return [
{
name: 'Rose',
conditionType: 'EQUAL_SELECTION',
condition: 'Rose',
image: 'https://example.com/rose.png',
},
];
}
Guidelines:
- Return only new variations; skip duplicates against
existingVariations. condition/conditionTypeshould match the alert'svariationConditions.imageis optional and becomes the variation thumbnail.- Lumia still does its own duplicate filtering before saving, but plugins should avoid returning duplicates themselves.
Variation Payload Contract (Plugins)
For plugin-triggered alerts, Lumia applies these runtime rules:
- Variation matching uses
dynamic(notextraSettings). - Plugin alert keys are auto-prefixed as
<pluginId>-<alert>unless already prefixed. - Reserved identity keys are stripped from plugin payloads:
pluginId,platform,site,origin. dynamic.namefrom plugins is stripped and ignored.dynamicis merged intoextraSettingsfor downstream consumers; if the same key exists in both objects,dynamicwins.
Use these dynamic fields for each variation type:
| Variation Condition Type | Dynamic Fields To Send |
|---|---|
EQUAL_NUMBER, EQUAL_SELECTION, EQUAL_STRING, GREATER_NUMBER, LESS_NUMBER | value |
EQUAL_CURRENCY_NUMBER, GREATER_CURRENCY_NUMBER | value, currency |
GIFT_SUB_EQUAL, GIFT_SUB_GREATER | giftAmount |
IS_GIFT | isGift and/or giftAmount |
SUBSCRIBED_MONTHS_EQUAL, SUBSCRIBED_MONTHS_GREATER | subMonths |
EQUAL_USERNAME | username (or value) |
EQUAL_USER_LEVEL | lumiauserlevels |
COUNT_IS_MULTIPLE_OF | total (previousTotal optional for deterministic threshold-crossing behavior) |
RANDOM | no dynamic field required |
If no matching selection is found for the provided condition values (or no dynamic payload is supplied), Lumia falls back to the base alert configuration and defaultMessage.
To also show a plugin-triggered alert inside the Event List, opt in explicitly:
await this.lumia.triggerAlert({
alert: "giftedMembership",
showInEventList: true,
extraSettings: {
gifter: "StreamerFan42",
recipient: "LuckyViewer",
gift_count: 5,
},
});
Recommendation: keep showInEventList disabled by default. Most utility/app/device plugins should not write every plugin-triggered alert to Event List. Enable it for platform/event-source integrations when those events are meant to appear there.
Tip: LumiaDynamicCondition in lumia-types/src/alert.types.ts:99 documents the dynamic fields used by variation checkers (value, currency, subMonths, giftAmount, etc.).
Complete Example
Here's a complete manifest for a hypothetical Discord integration plugin:
{
"id": "discord_integration",
"name": "Discord Integration",
"version": "2.1.0",
"author": "Lumia Stream",
"email": "dev@lumiastream.com",
"website": "https://lumiastream.com/plugins/discord",
"repository": "https://github.com/LumiaStream/discord-plugin",
"description": "Integrate your Discord server with Lumia Stream",
"license": "MIT",
"lumiaVersion": "^9.0.0",
"keywords": "discord, chat, community, notifications",
"category": "platforms",
"icon": "discord-icon.png",
"changelog": "# Changelog\n\n## 2.1.0\n- Added voice channel activity tracking\n- Improved message filtering\n- Bug fixes for role mentions\n\n## 2.0.0\n- Complete rewrite with new Discord API\n- Added slash command support\n- Enhanced security",
"config": {
"settings": [
{
"key": "botToken",
"label": "Bot Token",
"type": "password",
"placeholder": "Your Discord bot token",
"helperText": "Create a bot application at https://discord.com/developers/applications",
"required": true
},
{
"key": "guildId",
"label": "Server ID",
"type": "text",
"placeholder": "Your Discord server ID",
"helperText": "Right-click your server name and select 'Copy Server ID'",
"required": true,
"validation": {
"pattern": "^[0-9]{17,19}$"
}
},
{
"key": "channels",
"label": "Monitored Channels",
"type": "textarea",
"placeholder": "Channel IDs, one per line",
"helperText": "Leave empty to monitor all channels",
"defaultValue": ""
},
{
"key": "messageFilter",
"label": "Message Filter",
"type": "select",
"defaultValue": "all",
"options": [
{ "label": "All Messages", "value": "all" },
{ "label": "Mentions Only", "value": "mentions" },
{ "label": "Commands Only", "value": "commands" }
]
},
{
"key": "enableVoiceTracking",
"label": "Track Voice Activity",
"type": "toggle",
"defaultValue": false,
"helperText": "Monitor voice channel joins/leaves"
}
],
"actions": [
{
"type": "send_message",
"label": "Send Message",
"description": "Send a message to a Discord channel",
"fields": [
{
"key": "channelId",
"label": "Channel ID",
"type": "text",
"required": true
},
{
"key": "message",
"label": "Message",
"type": "textarea",
"required": true,
"validation": {
"maxLength": 2000
}
}
]
},
{
"type": "update_status",
"label": "Update Bot Status",
"description": "Update the bot's activity status",
"fields": [
{
"key": "activity",
"label": "Activity Type",
"type": "select",
"options": [
{ "label": "Playing", "value": "PLAYING" },
{ "label": "Listening", "value": "LISTENING" },
{ "label": "Watching", "value": "WATCHING" },
{ "label": "Streaming", "value": "STREAMING" }
]
},
{
"key": "text",
"label": "Status Text",
"type": "text",
"placeholder": "What is the bot doing?"
}
]
}
],
"variables": [
{
"name": "member_count",
"system": true,
"origin": "discord",
"value": 0,
"example": "member_count"
},
{
"name": "online_count",
"system": true,
"origin": "discord",
"value": 0,
"example": "online_count"
},
{
"name": "voice_count",
"system": true,
"origin": "discord",
"value": 0,
"example": "voice_count"
},
{
"name": "last_message",
"system": true,
"origin": "discord",
"value": "",
"example": "last_message"
},
{
"name": "last_user",
"system": true,
"origin": "discord",
"value": "",
"example": "last_user"
}
],
"alerts": [
{
"title": "New Message",
"key": "message",
"acceptedVariables": ["last_message", "last_user"],
"defaultMessage": "{{last_user}}: {{last_message}}"
},
{
"title": "Member Joined",
"key": "memberJoin",
"acceptedVariables": ["member_count", "last_user"],
"defaultMessage": "{{last_user}} joined the Discord! ({{member_count}} total)"
},
{
"title": "Voice Channel Activity",
"key": "voiceActivity",
"acceptedVariables": ["voice_count", "last_user"],
"defaultMessage": "{{last_user}} joined voice ({{voice_count}} in voice)"
}
]
}
}
Validation
The SDK includes manifest validation. Common validation errors:
- Invalid semver: Version must follow semantic versioning
- Missing required fields: All required fields must be present
- Invalid category: Category must be one of the allowed values
- Duplicate keys: Setting/action/variable keys must be unique
- Invalid types: Field types must be valid
- Circular dependencies: Alert conditions cannot create loops
Best Practices
- Unique IDs: Use your organization/username prefix (e.g., "mycompany-plugin-name")
- Semantic Versioning: Follow semver for version numbers
- Clear Descriptions: Write descriptive labels and help text
- Sensible Defaults: Provide good default values
- Validation: Use validation rules to prevent user errors
- Documentation: Include comprehensive descriptions and examples
- Testing: Test your manifest with the validation tools
- Tutorials: Provide clear
settings_tutorialandactions_tutorialsteps - No Test Controls: Avoid test-only settings and actions
- Retry Discipline: Implement a disconnect flow, use exponential backoff with a retry cap, mark the plugin disconnected when retries are exhausted, and pause polling until an explicit reconnect trigger
- Minimal Logs: Log errors and explicit user actions only
Localization
See Plugin Translations above for full config.translations documentation, including inline language maps, external .json file paths, supported language codes, and runtime key resolution.