SheevChat Action Library Manifest Wiki
This page documents the current SheevChat action library manifest format. It is written for humans and for AI agents that are asked to create importable .sheevactions or .sheevactionszip libraries from an application's API documentation.
The goal of an action library is to describe an external app's actions in JSON so SheevChat can expose them inside Custom Commands, Automations, Timers, and SheevPad without custom code for every app.
Quick Rules For AI Agents
When creating a manifest, output only valid JSON. Do not output Markdown, comments, trailing commas, or invented fields.
Use only SheevChat's schema, not a generic action schema. If an API feature cannot fit the supported schema, explain what runtime support would be needed instead of inventing new manifest keys.
The most common AI mistakes are:
- Making
sourcesan array. It must be an object keyed by source id. - Placing
rows,value,label, orextradirectly on a source. They must be insidesource.map. - Omitting
execute.type. - Using nested placeholders like
{scene.id}. SheevChat placeholders are flat, like{scene_id}. - Forgetting that WebSocket channel responses are wrapped as
{ "channel": "...", "payload": ... }. - Putting instance discovery settings beside
discoveryinstead of inside it. - Inventing field types. The only accepted field types are
text,textarea,number,select,dynamic_select,checkbox,color,file,hotkey,hidden, andinfo. Usecheckbox, nottoggle, for a boolean input. - Writing
%USERPROFILE%,$HOME,~, an absolute path, or a drive letter indiscovery.directory. Use a home-relative path such as.veadotube/instances. - Reusing a source containing multiple resource types without filtering it for the action. Use
map.where, or separate typed sources. - Assuming an API id is globally unique. If ids can overlap across types, use a collision-safe option value such as
{type}:{id}. - Creating write-only packs when the API also supports useful reads. Add query actions with typed sequence outputs when later actions can reuse the returned state.
File Type
Action libraries can be imported in either of two formats.
Plain Manifest
A plain manifest is a single JSON file. The recommended extension is:
.sheevactions
Internally, SheevChat installs the imported manifest as:
action-libraries/installed/<library-id>/manifest.json
Packaged Library
A packaged library is a zip file with the recommended extension:
.sheevactionszip
The package must contain a manifest at the root of the zip. Use either manifest.json or one root-level .sheevactions file. If both exist, manifest.json takes priority.
my-library.sheevactionszip
manifest.json
brand-icon.png
This is also valid:
my-library.sheevactionszip
my-library.sheevactions
brand-icon.png
Packages may include a brand icon. SheevChat selects the first compatible image file found in the zip package and stores that relative path in the installed manifest's icon field. The file does not need to be named icon.
Compatible icon extensions:
.png
.jpg
.jpeg
.webp
.svg
.gif
Safe package rules:
manifest.jsonor one.sheevactionsfile must be at the package root.- If no
manifest.jsonexists, do not include multiple root.sheevactionsfiles. - Paths must stay inside the package. Do not use absolute paths, drive letters, or
../. - Keep packages small. Icons should be optimized for UI display.
- Existing
.sheevactionsplain JSON imports continue to work.
Current Schema Version
"schemaVersion": 1
Only schema version 1 is accepted.
Top-Level Manifest Shape
{
"schemaVersion": 1,
"id": "example-app",
"name": "Example App",
"version": "1.0.0",
"publisher": "Example Publisher",
"category": "Example Controls",
"description": "Adds Example App actions to SheevChat.",
"homepage": "https://example.com/docs",
"icon": "",
"trust": "third-party",
"permissions": ["local_websocket"],
"compatibility": {},
"connections": [],
"sources": {},
"actions": []
}
Top-Level Static Fields
| Field | Required | Type | Accepted / Notes |
|---|---|---|---|
schemaVersion | yes | number | Must be 1. |
id | yes | string | Lowercase letters, numbers, hyphens, or underscores. 3-64 chars. Must start and end with a letter or number. |
name | yes | string | User-facing library name. Max 120 chars. |
version | yes | string | Semantic-ish version such as 1.0.0, 1.2, or 1.0.0-beta.1. |
publisher | no | string | Displayed in action category as Category by Publisher. Max 120 chars. |
category | no | string | User-facing action category. Defaults to Community. Max 80 chars. |
description | no | string | Library summary. Max 700 chars. |
homepage | no | string | Documentation/homepage URL. Max 500 chars. |
icon | no | string | Relative packaged icon path. Max 500 chars. For .sheevactionszip, SheevChat can auto-fill this with the first compatible image file found in the package. |
permissions | no | array | See permissions below. |
compatibility | no | object | Freeform metadata. Currently stored, not enforced. |
trust | no | string | Freeform label such as local, third-party, community, or example-pack. |
connections | no | array | Connection definitions. |
sources | no | object | Dynamic option sources keyed by source id. Must be an object, not an array. |
actions | yes | array | Must contain at least one action. |
ID Rules
Manifest ids, connection ids, source ids, action ids, field keys, and extra keys are normalized with the same id rules:
^[a-z0-9][a-z0-9_-]{1,62}[a-z0-9]$
Good:
vtube-studio
trigger_hotkey
state_node_id
local
Bad:
VTubeStudio
trigger.hotkey
state node
_hidden
id!
Action Category Display
SheevChat displays imported action categories as:
<category> by <publisher>
Example:
{
"category": "Avatar Controls",
"publisher": "ExampleApp"
}
Displays as:
Avatar Controls by ExampleApp
Use exactly one top-level category. Do not create per-action categories.
Permissions
Accepted permission values:
| Permission | Meaning |
|---|---|
local_http | Manifest may call local HTTP APIs. |
local_websocket | Manifest may call local WebSocket APIs. |
local_ipc | Manifest may call approved local IPC/RPC transports such as hardened named-pipe protocol presets. |
local_udp | Manifest may send bounded UDP request/response packets to localhost or private LAN targets. |
local_tcp | Manifest may send bounded TCP request/response messages to localhost or private LAN targets. |
local_mqtt | Manifest may publish bounded MQTT/MQTTS messages to declared topics on localhost/private LAN brokers and manually subscribe to declared topics for diagnostics. |
local_discovery | Manifest may perform approved LAN/local service discovery such as mDNS/DNS-SD. |
oauth | Manifest may declare a safe OAuth 2.0 authorization-code-with-PKCE profile. Loopback PKCE login and token storage are supported for public clients. |
hotkey | Manifest may send bounded Windows keystroke sequences through SheevChat's macro sender. |
launch_application | Manifest may launch a user-selected local app/file through SheevChat's bounded launcher. |
read_file | Manifest may read small UTF-8/JSON files from its own SheevChat-managed library storage folder. |
write_file | Manifest may write small UTF-8/JSON files inside its own SheevChat-managed library storage folder. |
sheevchat_command | Manifest may trigger a SheevChat command. |
remote_network | Allows non-local HTTP/WebSocket hosts. Without this, remote URLs are rejected. |
For current third-party app packs, prefer:
"permissions": ["local_websocket"]
or:
"permissions": ["local_http"]
Only use remote_network when the manifest intentionally calls an internet host.
Remote/cloud HTTP packs should also declare a connection-level remote.allowedHosts list so SheevChat can pin requests to the expected API host. See Remote HTTP Policy.
Connections
Connections describe how SheevChat talks to the target app.
"connections": [
{
"id": "local",
"label": "Example App",
"type": "local_websocket",
"defaultUrl": "ws://127.0.0.1:12345",
"test": {
"url": "ws://127.0.0.1:12345"
}
}
]
Accepted Connection Types
| Type | Current behavior |
|---|---|
none | No app connection required. Status defaults to connected. |
local_http | Uses fetch against a local HTTP API. |
local_websocket | Opens a WebSocket, sends a message, optionally waits for response. |
local_ipc | Opens a validated local IPC endpoint and uses a hardened protocol preset. |
local_udp | Sends bounded UDP packets to local/LAN targets. |
local_tcp | Opens a short-lived TCP socket to a local/LAN target, sends a bounded message, optionally waits for a bounded response. |
local_mqtt | Connects to a local/LAN MQTT broker for bounded publish actions and manual subscriptions. |
oauth | OAuth profile connection. Status defaults to needs login until the user signs in through the loopback PKCE flow. |
hotkey | No app connection required. Status defaults to connected. Used with execute.type: "hotkey". |
Connection Fields
| Field | Required | Type | Notes |
|---|---|---|---|
id | yes | string | Stable connection id. Usually local. |
label | no | string | User-facing connection label. Defaults to id. |
type | no | string | Defaults to none. |
defaultUrl | no | string | Base URL such as http://127.0.0.1:3000 or ws://127.0.0.1:8001. |
fallbackUrls / urls | no | array | Ordered fallback base URLs. SheevChat tries defaultUrl first, then each fallback. Useful for apps that expose one of several documented local ports. |
provider | no | string | Metadata for OAuth/provider naming. |
scopes | no | array | Metadata for OAuth scopes. |
discovery | no | object | Supports generic instance file discovery. |
auth | no | object | Describes local app authentication for APIs that require a token or session handshake. |
remote | no | object | Remote/cloud HTTP safety policy. Use with remote_network for internet APIs. |
signing | no | object | Declarative request signing preset for HTTP APIs that require HMAC signatures. Secrets must come from saved credentials. |
preflight | no | object | First-message or setup request sent before source dropdowns/actions. Use this for app hello/register messages that do not return a reusable token. |
protocol | no | object | Required for local_ipc. Selects a hardened IPC protocol preset such as discord_rpc. |
tcp | no | object | Optional local_tcp defaults such as framing, response framing, timeout, and payload/response byte caps. |
test | no | object | Connection test settings. |
OAuth 2.0 Profiles
Use connection.type: "oauth" when an imported cloud pack needs a user authorization flow instead of a pasted API key. The manifest must declare the oauth permission and include a connection.oauth profile. SheevChat supports desktop authorization-code-with-PKCE login through a loopback callback, exchanges the returned code for tokens, and stores tokens locally in the same user-config auth state used by other imported-library credentials.
OAuth profiles are for desktop authorization-code-with-PKCE flows. Public clients use tokenAuthMethod: "none". Confidential clients use client_secret_post or client_secret_basic with a user-entered local credential. Do not place client secrets, access tokens, refresh tokens, authorization headers, or pre-issued bearer tokens in the manifest.
{
"permissions": ["oauth"],
"connections": [
{
"id": "cloud",
"label": "Example Cloud",
"type": "oauth",
"provider": "example",
"oauth": {
"flow": "authorization_code_pkce",
"authorizationUrl": "https://auth.example.com/oauth/authorize",
"tokenUrl": "https://auth.example.com/oauth/token",
"revokeUrl": "https://auth.example.com/oauth/revoke",
"clientId": "public-desktop-client-id",
"scopes": ["devices.read", "devices.write"],
"redirectMode": "loopback",
"pkce": true,
"tokenAuthMethod": "none"
}
}
]
}
OAuth Object Fields
| Field | Required | Type | Notes |
|---|---|---|---|
flow / type | no | string | Only authorization_code_pkce is accepted. Hyphen form is normalized. |
authorizationUrl / authorizeUrl / authUrl | yes | string | HTTPS authorize URL. Embedded credentials are rejected. |
tokenUrl | yes | string | HTTPS token URL. |
revokeUrl / revocationUrl | no | string | HTTPS revocation URL. |
clientId / client_id | yes | string | Public client id for desktop/PKCE use. |
scopes / scope | no | array or string | Arrays are preferred. Space-separated strings are split into scopes. |
redirectMode / redirect | no | string | loopback or relay. Defaults to loopback. |
pkce | no | boolean | Defaults to true. |
tokenAuthMethod / authMethod | no | string | none, client_secret_post, or client_secret_basic. Secret methods require a matching locally saved credential. |
clientSecretKey / clientSecretCredential / client_secret_key | no | string | Credential key to use for client_secret_post or client_secret_basic. Defaults to client_secret. The credential value must be entered locally by the user, never embedded in the manifest. |
extraAuthorizeParams / authorizeParams | no | object | Scalar extra authorize parameters such as audience or prompt. Secrets/tokens are rejected. |
extraTokenParams / tokenParams | no | object | Scalar extra token parameters. Secrets/tokens are rejected. |
extraRevokeParams / revokeParams / revocationParams | no | object | Scalar extra revoke parameters. Secrets/tokens are rejected. |
When the user clicks Authenticate for an OAuth connection, SheevChat opens the provider authorize URL with:
response_type=codeclient_idredirect_uri=http://localhost:{current-port}/api/action-libraries/oauth/callbackstatescopecode_challengecode_challenge_method=S256- optional
audience,resource,prompt, andextraAuthorizeParams
After callback, SheevChat exchanges the code at tokenUrl with grant_type=authorization_code, client_id, redirect_uri, code_verifier, and any extraTokenParams. Stored values are exposed to later runtime templates as {auth.access_token}, {auth.refresh_token}, {auth.token_type}, {auth.scope}, and {auth.expires_at}.
For providers that require a confidential OAuth client, declare a local credential and choose the provider's token auth method:
{
"permissions": ["oauth", "remote_network"],
"connections": [
{
"id": "cloud",
"type": "oauth",
"remote": { "allowedHosts": ["api.example.com"] },
"credentials": [
{ "key": "client_secret", "label": "Client Secret", "type": "password" }
],
"oauth": {
"authorizationUrl": "https://auth.example.com/authorize",
"tokenUrl": "https://auth.example.com/token",
"clientId": "desktop-client-id",
"tokenAuthMethod": "client_secret_basic"
}
}
]
}
client_secret_post sends client_id and client_secret in the form body. client_secret_basic sends them in an HTTP Basic Authorization header. The same method is used for authorization-code exchange, refresh, and revoke calls.
oauth_api actions reference an OAuth connection and run HTTP requests with the stored access token injected as an Authorization header. They use the same request shape as local_http actions: method, path/url, headers, query, body, json, bodyEncoding, responseMap, responseNamespace, and responseTemplate.
{
"id": "set_state",
"label": "Set State",
"connectionId": "cloud",
"execute": {
"type": "oauth_api",
"connectionId": "cloud",
"method": "POST",
"path": "/v1/state",
"body": { "enabled": true }
}
}
OAuth request rules:
- The user must authenticate the OAuth connection first.
- Runtime token exchange supports public PKCE clients with
tokenAuthMethod: "none"plus confidential-clientclient_secret_postandclient_secret_basicmethods when the secret is saved as a local credential. oauth_apirequests automatically addAuthorization: Bearer {auth.access_token}unless the manifest explicitly provides anAuthorizationheader.- Remote/cloud API hosts still use the normal remote HTTP safety policy. Include
remote_networkandconnection.remote.allowedHostswhen calling internet APIs. - If the stored access token is expired and
{auth.refresh_token}exists, SheevChat refreshes the token before running OAuth sources/actions. If refresh fails or no refresh token exists, the connection returns toneeds login. - If the provider returns no replacement refresh token during refresh, SheevChat preserves the existing stored refresh token.
- Clearing OAuth auth deletes SheevChat's local tokens. If
revokeUrlis present, SheevChat first makes a best-effort revocation request withtoken,token_type_hint,client_id, and anyextraRevokeParams; local cleanup still completes if the provider revoke call fails.
OAuth dynamic dropdown sources are supported with source.type: "connection_request" against an OAuth connection. This lets authenticated packs populate cloud-side lists before running an action.
Do not fake OAuth by asking users to paste access tokens into normal fields unless the provider officially documents API-key-style personal tokens.
Remote HTTP Policy
Use connection.remote for cloud APIs and internet-hosted webhooks. This makes remote action packs first-class and reviewable instead of relying on arbitrary full URLs inside individual actions.
Remote HTTP rules:
- The top-level manifest must include
remote_network. - Remote HTTP URLs must use HTTPS by default.
- Remote URLs cannot contain embedded credentials such as
https://user:pass@example.com. - Remote hosts must match the connection's
remote.allowedHostsor the remote host indefaultUrl/fallbackUrls. - Redirects are followed manually and capped. Cross-host redirects are rejected so credential-bearing headers are not forwarded to another host.
- Responses are size-limited before parsing.
- HTTP
429responses are surfaced as rate-limit feedback, includingRetry-Afterwhen present.
{
"permissions": ["local_http", "remote_network"],
"connections": [
{
"id": "govee",
"label": "Govee Cloud",
"type": "local_http",
"defaultUrl": "https://openapi.api.govee.com",
"remote": {
"allowedHosts": ["openapi.api.govee.com"],
"maxResponseBytes": 1048576,
"maxRedirects": 2
},
"credentials": [
{
"key": "govee_api_key",
"label": "Govee API Key",
"type": "password",
"required": true
}
],
"test": {
"method": "GET",
"path": "/router/api/v1/user/devices",
"headers": {
"Govee-API-Key": "{auth.govee_api_key}"
}
}
}
]
}
Remote Object Fields
| Field | Required | Type | Notes |
|---|---|---|---|
allowedHosts | recommended | array | Exact remote hostnames, such as api.example.com. Do not include protocol, paths, query strings, ports, or credentials. Alias: hosts. |
allowInsecureHttp | no | boolean | Defaults to false. Only use true for a documented local/private API that cannot use TLS. Marketplace cloud packs should not use it. |
maxResponseBytes | no | number | Maximum response body size. Defaults to 1 MB and is capped at 5 MB. |
maxRedirects | no | number | Redirect limit. Defaults to 3 and is capped at 5. |
For local APIs, do not add remote_network or connection.remote.
Local HTTP Connections
Use local_http for REST-like APIs.
Imported action libraries intentionally support only interactive HTTP methods:
GET
POST
PUT
PATCH, DELETE, HEAD, and other methods are rejected. SheevChat action libraries should query or control apps, not remove records or perform arbitrary destructive mutation.
{
"id": "local",
"label": "Example REST API",
"type": "local_http",
"defaultUrl": "http://127.0.0.1:4567",
"test": {
"url": "/health",
"method": "GET"
}
}
Relative URLs are resolved against defaultUrl.
HTTP Body Encoding
HTTP requests default to JSON bodies. For token endpoints, webhook-style APIs, and desktop apps that expect non-JSON payloads, add bodyEncoding to a non-GET request.
Supported values:
| Value | Body behavior | Default content type |
|---|---|---|
json | Object bodies are sent as JSON. String bodies pass through as-is. | application/json |
form | Object bodies are sent as URL-encoded form data. Array values repeat the same key. | application/x-www-form-urlencoded |
form_urlencoded | Alias for form. | application/x-www-form-urlencoded |
urlencoded | Alias for form. | application/x-www-form-urlencoded |
text | Body is sent as plain text. | text/plain; charset=utf-8 |
raw | Alias for text. | text/plain; charset=utf-8 |
If a request already declares a Content-Type header, SheevChat preserves it. bodyEncoding is ignored for GET requests and local import diagnostics will warn about that mismatch.
{
"method": "POST",
"url": "/oauth/token",
"bodyEncoding": "form",
"body": {
"grant_type": "refresh_token",
"refresh_token": "{auth.refresh_token}"
}
}
Local UDP Connections
Use local_udp for local or LAN device APIs that communicate through UDP request/response packets. This runtime is intentionally bounded for safety:
- The manifest must include
local_udp. - Connection URLs must use
udp://host:port. - Targets must be localhost, RFC1918 private LAN addresses, or
.localhostnames. - Broadcast and multicast targets require explicit opt-in with
udp.allowBroadcastorudp.allowMulticast. - Packets and responses are size-limited.
- Retries are capped.
- Sources/actions may parse UDP responses into text, JSON, hex, or base64.
{
"permissions": ["local_udp"],
"connections": [
{
"id": "local",
"label": "LAN Device",
"type": "local_udp",
"defaultUrl": "udp://192.168.1.50:4001",
"udp": {
"retries": 1,
"timeoutMs": 2500,
"maxPacketBytes": 8192,
"maxResponseBytes": 8192
},
"test": {
"payload": { "event": "ping" },
"encoding": "json",
"responseEncoding": "json"
}
}
]
}
UDP Connection Fields
| Field | Required | Type | Notes |
|---|---|---|---|
defaultUrl | usually | string | udp://host:port. The host must be local/LAN. |
udp.retries | no | number | Integer from 0 to 5. Defaults to 0. |
udp.timeoutMs | no | number | Timeout from 250ms to 10000ms. Defaults to 2500ms. |
udp.maxPacketBytes | no | number | Outgoing packet cap from 64 to 65507 bytes. Defaults to 8192. |
udp.maxResponseBytes | no | number | Incoming response cap from 64 to 65507 bytes. Defaults to 65507. |
udp.allowBroadcast | no | boolean | Required for broadcast targets such as udp://255.255.255.255:4001 or subnet broadcasts ending in .255. Defaults to false. |
udp.allowMulticast | no | boolean | Required for multicast targets in 224.0.0.0/4. Defaults to false. |
udp.multicastTtl | no | number | Multicast TTL from 1 to 8. Defaults to 1. Keep this low for LAN discovery. |
UDP Request Fields
UDP requests can appear in connection.test, sources.<id>.request, or actions[].execute.
| Field | Required | Type | Notes |
|---|---|---|---|
url / path | no | string | Optional udp://host:port override. Most packs use the connection defaultUrl. |
packet / message / payload / body | yes | any | Packet contents. Objects are normally encoded as JSON. |
encoding / payloadEncoding | no | string | json, text, hex, or base64. Defaults to json for objects and text for strings. |
responseEncoding / parse | no | string | json, text, hex, or base64. Defaults to json. |
awaitResponse | no | boolean | Defaults to true. Set false for fire-and-forget controls. Query actions must wait for a response. |
retries | no | number | Overrides connection retry count for this request. 0-5. |
timeoutMs | no | number | Overrides request timeout. 250-10000ms. |
maxPacketBytes | no | number | Overrides outgoing packet cap. |
maxResponseBytes | no | number | Overrides incoming response cap. |
responseFromAny | no | boolean | Defaults to false. Leave off unless the protocol documents replies from a different address/port. |
UDP Broadcast And Multicast Discovery
Use broadcast or multicast only for documented LAN discovery protocols. The connection must still use local_udp, and the target must be a UDP URL with an explicit port.
{
"permissions": ["local_udp"],
"connections": [
{
"id": "discovery",
"type": "local_udp",
"defaultUrl": "udp://255.255.255.255:4001",
"udp": {
"allowBroadcast": true,
"timeoutMs": 1500,
"retries": 1,
"maxPacketBytes": 512,
"maxResponseBytes": 8192
},
"test": {
"message": "discover",
"encoding": "text",
"responseEncoding": "json",
"responseFromAny": true
}
}
]
}
For multicast:
{
"defaultUrl": "udp://239.255.0.1:4001",
"udp": {
"allowMulticast": true,
"multicastTtl": 1
}
}
Discovery responses often come from a device-specific IP/port rather than the discovery destination. In those cases, set responseFromAny: true on the specific test/source/action request that expects discovery replies. Do not set it on ordinary device-control actions unless the protocol requires it.
Example action:
{
"id": "toggle_power",
"label": "Toggle Power",
"connectionId": "local",
"fields": [],
"execute": {
"type": "local_udp",
"connectionId": "local",
"payload": { "cmd": "toggle" },
"encoding": "json",
"awaitResponse": false
}
}
Local MQTT Connections
Use local_mqtt for local or LAN MQTT brokers, such as smart-home bridges and IoT tools that expose a documented MQTT control API. This runtime is intentionally bounded for safety:
- The manifest must include
local_mqtt. - Broker URLs must use
mqtt://ormqtts://. - Broker hosts must be localhost, RFC1918 private LAN addresses, or
.localhostnames. - Broker URLs cannot contain embedded credentials.
- Credentials must be declared with
connection.credentialsand referenced through{auth.<key>}placeholders. - Publish and subscribe topics must be allowlisted in
connection.mqtt.topics. - Publish actions are fire-and-forget. MQTT query/request-response actions are not supported yet.
- Payloads are capped and can be encoded as JSON, text, hex, or base64.
- Retained publishes are disabled unless the connection explicitly sets
mqtt.allowRetain: true.
{
"permissions": ["local_mqtt"],
"connections": [
{
"id": "mqtt",
"label": "Local MQTT Broker",
"type": "local_mqtt",
"defaultUrl": "mqtt://127.0.0.1:1883",
"credentials": [
{
"key": "username",
"label": "Username",
"type": "text",
"required": false
},
{
"key": "password",
"label": "Password",
"type": "password",
"required": false
}
],
"mqtt": {
"clientId": "sheevchat-{library.id}",
"username": "{auth.username}",
"password": "{auth.password}",
"topics": {
"publish": ["lights/+/set"],
"subscribe": ["lights/+/state"]
},
"qos": 0,
"timeoutMs": 3000,
"maxPayloadBytes": 65536
}
}
]
}
MQTT Connection Fields
| Field | Required | Type | Notes |
|---|---|---|---|
defaultUrl | yes | string | mqtt://host:port or mqtts://host:port. The host must be local/LAN. |
credentials | no | array | Declare username/password/API-key fields here. Never embed credential values in defaultUrl. |
mqtt.clientId | no | string | Client id sent to the broker. Supports placeholders such as {library.id}. |
mqtt.username | no | string | Broker username. Usually a {auth.username} placeholder. |
mqtt.password | no | string | Broker password/API key. Usually a {auth.password} placeholder. |
mqtt.clean | no | boolean | Defaults to true. |
mqtt.keepalive | no | number | Keepalive seconds from 0 to 120. |
mqtt.timeoutMs | no | number | Connection timeout from 500ms to 30000ms. Defaults to 3000ms. |
mqtt.protocolVersion | no | number | 4 or 5. Defaults to the MQTT library default. |
mqtt.rejectUnauthorized | no | boolean | Defaults to true. Set false only for documented local self-signed brokers. |
mqtt.qos | no | number | Default QoS from 0 to 2. |
mqtt.allowRetain | no | boolean | Required before any action can publish retained messages. Defaults to false. |
mqtt.maxPayloadBytes | no | number | Payload cap from 64 bytes to 1 MB. Defaults to 64 KB. |
mqtt.topics.publish | yes for publish actions | array | Topic filters that publish actions may target. MQTT wildcards are allowed here as allowlist filters. |
mqtt.topics.subscribe | yes for subscriptions | array | Topic filters that manual event subscriptions may listen to. |
MQTT Publish Action
{
"id": "set_light",
"label": "Set Light",
"connectionId": "mqtt",
"fields": [
{ "key": "light", "label": "Light", "type": "text" },
{ "key": "enabled", "label": "On", "type": "checkbox" }
],
"execute": {
"type": "local_mqtt",
"connectionId": "mqtt",
"topic": "lights/{light}/set",
"payload": {
"on": "{enabled}"
},
"encoding": "json",
"qos": 0
}
}
MQTT Request Fields
MQTT publish requests can appear in actions[].execute. connection.test may connect to the broker, but it does not publish unless used by a future explicit test payload.
| Field | Required | Type | Notes |
|---|---|---|---|
topic / publishTopic | yes | string | Concrete publish topic. It cannot contain MQTT wildcards after placeholders resolve. |
payload / message / body / json | no | any | Payload contents. Objects are normally encoded as JSON. |
encoding / payloadEncoding | no | string | json, text, hex, or base64. Defaults to json for objects and text for strings. |
qos | no | number | 0, 1, or 2. Defaults to connection.mqtt.qos or 0. |
retain | no | boolean | Only honored when connection.mqtt.allowRetain is true. |
maxPayloadBytes | no | number | Optional lower payload cap for this request. |
Do not set query: true or awaitResponse: true on MQTT publish actions. MQTT request/response patterns need a future bounded correlation design before they can be safely used as query outputs. For the same reason, local_mqtt does not support sources.<id>.type: "connection_request" dropdown sources yet.
Local TCP Connections
Use local_tcp for local or LAN desktop/device APIs that communicate through plain TCP sockets. This is useful for simple line-delimited command protocols, local control bridges, and LAN tools that do not expose HTTP, WebSocket, UDP, MQTT, or a supported IPC preset.
The runtime is intentionally bounded:
- The manifest must include
local_tcp. - Connection URLs must use
tcp://host:port. - Targets must be localhost, RFC1918 private LAN addresses, or
.localhostnames. - TCP URLs cannot contain embedded credentials.
- Payloads and responses are size-limited.
- Requests can be raw bytes or line-framed messages.
- Payloads and responses can be encoded as JSON, text, hex, or base64.
- Query actions and dynamic dropdown sources are supported only when
awaitResponseis true.
{
"permissions": ["local_tcp"],
"connections": [
{
"id": "local",
"label": "Local TCP App",
"type": "local_tcp",
"defaultUrl": "tcp://127.0.0.1:9000",
"tcp": {
"framing": "line",
"responseFraming": "line",
"timeoutMs": 3000,
"maxPayloadBytes": 65536,
"maxResponseBytes": 65536
},
"test": {
"payload": { "event": "ping" },
"encoding": "json",
"responseEncoding": "json"
}
}
]
}
TCP Connection Fields
| Field | Required | Type | Notes |
|---|---|---|---|
defaultUrl | usually | string | tcp://host:port. The host must be local/LAN. |
fallbackUrls / urls | no | array | Ordered tcp://host:port fallbacks. Useful for apps that bind one of several documented ports. |
tcp.timeoutMs | no | number | Timeout from 250ms to 30000ms. Defaults to 3000ms. |
tcp.framing | no | string | raw or line. Defaults to raw. line appends \n when the payload does not already end in a newline. |
tcp.responseFraming | no | string | raw or line. Defaults to the request framing. line resolves after the first newline-delimited response frame. |
tcp.maxPayloadBytes | no | number | Outgoing payload cap from 64 bytes to 1 MB. Defaults to 64 KB. |
tcp.maxResponseBytes | no | number | Incoming response cap from 64 bytes to 1 MB. Defaults to 64 KB. |
TCP Request Fields
TCP requests can appear in connection.test, sources.<id>.request, eventSubscriptions[].subscribe, or actions[].execute.
| Field | Required | Type | Notes |
|---|---|---|---|
url / path | no | string | Optional tcp://host:port override. Most packs use the connection defaultUrl. |
packet / message / payload / body | yes | any | Message contents. Objects are normally encoded as JSON. |
encoding / payloadEncoding | no | string | json, text, hex, or base64. Defaults to json for objects and text for strings. |
framing | no | string | raw or line. Overrides connection.tcp.framing. |
responseEncoding / parse | no | string | json, text, hex, or base64. Defaults to json. |
responseFraming | no | string | raw or line. Overrides connection.tcp.responseFraming. |
awaitResponse | no | boolean | Defaults to true. Set false for fire-and-forget controls. Query actions and dropdown sources must wait for a response. |
timeoutMs | no | number | Overrides request timeout. 250-30000ms. |
maxPayloadBytes | no | number | Overrides outgoing payload cap. |
maxResponseBytes | no | number | Overrides incoming response cap. |
Example action:
{
"id": "set_mode",
"label": "Set Mode",
"connectionId": "local",
"fields": [
{ "key": "mode", "label": "Mode", "type": "text" }
],
"execute": {
"type": "local_tcp",
"connectionId": "local",
"payload": { "event": "set_mode", "mode": "{mode}" },
"encoding": "json",
"framing": "line",
"awaitResponse": false
}
}
Example dynamic source:
"sources": {
"modes": {
"type": "connection_request",
"connectionId": "local",
"request": {
"payload": { "event": "list_modes" },
"encoding": "json",
"framing": "line",
"responseEncoding": "json",
"responseFraming": "line"
},
"map": {
"root": "$.modes",
"value": "$.id",
"label": "$.name"
}
}
}
Connection Preflight / Hello Messages
Use connection.preflight when a local HTTP or WebSocket API requires a setup message before normal requests, but does not have a token/session authentication flow. This is common for desktop app control APIs that require a client registration, hello message, or app key as the first WebSocket message.
Preflight is different from connection.auth:
authexpects SheevChat to prove authentication succeeded throughstatusMap,tokenMap, orsessionMap.preflightsends setup messages before sources/actions and does not require an authenticated response.
When To Use Preflight, Auth, Or Both
| API behavior | Use | Why |
|---|---|---|
| The app requires a hello/register/client-key message as the first WebSocket message, then normal commands work. | connection.preflight | There is no token to store and no authenticated boolean to prove. |
| The app returns a reusable token that must be saved locally. | connection.auth.tokenRequest + tokenMap | SheevChat maps and stores returned values as {auth.token} or {auth.key}. |
| The app returns a token and each new WebSocket session must authenticate with that token. | connection.auth.tokenRequest + tokenMap + sessionRequest + sessionMap | SheevChat stores the token, then sends session auth before sources/actions. |
| The app can report whether the current session is already authenticated. | connection.auth.statusRequest + statusMap | SheevChat can show authenticated without requesting a new token. |
| The app requires both a first-message hello and later token/session auth. | connection.preflight + connection.auth | Preflight runs first, auth/session requests run after it. |
Do not force a hello/register API into connection.auth unless the API actually returns an auth result that can be mapped to authenticated, connected, ok, or success.
Fallback URLs / Multiple Local Ports
Use fallbackUrls when a desktop app may bind to one of several local ports. SheevChat tries the connection in this order:
defaultUrl- each URL in
fallbackUrls
Fallback URLs are sufficient when the app uses a small documented port range. They do not replace endpoint-file discovery when the active port can be arbitrary. Some apps also require WebSocket handshake headers such as Origin; a manifest cannot emulate those headers unless the core connection schema and runtime explicitly support them.
The first URL that opens and completes the request is used for that operation. This is generic and not tied to any specific app.
"connections": [
{
"id": "local",
"label": "Local App",
"type": "local_websocket",
"defaultUrl": "ws://127.0.0.1:59129/v1/",
"fallbackUrls": [
"ws://127.0.0.1:20000/v1/",
"ws://127.0.0.1:39273/v1/"
],
"preflight": {
"beforeSources": true,
"beforeActions": true,
"awaitResponse": false,
"request": {
"id": "register-sheevchat",
"action": "registerClient",
"payload": {
"clientKey": "public-developer-client-key"
}
}
}
}
]
| Field | Required | Type | Meaning |
|---|---|---|---|
request | yes, unless requests is used | object | Single preflight request. |
requests | yes, unless request is used | array | Multiple preflight requests in order. |
beforeSources | no | boolean | Defaults to true. Sends preflight before dynamic dropdown source requests. |
beforeActions | no | boolean | Defaults to true. Sends preflight before action execution. |
awaitResponse | no | boolean | Defaults to false. Set true only when the app always replies and the reply should be received before the next request. |
For WebSockets, preflight requests are sent on the same socket immediately before the source/action request, so APIs that require “first message must be registration” work correctly.
Match the Intended WebSocket Response
A WebSocket connection can receive more than the direct reply to the request SheevChat just sent. Apps may emit an initial server greeting, registration acknowledgement, status event, subscription update, or another unsolicited message on the same socket. A valid map.rows or responseMap path will still fail if SheevChat applies it to one of those unrelated frames.
When the API can emit multiple message shapes, add responseMatch to every dynamic source request and awaited query action. Match a stable response discriminator documented by the API, such as an action type, event name, request id, or channel. SheevChat ignores non-matching frames and waits for the intended response before applying map.rows or responseMap.
Dynamic source example:
"request": {
"id": "sheevchat-voicemod",
"action": "getVoices",
"payload": {},
"responseMatch": {
"$.actionType": "getVoices"
}
}
Awaited query action example:
"execute": {
"type": "local_websocket",
"connectionId": "local",
"action": "getCurrentVoice",
"payload": {},
"awaitResponse": true,
"responseMatch": {
"$.actionType": "getCurrentVoice"
},
"responseNamespace": "voicemod",
"responseMap": {
"voice_id": {
"path": "$.actionObject.voiceID",
"default": ""
}
}
}
Choose the narrowest stable discriminator available. Do not match fields whose values change between calls. If the protocol echoes a unique request id, prefer that id; otherwise use a documented message type such as $.actionType. A preflight with awaitResponse: false makes response matching especially important because its acknowledgement or the app's greeting may arrive before the requested data.
When an empty_rows_path warning reports top-level keys that do not belong to the expected response, first confirm that the wrong WebSocket frame was selected. Do not change a documented rows path merely to fit a greeting or status frame.
Voicemod-Style Example
Voicemod's Control API requires a registerClient message first, but that is not token/session auth. Model it as preflight:
{
"id": "local",
"label": "Voicemod",
"type": "local_websocket",
"defaultUrl": "ws://127.0.0.1:59129/v1/",
"fallbackUrls": [
"ws://127.0.0.1:20000/v1/",
"ws://127.0.0.1:39273/v1/",
"ws://127.0.0.1:42152/v1/",
"ws://127.0.0.1:43782/v1/",
"ws://127.0.0.1:46667/v1/",
"ws://127.0.0.1:35679/v1/",
"ws://127.0.0.1:37170/v1/",
"ws://127.0.0.1:38501/v1/",
"ws://127.0.0.1:33952/v1/",
"ws://127.0.0.1:30546/v1/"
],
"test": {},
"preflight": {
"beforeSources": true,
"beforeActions": true,
"awaitResponse": false,
"request": {
"id": "register-sheevchat",
"action": "registerClient",
"payload": {
"clientKey": "controlapi-example"
}
}
}
}
Then a normal action can send only the actual app command:
"execute": {
"type": "local_websocket",
"connectionId": "local",
"id": "load-voice",
"action": "loadVoice",
"payload": {
"voiceID": "{voice_id}"
}
}
Local WebSocket Connections
Use local_websocket for local WebSocket APIs.
{
"id": "local",
"label": "Example WebSocket API",
"type": "local_websocket",
"defaultUrl": "ws://127.0.0.1:8001",
"websocket": {
"origin": "streamdeck://"
},
"test": {
"message": { "id": 100, "jsonrpc": "2.0", "method": "getApplicationInfo", "params": null },
"awaitResponse": true,
"responseMatch": { "$.id": 100 }
}
}
If test.message is omitted, Local WebSocket tests only check whether the socket opens. That does not prove protocol compatibility. When the app has a cheap info/ping request, include it in test.message with awaitResponse and responseMatch.
Use connection.websocket.origin when an app requires a WebSocket Origin during the opening handshake. Use connection.websocket.headers only for safe custom headers. Do not place Origin, Host, Connection, Upgrade, Sec-WebSocket-*, Content-Length, or Transfer-Encoding inside headers; those are controlled by the WebSocket transport. Header values are not logged.
The same WebSocket handshake settings apply to:
- connection tests
- dynamic source requests
- preflight sequences
- local auth/session requests
- query actions
- non-query actions
If the target app requires authentication, define auth so SheevChat can authenticate before resolving dynamic dropdowns or running actions.
Local IPC Connections
Use local_ipc when an app exposes a local IPC/RPC endpoint instead of HTTP or WebSocket. The first supported transport is Windows named pipes through hardened protocol presets. Do not use local_ipc for arbitrary files, device paths, remote pipe hosts, or custom native code.
Local IPC requires:
"permissions": ["local_ipc"]
Discord desktop RPC example:
{
"id": "discord",
"type": "local_ipc",
"discovery": {
"type": "named_pipe_range",
"template": "discord-ipc-{index}",
"start": 0,
"end": 9
},
"protocol": {
"type": "discord_rpc",
"clientId": "{auth.client_id}"
}
}
Named-Pipe Discovery
named_pipe_range expands a local pipe-name template such as discord-ipc-{index}. The template must resolve to simple local names only. SheevChat rejects slashes, backslashes, drive letters, traversal, remote pipe hosts, null bytes, and unrestricted device paths.
| Field | Required | Description |
|---|---|---|
type | yes | Must be named_pipe_range. |
template | yes | Local pipe-name template containing {index}. |
start | no | First integer index. Defaults to 0. |
end | no | Last integer index. Defaults to start. |
Protocol Presets
local_ipc separates transport from protocol. The manifest can select a supported protocol preset, but it cannot define arbitrary binary parsing logic.
Currently supported:
| Protocol | Purpose |
|---|---|
discord_rpc | Discord desktop RPC opcode/header framing, handshake, nonce correlation, and dispatch-event handling. |
Discord RPC uses bounded binary frames containing JSON payloads. SheevChat correlates responses by nonce and ignores unrelated dispatch events while a request is pending.
Discord's supported desktop RPC transport is IPC. Discord's localhost WebSocket RPC is deprecated and unavailable to new integrations. If a manifest points Discord controls at localhost WebSocket ports, SheevChat reports a warning and recommends local_ipc with protocol.type = "discord_rpc".
Discord RPC requires a Discord application client ID and OAuth authorization before commands can run. Do not place secrets or user access tokens directly in the manifest. Use SheevChat connection storage for credentials/tokens. Runtime errors should tell the user whether Discord is closed, no matching pipe was found, the handshake failed, authorization is required, the command is unsupported, or the request timed out.
When IPC requests fail, SheevChat shows a sanitized user-safe message and includes a short developer-facing errorCode in the connection status. The code is useful for debugging imported packs, while tokens, authorization headers, secrets, and raw sensitive payload values remain redacted.
Security restrictions:
- Only approved protocol presets are accepted.
- Pipe targets must be validated local names.
- Arbitrary filesystem paths, traversal, remote hosts, environment expansion, and unrestricted device paths are rejected.
- Frames and responses are size bounded.
- Connection and request timeouts are enforced.
- Tokens, secrets, raw Authorization headers, and sensitive IPC payloads are not logged.
- Complex binary protocols should be implemented as hardened presets, not untrusted manifest-defined parsers.
Manual Connection Credentials
Use connection.credentials when a library needs a user-entered API key, bearer token, webhook key, client identifier, or similar connection-level value. Credentials are stored locally by SheevChat and are not embedded in the manifest, exported with the pack, or returned to the renderer after saving.
Manual credentials are best for services such as Govee Cloud API keys, Lumia local API tokens, IFTTT webhook keys, and other APIs where the user obtains a value outside SheevChat and pastes it once.
{
"id": "cloud",
"label": "Govee Cloud",
"type": "local_http",
"defaultUrl": "https://openapi.api.govee.com",
"credentials": [
{
"key": "govee_api_key",
"label": "Govee API Key",
"type": "password",
"required": true,
"placeholder": "Paste API key",
"help": "Create this in the Govee Developer portal."
}
],
"test": {
"method": "GET",
"path": "/router/api/v1/user/devices",
"headers": {
"Govee-API-Key": "{auth.govee_api_key}"
}
}
}
Credential Fields
| Field | Required | Type | Notes |
|---|---|---|---|
key | yes | string | Placeholder key. Must be unique within the connection. Use lowercase snake case, such as govee_api_key. |
label | no | string | User-facing field label. Defaults to the key. |
type | no | string | password or text. Defaults to password. Use password for API keys, bearer tokens, webhook secrets, and session-like values. |
required | no | boolean | Defaults to true. Required credentials must be saved before tests, dropdowns, or actions can run. |
placeholder | no | string | Placeholder shown before a value is saved. |
help | no | string | Short user-facing helper text. |
Credential definitions must never include a value. SheevChat rejects manifest fields such as value, default, defaultValue, secret, token, apiKey, and api_key inside each credential object. Marketplace packs should only declare what the user needs to enter.
Credential Placeholders
Saved credential values are available through the same auth placeholder namespace used by local app authentication:
| Placeholder | Meaning |
|---|---|
{auth.govee_api_key} | Saved value for the govee_api_key credential. |
{govee_api_key} | Convenience alias for the same saved value. Prefer the {auth.*} form. |
Credentials can be used in approved connection URL, query, header, WebSocket message, and body positions. This includes connection.defaultUrl, connection.fallbackUrls, connection.test.url, and request/action URLs.
{
"method": "POST",
"url": "https://maker.ifttt.com/trigger/{event_name}/with/key/{auth.ifttt_key}",
"json": {
"value1": "{message}"
}
}
For remote/cloud hosts, the manifest must also request remote_network, and the URL must pass the current runtime's remote-host validation.
Editable local ports should use a non-required text credential with a normal fallback URL. SheevChat tries the interpolated URL first when the user has saved a value, then falls back to the documented default:
{
"id": "local",
"label": "Local WebSocket App",
"type": "local_websocket",
"defaultUrl": "ws://127.0.0.1:{auth.websocket_port}/",
"fallbackUrls": ["ws://127.0.0.1:8081/"],
"credentials": [
{
"key": "websocket_port",
"label": "WebSocket Port",
"type": "text",
"required": false,
"placeholder": "8081",
"help": "Use this only if the app's WebSocket server is configured to a different port."
}
]
}
Declarative HTTP Request Signing
Use connection.signing when an HTTP API requires a predictable signature on every request. This is for approved signing presets only. It does not run manifest code, and it does not allow secrets to be embedded in the pack.
Current supported preset:
| Field | Required | Type | Accepted / Notes |
|---|---|---|---|
type | no | string | Currently hmac_sha256. Defaults to hmac_sha256. |
secret / secretTemplate | yes | string | Must reference a locally saved credential such as {auth.client_secret}. Do not put literal secrets in manifests. |
payload / template | usually | string | String to sign after placeholder substitution. Required unless canonical is configured. |
canonical | no | string/object | Approved canonical request preset. Currently supports tuya_v2. |
target | no | string | header, query, or body. Defaults to header. |
name | yes | string | Header name, query parameter name, or object body field that receives the signature. Aliases: header, query, field. |
encoding | no | string | hex, base64, or base64url. Defaults to hex. |
case / outputCase | no | string | preserve, upper, or lower. Defaults to preserve. |
prefix | no | string | Optional text prepended to the signature value, such as sha256=. |
bodyHashEncoding | no | string | Encoding for {signing.bodySha256} / {body.sha256}. Defaults to hex. |
timestamp | no | boolean/object | true enables {signing.timestamp} using unix_ms. Object accepts { "format": "unix_ms" }, { "format": "unix" }, or { "format": "iso" }. |
nonce | no | boolean/object | true enables {signing.nonce} using 16 random bytes as hex. Object accepts bytes from 8-64 and encoding of hex, base64, or base64url. |
Signing placeholders available inside payload:
| Placeholder | Meaning |
|---|---|
{request.method} | Final HTTP method, uppercased. |
{request.url} | Final target URL before signature injection. |
{request.path} | URL path plus query string. |
{request.pathname} | URL path only. |
{request.query} | Query string without leading ?. |
{request.body} | Exact string body SheevChat will send. Empty for GET/HEAD or no body. This respects bodyEncoding, so form bodies are signed as URL-encoded text instead of JSON. |
{body.sha256} / {signing.bodySha256} | SHA-256 hash of {request.body}. |
{signing.timestamp} | Generated timestamp when timestamp is enabled. |
{signing.nonce} | Generated nonce when nonce is enabled. |
{auth.<key>} | Saved credential or local auth value. Use this for signing secrets and related client ids/tokens. |
Canonical Request Presets
Use a canonical preset when an API documents a fixed request-signing recipe that should not be hand-assembled in every pack. Presets still use the same credential, timestamp, nonce, and target rules as normal signing.
tuya_v2
The tuya_v2 preset builds the Tuya-style HMAC payload:
client_id + access_token + timestamp + nonce + stringToSign
Where stringToSign is:
METHOD
bodySha256
signedHeaderBlock
canonicalPathAndSortedQuery
Use includeAccessToken: false for token acquisition endpoints that do not have an access token yet. Use signedHeaders only when the target API explicitly requires extra headers in the canonical string.
{
"signing": {
"type": "hmac_sha256",
"secret": "{auth.client_secret}",
"canonical": {
"preset": "tuya_v2",
"clientId": "{auth.client_id}",
"accessToken": "{auth.access_token}",
"timestamp": "{signing.timestamp}",
"nonce": "{signing.nonce}",
"includeAccessToken": true
},
"target": "header",
"name": "sign",
"encoding": "hex",
"case": "upper",
"timestamp": { "format": "unix_ms" },
"nonce": { "bytes": 16, "encoding": "hex" }
}
}
Then declare the visible API headers in each request:
{
"method": "POST",
"path": "/v1.0/devices/{device_id}/commands",
"headers": {
"client_id": "{auth.client_id}",
"access_token": "{auth.access_token}",
"t": "{signing.timestamp}",
"nonce": "{signing.nonce}"
},
"body": {
"commands": [
{ "code": "switch_led", "value": true }
]
}
}
For token requests:
{
"canonical": {
"preset": "tuya_v2",
"clientId": "{auth.client_id}",
"timestamp": "{signing.timestamp}",
"nonce": "{signing.nonce}",
"includeAccessToken": false
}
}
If the API requires timestamp, nonce, client id, or token values in headers/query/body, add those placeholders to the request itself:
{
"permissions": ["local_http", "remote_network"],
"connections": [
{
"id": "cloud",
"label": "Signed Cloud API",
"type": "local_http",
"defaultUrl": "https://api.example.com",
"remote": { "allowedHosts": ["api.example.com"] },
"credentials": [
{ "key": "client_id", "label": "Client ID", "type": "text" },
{ "key": "client_secret", "label": "Client Secret", "type": "password" }
],
"signing": {
"type": "hmac_sha256",
"secret": "{auth.client_secret}",
"payload": "{request.method}\n{request.path}\n{body.sha256}\n{signing.timestamp}",
"target": "header",
"name": "X-Signature",
"encoding": "hex",
"case": "upper",
"timestamp": { "format": "unix_ms" }
}
}
],
"actions": [
{
"id": "set_power",
"label": "Set Power",
"connectionId": "cloud",
"fields": [],
"execute": {
"type": "local_http",
"connectionId": "cloud",
"method": "POST",
"path": "/v1/device/power",
"headers": {
"X-Client-ID": "{auth.client_id}",
"X-Timestamp": "{signing.timestamp}"
},
"body": { "on": true }
}
}
]
}
Important limits:
- This section only signs HTTP requests. For refreshable access tokens, use
connection.auth.refreshRequest/refreshMapin the token lifecycle section below. - Body-target signing can only inject into object bodies. Use header or query signing for text bodies.
- Marketplace review should reject packs with literal signing secrets, even if local import diagnostics already catch this for developers.
Clock Skew Feedback
Some signed APIs reject otherwise-correct requests when the user's PC clock is several minutes away from the API server clock. Pack authors do not need to declare a special field for this. When a signed HTTP request receives 401 or 403, SheevChat compares the response's standard HTTP Date header to the local clock. If the clocks differ by more than about five minutes, the runtime adds a user/developer-facing hint that the PC clock may need to be corrected.
Runtime Secret Redaction
When a connection uses connection.credentials, connection.auth, or {auth.<key>} placeholders, SheevChat treats those runtime values as secrets. If a request fails after interpolation, SheevChat redacts saved auth values and their URL-encoded forms before storing connection status details or returning action-library API errors to the UI.
Pack authors should still avoid putting secret-bearing values in friendly response templates, labels, descriptions, static options, or output variables. Redaction is a runtime safety net for failures and diagnostics, not a reason to expose credentials through normal manifest metadata.
For best diagnostics, APIs should return a normal Date response header on authentication/signature failures.
Local App Authentication
Use connection.auth when a local app requires a token, session authentication, or pairing approval before source dropdowns or actions can work. This is for local HTTP/WebSocket/IPC/UDP app APIs, not platform OAuth. The manifest describes the handshake; SheevChat stores returned local tokens in the user's local config, keyed by library and connection.
Typical flow:
- User expands Action Libraries details.
- User clicks Test to confirm the app is reachable.
- User clicks Authenticate.
- SheevChat sends the token request, maps the returned token, stores it locally, then sends the session request.
- Future dropdowns and actions automatically run the session request first when required.
For device-code or approval-code flows, connection.auth.pairing can start pairing, show the user code/verification URL, poll in the background, then store the returned token as normal {auth.*} values.
Auth Object Fields
| Field | Required | Type | Notes |
|---|---|---|---|
type | no | string | Metadata. Use local_session unless a more specific label helps the library author. |
label | no | string | User-facing auth label shown in details. |
buttonLabel | no | string | Button text. Defaults to Authenticate. |
instructions | no | string | Short user-facing note, such as "Approve the prompt in the target app." |
tokenKey | no | string | Stored token field name. Defaults to token. |
persist | no | boolean | Defaults to true. Set false for one-time session data that should not be saved. |
statusRequest | no | object | Optional request to check whether the app/session is already authenticated. |
statusMap | no | object | Maps response values from statusRequest, commonly { "authenticated": "$.data.authenticated" }. |
tokenRequest | no | object | Request used to obtain a token. |
tokenMap | no | object | Maps response values from tokenRequest, commonly { "token": "$.data.authenticationToken" }. |
tokenPath | no | string | Shortcut for tokenMap.token. |
pairing | no | object | Device-code/local pairing flow with startRequest, startMap, pollRequest, pollMap, intervalMs, and timeoutMs. |
refreshRequest | no | object | Request used to refresh an expiring token before sources/actions run. Uses saved {auth.*} values. |
refreshMap | no | object | Maps refreshed response values, commonly { "access_token": "$.access_token", "expires_in": "$.expires_in" }. Falls back to tokenMap if omitted. |
refreshTokenKey | no | string | Stored refresh token key. Defaults conceptually to refresh_token for generated manifests. |
expiresInKey | no | string | Response/storage key containing seconds until expiry. Defaults to expires_in. |
expiresAtKey | no | string | Stored absolute expiry key. Defaults to expires_at. |
refreshSkewSeconds | no | number | Refresh this many seconds before expiry. Defaults to 60. Max 3600. |
sessionRequest | no | object | Request used to authenticate the current session/socket with the saved token. |
sessionMap | no | object | Maps response values from sessionRequest, commonly { "authenticated": "$.data.authenticated" }. |
authenticatedPath | no | string | Shortcut for sessionMap.authenticated. |
inject | no | object | Controls automatic session preflight before sources/actions. Defaults to both enabled. |
At least one of statusRequest, tokenRequest, sessionRequest, or pairing must be present.
Auth Response Truthiness
For statusMap.authenticated or sessionMap.authenticated, SheevChat treats these values as authenticated:
true, 1, "true", "1", "yes", "authenticated", "connected", "ok", "success"
Anything else is treated as not authenticated.
Pairing Auth Flow
Use connection.auth.pairing for local or LAN apps that require a user to approve SheevChat with a code, browser URL, device URL, or in-app prompt before returning a reusable token. The pairing flow is intentionally bounded and uses the same local HTTP/WebSocket/IPC/UDP request runtime as other action-library auth.
Runtime behavior:
- User clicks Authenticate.
- SheevChat sends
pairing.startRequest. - SheevChat maps the response with
pairing.startMap. - If a user code or verification URL is mapped, SheevChat shows it in the connection status/toast.
- SheevChat polls
pairing.pollRequestin the background untilpairing.pollMapreturns the configured token key. - The mapped token values are stored locally and become
{auth.token}/{auth.<key>}for future sources/actions.
Pairing object fields:
| Field | Required | Type | Notes |
|---|---|---|---|
startRequest | yes | object | Request that begins pairing. |
startMap | no | object | Maps code/display fields from the start response. Common keys: device_code, user_code, verification_uri, verification_url, expires_in, interval. |
pollRequest | yes | object | Request repeated until a token is returned. Can use {pairing.device_code}, {pairing.user_code}, or any key mapped by startMap. |
pollMap | no | object | Maps token values from the poll response. Should include the auth tokenKey, or token. |
intervalMs | no | number | Poll interval from 1000 to 30000 ms. Defaults to 5000. |
timeoutMs | no | number | Pairing timeout from 10000 to 300000 ms. Defaults to 120000. |
Example:
{
"id": "local",
"label": "Example Pairing App",
"type": "local_http",
"defaultUrl": "http://127.0.0.1:4000",
"auth": {
"type": "local_pairing",
"buttonLabel": "Pair",
"tokenKey": "access_token",
"pairing": {
"startRequest": {
"method": "POST",
"path": "/pair/start"
},
"startMap": {
"device_code": "$.device_code",
"user_code": "$.user_code",
"verification_uri": "$.verification_uri",
"expires_in": "$.expires_in"
},
"pollRequest": {
"method": "POST",
"path": "/pair/poll",
"body": {
"device_code": "{pairing.device_code}"
}
},
"pollMap": {
"access_token": "$.access_token",
"refresh_token": "$.refresh_token",
"expires_in": "$.expires_in"
},
"intervalMs": 5000,
"timeoutMs": 120000
}
}
}
Do not use pairing for OAuth authorization-code flows. Use connection.type: "oauth" and connection.oauth for those.
Token Expiry And Refresh
For cloud APIs or local APIs with short-lived access tokens, map both the token and expiry values. SheevChat stores the mapped values locally, computes expires_at from expires_in when needed, and refreshes just before a source dropdown or imported action runs.
Use snake_case keys in generated manifests:
{
"auth": {
"type": "token_lifecycle",
"tokenKey": "access_token",
"refreshTokenKey": "refresh_token",
"expiresInKey": "expires_in",
"expiresAtKey": "expires_at",
"refreshSkewSeconds": 120,
"tokenRequest": {
"method": "POST",
"path": "/v1/token",
"body": {
"client_id": "{auth.client_id}",
"client_secret": "{auth.client_secret}"
}
},
"tokenMap": {
"access_token": "$.access_token",
"refresh_token": "$.refresh_token",
"expires_in": "$.expires_in"
},
"refreshRequest": {
"method": "POST",
"path": "/v1/token/refresh",
"body": {
"refresh_token": "{auth.refresh_token}"
}
},
"refreshMap": {
"access_token": "$.access_token",
"expires_in": "$.expires_in"
}
}
}
Runtime behavior:
- If no expiry is stored, SheevChat does not refresh automatically.
- If an expiry is stored and the token is within the refresh skew window, SheevChat sends
refreshRequest, maps the response, stores the new values, then continues with the original source/action. - If refresh fails, SheevChat marks the connection as needing authentication and stops the original request.
- If the provider returns no new refresh token, the old stored
{auth.refresh_token}remains available because refreshed values merge into the existing local auth store. - This is still not a full generic OAuth 2.0 authorization-code runtime. It is token lifecycle support for APIs where the manifest can express token/refresh HTTP requests safely.
WebSocket Token + Session Example
Use this shape for apps that first return a reusable token, then require the token to authenticate each WebSocket session:
{
"id": "local",
"label": "Example Local App",
"type": "local_websocket",
"defaultUrl": "ws://127.0.0.1:8001",
"auth": {
"type": "local_session",
"label": "Local app authentication",
"buttonLabel": "Authenticate",
"instructions": "Approve the prompt in the target app if one appears.",
"tokenKey": "token",
"statusRequest": {
"messageType": "APIStateRequest"
},
"statusMap": {
"authenticated": "$.data.currentSessionAuthenticated"
},
"tokenRequest": {
"messageType": "AuthenticationTokenRequest",
"data": {
"pluginName": "SheevChat",
"pluginDeveloper": "SheevChat"
}
},
"tokenMap": {
"token": "$.data.authenticationToken"
},
"sessionRequest": {
"messageType": "AuthenticationRequest",
"data": {
"pluginName": "SheevChat",
"pluginDeveloper": "SheevChat",
"authenticationToken": "{auth.token}"
}
},
"sessionMap": {
"authenticated": "$.data.authenticated"
},
"inject": {
"beforeSources": true,
"beforeActions": true
}
}
}
Auth Placeholders
Mapped auth values are available as placeholders:
| Placeholder | Meaning |
|---|---|
{auth.token} | Saved token when tokenKey is token. |
{token} | Convenience alias for the same saved token. |
{auth.some_key} | Saved auth value from tokenMap.some_key. |
{some_key} | Convenience alias for the same saved value. |
Prefer {auth.token} in generated manifests because it makes the source of the value obvious.
Auth Preflight
When inject.beforeSources is true, SheevChat sends sessionRequest before a connection_request source request. This lets authenticated dropdowns populate correctly.
When inject.beforeActions is true, SheevChat sends sessionRequest before an imported action runs.
For WebSocket APIs, the preflight and the real request are sent on the same short-lived WebSocket connection. For HTTP APIs, SheevChat sends the preflight request first, then sends the real request.
Generic Instance File Discovery
Use instance file discovery when an app writes local JSON files that tell clients which instance/port is active.
Example:
{
"id": "local",
"label": "Example App",
"type": "local_websocket",
"discovery": {
"type": "instance_file",
"directory": ".example-app/instances",
"map": {
"time": "$.time",
"id": "$.id",
"server": "$.server",
"name": "$.name",
"version": "$.version",
"language": "$.language"
},
"preferredType": "main",
"selectionMode": "exact_or_newest",
"clientName": "SheevChat",
"staleMs": 10000
}
}
Instance Discovery Fields
| Field | Required | Type | Notes |
|---|---|---|---|
type | yes | string | Must be instance_file. |
provider | no | string | Metadata only. Defaults to generic. |
directory | no | string | Relative paths resolve under the user's home directory. |
map | no | object | Maps instance JSON fields to SheevChat fields. |
preferredType | no | string | Chooses a matching instance type when multiple are active. |
selectionMode | no | string | How SheevChat picks an instance. Defaults to exact_or_newest. |
instanceSlot | no | number | Default 1-based ordinal slot used when selectionMode is ordinal. |
clientName | no | string | Used in generated WebSocket query string. Defaults to SheevChat. |
staleMs | no | number | Active window for instance files. Clamped 1000-60000 ms. Defaults to 10000. |
Relative directory example:
"directory": ".veadotube/instances"
The directory value is already resolved under the user's home directory. Do not put environment variables, home aliases, absolute paths, or drive letters in the manifest.
Do not use:
%USERPROFILE%\.veadotube\instances
$HOME/.veadotube/instances
~/.veadotube/instances
C:\Users\name\.veadotube\instances
On Windows, this means:
%USERPROFILE%\.veadotube\instances
Instance Map
Default instance map:
{
"time": "$.time",
"id": "$.id",
"server": "$.server",
"name": "$.name",
"version": "$.version",
"language": "$.language"
}
Required mapped values are:
timeidserver
The server value can be:
127.0.0.1:12345
ws://127.0.0.1:12345
wss://127.0.0.1:12345
SheevChat converts bare host:port values to ws://host:port for WebSocket instance connections.
Instance Selection Modes
Use selectionMode when an app can run more than one instance at the same time.
| Mode | Meaning |
|---|---|
exact_or_newest | Backward-compatible default. Use the selected instance_id/instance_server if it is active; otherwise fall back to the newest matching instance. |
exact | Require the selected instance_id/instance_server. If that instance is not active, the action fails instead of switching targets. |
newest | Pick the newest active instance, filtered by preferredType when possible. |
oldest | Pick the oldest active instance, filtered by preferredType when possible. |
ordinal | Pick a 1-based slot from active instances ordered oldest-to-newest. Slot 5 only works when the previous four matching instances are also open. |
Action fields may override the connection default by sending hidden values named:
instance_selection_mode
instance_slot
instance_id
instance_server
preferredType
For explicit user-picked instance dropdowns, use an instance_file source. The dropdown option stores instance_selection_mode: "exact" so the selected instance stays pinned while it is active.
Generic Endpoint File Discovery
Use endpoint-file discovery when an app writes one stable JSON file containing its current port or WebSocket URL. This is different from directory-scanned instance discovery:
instance_filescans a directory of active instance files and requires mappedtime,id, andserver.endpoint_filereads one specific JSON file and can generate a stable endpoint from a mappedport.
Endpoint files are useful for apps that randomize their local port each launch.
Example:
{
"id": "local",
"label": "Example App",
"type": "local_websocket",
"discovery": {
"type": "endpoint_file",
"root": "localAppData",
"file": "Packages/Example.App/LocalState/ws-info.json",
"map": {
"port": "$.port"
},
"urlTemplate": "ws://127.0.0.1:{port}"
}
}
Endpoint Discovery Fields
| Field | Required | Type | Notes |
|---|---|---|---|
type | yes | string | Must be endpoint_file. |
root | no | string | Safe root. Use home or localAppData. Defaults to home. |
file | yes | string | Relative JSON file path under the safe root. |
map | no | object | Maps port, optional host, protocol, path, url, id, name, version, and language. |
urlTemplate | no | string | WebSocket URL template such as ws://127.0.0.1:{port}. |
host | no | string | Default host when no url is mapped. Defaults to 127.0.0.1. |
protocol | no | string | ws or wss. Defaults to ws. |
pathSuffix / endpointPath | no | string | Optional path appended when SheevChat builds a URL without urlTemplate. |
selectionMode | no | string | Usually newest; present for shared selection behavior. |
The file value must be relative to its safe root. Do not use %LOCALAPPDATA%, %USERPROFILE%, $HOME, ~, drive letters, absolute paths, or ...
For Windows Store apps, prefer:
{
"root": "localAppData",
"file": "Packages/Publisher.App/LocalState/ws-info.json"
}
SheevChat resolves that under the current user's %LOCALAPPDATA% without exposing arbitrary absolute file access.
URL Templates
urlTemplate may use mapped/default placeholders:
{port}
{host}
{protocol}
{path}
{url}
{id}
{name}
{version}
{language}
Every placeholder must be known from the endpoint map or built-in defaults. The resolved URL must be ws:// or wss:// and must include a valid port from 1 to 65535.
mDNS / DNS-SD Service Discovery
Use mdns_service when the target app or device advertises itself on the local network with Bonjour/mDNS/DNS-SD. This is common for LAN devices and desktop companion apps that do not have a fixed port, but do publish a service type.
mDNS discovery is intentionally narrow:
- The manifest must include
local_discovery. - The service type must be explicit, such as
_elg._tcp.local. - SheevChat only accepts discovered endpoints that resolve to loopback, RFC1918 LAN addresses, or
.localnames. - The manifest chooses the protocol and optional path suffix used to build the endpoint URL.
- Discovery results are cached briefly so repeated dropdown loads and actions do not constantly probe the LAN.
{
"permissions": ["local_http", "local_discovery"],
"connections": [
{
"id": "local",
"label": "Elgato Light",
"type": "local_http",
"discovery": {
"type": "mdns_service",
"provider": "elgato",
"serviceType": "_elg._tcp.local",
"protocol": "http",
"path": "/elgato/lights",
"selectionMode": "newest",
"timeoutMs": 1200,
"cacheMs": 10000
}
}
]
}
mDNS Discovery Fields
| Field | Required | Type | Notes |
|---|---|---|---|
type | yes | string | Must be mdns_service. |
serviceType / service | yes | string | DNS-SD service type. Must look like _service._tcp.local or _service._udp.local. |
provider | no | string | Metadata label for the app/device family. |
protocol | no | string | http, https, ws, or wss. Defaults to http. |
path / pathSuffix | no | string | Optional endpoint path appended to the discovered host and port. |
selectionMode / mode | no | string | newest, oldest, ordinal, exact, or exact_or_newest. Defaults to newest. |
instanceSlot / slot | no | number | Used with selectionMode: "ordinal" to select the Nth discovered service after stable ordering. |
timeoutMs | no | number | Discovery wait time. Clamped from 250ms to 5000ms. Defaults to 1200ms. |
cacheMs | no | number | Runtime connection cache time. Clamped from 0ms to 60000ms. Defaults to 10000ms. |
mDNS Dynamic Instance Dropdowns
If a source uses type: "instance_file" against a connection whose discovery type is mdns_service, SheevChat returns discovered services as selectable options. The option extra values include the same common instance fields used by file-based discovery:
instance_id
instance_name
instance_server
instance_type
instance_version
instance_index
instance_ordinal
instance_selection_mode
Use this when a user needs to select one specific LAN device from several discovered devices.
"sources": {
"lights": {
"type": "instance_file",
"connectionId": "local"
}
}
The selected option can then pass instance_id, instance_server, or ordinal values into actions that should target a specific discovered service.
Response-Based Pipeline Discovery
Use pipeline discovery when an app writes a local bootstrap file, but the file does not contain the final service URL directly. A pipeline can read mapped values from a safe local JSON file, call a loopback HTTP/HTTPS discovery endpoint, map the response, and return the final app service URL.
This is useful for apps with a local broker that reports sub-application endpoints after every restart.
Example:
{
"id": "sonar",
"label": "SteelSeries GG Sonar Local API",
"type": "local_http",
"discovery": {
"type": "pipeline",
"cacheMs": 3000,
"steps": [
{
"type": "json_file",
"base": "programData",
"file": "SteelSeries/GG/coreProps.json",
"map": {
"gg_address": "$.ggEncryptedAddress"
}
},
{
"type": "http_request",
"method": "GET",
"urlTemplate": "https://{gg_address}/subApps",
"allowSelfSignedLoopback": true,
"map": {
"service_url": "$.subApps.sonar.metadata.webServerAddress",
"sonar_enabled": "$.subApps.sonar.isEnabled",
"sonar_running": "$.subApps.sonar.isRunning",
"sonar_ready": "$.subApps.sonar.isReady"
},
"readiness": [
{ "key": "sonar_enabled", "equals": true, "message": "Sonar is installed but disabled." },
{ "key": "sonar_running", "equals": true, "message": "Sonar is not running." },
{ "key": "sonar_ready", "equals": true, "message": "Sonar is not ready yet." }
]
}
],
"result": "{service_url}"
},
"test": {
"method": "GET",
"path": "/Mode"
}
}
Pipeline Fields
| Field | Required | Type | Notes |
|---|---|---|---|
type | yes | string | Must be pipeline. |
cacheMs | no | number | Short-lived resolved endpoint cache. Defaults to 3000 ms. |
steps | yes | array | Ordered json_file and http_request steps. |
result | yes | string | Template for the final service URL, such as {service_url}. |
Pipeline Step Types
json_file reads one safe local JSON file and maps values for later steps.
| Field | Required | Type | Notes |
|---|---|---|---|
type | yes | string | Must be json_file. |
base / root | no | string | Safe root. Use home, localAppData, or programData. |
file | yes | string | Relative JSON file path under the safe root. |
map | yes | object | Maps variable names to JSON paths. |
http_request calls a loopback discovery endpoint and maps the response.
| Field | Required | Type | Notes |
|---|---|---|---|
type | yes | string | Must be http_request. |
method | no | string | GET, POST, or PUT. Defaults to GET. |
urlTemplate | yes | string | http:// or https:// loopback URL template. |
allowSelfSignedLoopback | no | boolean | Allows a self-signed certificate only for this loopback HTTPS discovery request. |
map | no | object | Maps response values for later steps or result. |
readiness | no | array | Optional checks that must pass before accepting the endpoint. |
Pipeline Safety Rules
- File paths must stay under their selected safe root.
- Do not use absolute paths, drive letters, UNC paths, environment variables, home aliases, or
... - Discovery request URLs must resolve to loopback hosts only:
127.0.0.1,localhost, or::1. - Discovery requests may use
http://orhttps://; final app service URLs may usehttp://,https://,ws://, orwss://. - URLs with embedded credentials are rejected.
- Redirects must stay on loopback.
allowSelfSignedLoopbacknever disables TLS checks globally and does not apply to normal action/source requests.
Readiness Checks
Use readiness when a discovery response can identify installed-but-disabled, stopped, or not-yet-ready states.
{
"readiness": [
{ "key": "app_enabled", "equals": true, "message": "The app is installed but disabled." },
{ "key": "app_running", "equals": true, "message": "The app is not running." }
]
}
Each key must be mapped earlier in that step's map. If equals is omitted, SheevChat treats the mapped value as a truthy/falsy readiness value.
Sources
Sources power dynamic dropdowns. sources must be an object keyed by source id.
Correct:
"sources": {
"items": {
"type": "static_options",
"options": []
}
}
Incorrect:
"sources": [
{
"id": "items",
"type": "static_options"
}
]
Accepted Source Types
| Type | Purpose |
|---|---|
static_options | Fixed dropdown options defined in the manifest. |
instance_file | Dropdown of currently discovered app instances. |
connection_request | Calls the app/API and maps the response into dropdown options. |
Source Fields
| Field | Required | Type | Notes |
|---|---|---|---|
type | no | string | Defaults to connection_request. |
connectionId | depends | string | Required for instance_file and connection_request. |
dependsOn | no | array | Source-level dependencies. Used with cascading fields. |
request | no | object | HTTP/WebSocket request definition for connection_request. |
map | no | object | Maps API response into dropdown options. |
options | no | array | Fixed options for static_options. |
cacheMs | no | number | Cache duration for source results. 0 disables cache. |
Static Options Sources
Use static_options when all dropdown choices are known in advance.
"sources": {
"boolean_state": {
"type": "static_options",
"options": [
{ "value": "on", "label": "On" },
{ "value": "off", "label": "Off" },
{ "value": "toggle", "label": "Toggle" }
]
}
}
Static options may include extra values:
{
"value": "scene_a",
"label": "Scene A",
"extra": {
"scene_id": "abc123"
}
}
Static value, label, and extra strings can use placeholders from existing values.
Instance File Sources
Use this with a connection that has discovery.type = instance_file.
"sources": {
"instances": {
"type": "instance_file",
"connectionId": "local",
"cacheMs": 1000
}
}
SheevChat returns options with these hidden extras:
| Extra key | Meaning |
|---|---|
instance_id | Discovered instance id. |
instance_name | Discovered instance name. |
instance_server | Discovered host/port or URL. |
instance_type | Inferred instance type. |
instance_version | Discovered app version. |
instance_index | Index in the discovered instance list. |
instance_ordinal | 1-based slot when instances are ordered oldest-to-newest. |
instance_selection_mode | Set to exact for selected instance options. |
For multi-instance apps, include an Instance field first in every action:
{
"key": "instance",
"label": "Instance",
"type": "dynamic_select",
"source": "instances",
"labelKey": "instance_name"
}
Connection Request Sources
Use connection_request to ask the target app/API for dropdown options.
"sources": {
"hotkeys": {
"type": "connection_request",
"connectionId": "local",
"cacheMs": 5000,
"request": {
"messageType": "HotkeysRequest"
},
"map": {
"value": "$.data.hotkeys[].id",
"label": "$.data.hotkeys[].name"
}
}
}
For HTTP connections, request can contain:
{
"url": "/api/items",
"method": "GET",
"headers": {
"Accept": "application/json"
}
}
For non-GET HTTP requests (POST and PUT), body is supported:
{
"url": "/api/items/search",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": {
"query": "{search}"
}
}
Use bodyEncoding when the API expects form data or plain text instead of JSON:
{
"url": "/oauth/token",
"method": "POST",
"bodyEncoding": "form_urlencoded",
"body": {
"grant_type": "client_credentials",
"client_id": "{auth.client_id}",
"client_secret": "{auth.client_secret}"
}
}
Do not use PATCH or DELETE in imported action libraries. They are intentionally unsupported.
For WebSocket connections, request can be a plain JSON message:
{
"messageType": "ListHotkeysRequest",
"data": {
"modelID": "{model_id}"
}
}
Or a channel-style message:
{
"channel": "nodes",
"payload": {
"event": "list"
}
}
WebSocket Channel Messages
Some apps use messages shaped like:
nodes: {"event":"list"}
Represent that in the manifest as:
"request": {
"channel": "nodes",
"payload": {
"event": "list"
}
}
SheevChat sends:
nodes: {"event":"list"}
When the app replies:
nodes: {"event":"list","entries":[{"id":"mini","name":"avatar state"}]}
SheevChat parses it as:
{
"channel": "nodes",
"payload": {
"event": "list",
"entries": [
{
"id": "mini",
"name": "avatar state"
}
]
}
}
Therefore the map path must include $.payload:
"map": {
"rows": "$.payload.entries[]",
"value": "{id}",
"label": "{name}"
}
For nested payload responses:
nodes: {"event":"payload","payload":{"states":[...]}}
Use:
"rows": "$.payload.payload.states[]"
Source Mapping
The map object converts an API response into dropdown options.
Simple Path Mapping
Use this when values and labels are parallel arrays or array paths.
"map": {
"value": "$.data.items[].id",
"label": "$.data.items[].name"
}
value and label also support fallback path arrays and full extraction objects:
"map": {
"value": {
"paths": ["$.data.items[].id", "$.items[].id", "$.results[].value"]
},
"label": {
"paths": ["$.data.items[].name", "$.items[].name", "$.results[].label"]
}
}
The full extraction object accepts the same fields as execute.responseMap: path, paths, default, transform, and separator.
Row Mapping
Use row mapping for most APIs.
"map": {
"rows": "$.payload.entries[]",
"value": "{id}",
"label": "{name}"
}
Each row becomes a temporary object. {id} and {name} are read from that row.
Parent/Child Row Flattening
Use childRows when an API returns parent resources that contain nested child arrays, but the user should see one friendly dropdown. Each child becomes an option while the parent row stays available as $.parent.
Example response:
{
"result": {
"inputDevices": [
{
"id": "device-id",
"name": "Wave:3",
"inputs": [
{ "id": "input-id", "name": "Wave:3" }
]
}
]
}
}
Mapping:
"map": {
"rows": "$.result.inputDevices[]",
"childRows": "$.inputs[]",
"value": "{parent_id}:{id}",
"label": "{parent_name} - {name}",
"extra": {
"device_id": "$.parent.id",
"input_id": "$.id",
"device_name": "$.parent.name",
"input_name": "$.name"
}
}
For templates, SheevChat provides flat parent aliases for scalar parent properties, such as {parent_id} and {parent_name}. JSON paths in extra can use $.parent.id for parent fields and $.id for child fields.
Filtering Rows
Use where to filter rows.
"map": {
"rows": "$.payload.entries[]",
"where": {
"type": "stateEvents"
},
"value": "{type}:{id}",
"label": "{name}"
}
where is a flat object of row property/path keys to scalar exact-match values. Supported comparison values are strings, numbers, booleans, and null.
Correct:
"where": {
"type": "boolean",
"enabled": true
}
Do not use extraction or operator objects inside where:
"where": {
"path": "$.type",
"equals": "boolean"
}
That shape is not a filter expression. It compares row properties named path and equals; it does not mean “read this path and compare it.” Use the row property or JSON-style path as the key instead:
"where": {
"$.type": "boolean"
}
If one response contains heterogeneous resource types, do not reuse the unfiltered source for type-specific actions. A boolean action should only offer boolean nodes, a number action should only offer number nodes, and so on. Create separate filtered sources when that produces a safer builder experience.
Option values must also be unique within a source. When the API may reuse the same id for different types, prefer a composite value such as {type}:{id} while keeping the raw id and type in map.extra for the request payload.
Source Dependencies And Cache Correctness
List every field that can change a source request or its result in source.dependsOn. Dependencies are not only builder ordering metadata; they also participate in source refresh and cache identity.
For a state list selected by both instance and node:
"dependsOn": ["instance", "state_node"]
Do not omit instance merely because selecting an instance usually refreshes the node dropdown. Two instances may expose the same node option value, such as stateEvents:mini. If the cache only depends on state_node, changing instances can reuse state options from the previous instance.
Hidden Extra Values
Use extra to store stable ids or related values needed by later fields/actions.
"map": {
"rows": "$.payload.entries[]",
"value": "{type}:{id}",
"label": "{name}",
"extra": {
"state_node_type": "$.type",
"state_node_id": "$.id",
"state_node_name": "$.name"
}
}
extra values can also use fallback paths or full extraction objects:
"extra": {
"stable_id": ["$.id", "$.uuid", "$.name"],
"enabled": { "path": "$.enabled", "default": false, "transform": "boolean" }
}
If an action later needs the selected node id, use:
"id": "{state_node_id}"
Do not use:
"id": "{state_node.state_node_id}"
SheevChat placeholders are flat.
Supported Map Paths
Map paths are simple JSON paths.
Supported examples:
$
$.data
$.data.items
$.data.items[]
$.data.items[].id
$.payload.entries[]
$.payload.payload.states[]
$.items[0].id
Unsupported examples:
$..id
$.items[?(@.type=="scene")]
$.items[*].id
Use rows plus where instead of JSONPath filters.
Fields
Fields define the form users fill out when configuring an action.
"fields": [
{
"key": "message",
"label": "Message",
"type": "text",
"default": "Hello!"
}
]
Accepted Field Types
| Type | Purpose |
|---|---|
text | Single-line text input. |
textarea | Multi-line text input. |
number | Numeric input. |
select | Fixed dropdown defined directly on the field. |
dynamic_select | Dropdown populated by a source. |
checkbox | Boolean field. |
color | Color picker field. |
file | File path field. |
hotkey | Hotkey capture field. |
hidden | Hidden value field. |
info | Informational field. |
Field Fields
| Field | Required | Type | Notes |
|---|---|---|---|
key | yes | string | Flat placeholder key. |
label | yes | string | User-facing label. |
type | no | string | Defaults to text. |
placeholder | no | string | Placeholder text. |
default | no | any | Default value. |
options | no | array | Required for useful select fields. |
source | no | string | Source id for dynamic_select. |
dependsOn | no | array | Field keys this field depends on. |
labelKey | no | string | Extra key that stores the selected option label. |
min | no | number | Numeric minimum. |
max | no | number | Numeric maximum. |
hint | no | string | Help text. |
Select Fields
Use select when choices are fixed and small.
{
"key": "operation",
"label": "Operation",
"type": "select",
"default": "set",
"options": [
{ "value": "set", "label": "Set" },
{ "value": "toggle", "label": "Toggle" }
]
}
For fixed dropdowns that must send a non-string JSON value, use valueJson instead of value. This is useful for APIs that require real booleans or numbers in an exact placeholder.
{
"key": "muted",
"label": "Mute State",
"type": "select",
"default": "toggle",
"options": [
{ "value": "toggle", "label": "Toggle" },
{ "valueJson": true, "label": "Muted" },
{ "valueJson": false, "label": "Unmuted" }
]
}
When an action uses an exact placeholder, SheevChat preserves the selected JSON type:
{
"isMuted": "{muted}"
}
If the user selects Muted, the outgoing payload contains "isMuted": true, not "isMuted": "true". Use this pattern for toggle-state controls where the app accepts a named toggle value plus explicit set values.
Remembered Toggle-State Selects
SheevChat gives the Toggle option special runtime behavior when one select field contains all three of these values:
- string
"toggle" - JSON boolean
true - JSON boolean
false
This pattern is intended for mute, visibility, enabled, and similar state controls where users expect one button to flip state.
Runtime behavior:
- If the user selects
Toggleand SheevChat has never seen that exact action target before, SheevChat sendstruefirst. - After that first toggle, SheevChat remembers the target state and alternates between
falseandtrue. - If the user runs the same action with explicit
Muted/Unmuted,Visible/Hidden, or another true/false pair, SheevChat updates the remembered state to match. - State memory is per action target. Different selected channels, sources, devices, or other field values keep separate toggle memory.
- State memory is runtime/session memory, not a guaranteed read from the external app. If the external app changes outside SheevChat, use an explicit true/false action once to resync.
Use a stable target field such as channel_id, source_id, device_id, or input_id so SheevChat can keep independent toggle memory for each target.
Dynamic Select Fields
Use dynamic_select when choices come from the app/API.
{
"key": "hotkey_id",
"label": "Hotkey",
"type": "dynamic_select",
"source": "hotkeys",
"labelKey": "hotkey_name"
}
source must match a key in top-level sources.
labelKey should match an extra key returned by that source when you want to retain the selected label.
Cascading Dropdowns
Cascading dropdowns let one selection populate another.
Example:
- Select model.
- Fetch hotkeys for selected model.
- Select hotkey.
"sources": {
"models": {
"type": "connection_request",
"connectionId": "local",
"request": {
"messageType": "CurrentModelRequest"
},
"map": {
"value": "$.data.modelID",
"label": "$.data.modelName"
}
},
"hotkeys": {
"type": "connection_request",
"connectionId": "local",
"dependsOn": ["model_id"],
"request": {
"messageType": "HotkeysInCurrentModelRequest",
"data": {
"modelID": "{model_id}"
}
},
"map": {
"value": "$.data.availableHotkeys[].hotkeyID",
"label": "$.data.availableHotkeys[].name"
}
}
}
Action fields:
"fields": [
{
"key": "model_id",
"label": "Model",
"type": "dynamic_select",
"source": "models",
"labelKey": "model_name"
},
{
"key": "hotkey_id",
"label": "Hotkey",
"type": "dynamic_select",
"source": "hotkeys",
"dependsOn": ["model_id"],
"labelKey": "hotkey_name"
}
]
The child field's dependsOn must reference another field key in the same action.
Actions
Actions are what users add to commands, automations, timers, or SheevPad buttons.
{
"id": "trigger_hotkey",
"label": "Trigger Hotkey",
"description": "Runs one hotkey.",
"connectionId": "local",
"contexts": ["timer", "automation", "command", "pad"],
"fields": [],
"execute": {
"type": "local_websocket",
"connectionId": "local",
"message": {}
}
}
Action Fields
| Field | Required | Type | Notes |
|---|---|---|---|
id | yes | string | Stable action id. Runtime type becomes <library-id>.<action-id>. |
label | yes | string | User-facing action name. |
description | no | string | User-facing action description. |
contexts | no | array | Defaults to all contexts. |
connectionId | no | string | Connection used by the action. |
fields | no | array | Config fields. |
execute | yes | object | Executor definition. Must include type. |
Contexts
Accepted action contexts:
| Context | Meaning |
|---|---|
timer | Available to timers. |
automation | Available to automations. |
command | Available to custom commands. |
pad | Available to SheevPad. |
Default:
["timer", "automation", "command", "pad"]
Event Subscriptions
Some apps expose pushed events, notification streams, or subscription APIs in addition to request/response actions. SheevChat recognizes top-level eventSubscriptions metadata so pack authors can document those future integration points without inventing fake actions.
Current runtime boundary:
local_websocketsubscriptions can be started and stopped explicitly through SheevChat's Action Libraries subscription API. SheevChat owns the socket, sends the declared subscribe message, receives JSON/channel messages, applieseventMatch, maps values witheventMap, and reports last-event diagnostics.local_http_pollsubscriptions can be started and stopped explicitly through the same API. SheevChat owns the timer, sends the declared HTTP request on a capped interval, applieseventMatch, maps values witheventMap, and reports last-event diagnostics.local_udpsubscriptions can be started and stopped explicitly through the same API. SheevChat owns a bounded local UDP listener, receives JSON/text/channel packets, applieseventMatch, maps values witheventMap, and reports last-event diagnostics.local_tcpsubscriptions can be started and stopped explicitly through the same API. SheevChat owns the TCP socket, sends the declared subscribe message if present, receives bounded raw or line-framed JSON/text/hex/base64 messages, applieseventMatch, maps values witheventMap, and reports last-event diagnostics.local_ipcsubscriptions can be started and stopped explicitly through the same API for approved IPC protocol presets. The first supported event runner is Discord RPC dispatch events over safe named-pipe discovery.local_mqttsubscriptions can be started and stopped explicitly through the same API. SheevChat owns the broker connection, subscribes only to declared allowlisted topics, receives bounded JSON/text/hex/base64 payloads, applieseventMatch, maps values witheventMap, and reports last-event diagnostics.- Local event subscriptions start manually by default. Add
autoStart: truewhen SheevChat should start the subscription after the library loads. - Persistent socket-style subscriptions (
local_websocket,local_tcp,local_ipc,local_mqtt) can opt into capped reconnect attempts withreconnect: true. - Local event subscriptions can emit SheevChat Automations through the explicit
Imported app eventtrigger. Automation matching uses the installed library id, event stream id, and mappedeventMapvalues. SheevChat applies a small per-stream internal throttle before routing these events to automations so rapid duplicate payloads cannot run unbounded action chains. - Installed Apps can locally override stream enablement, auto-start, reconnect, and Automation routing without editing the manifest. These user settings are stored in SheevChat config and take precedence over pack defaults. Disabling a stream stops it immediately; disabling Automation routing keeps diagnostics running while preventing
Imported app eventautomations from firing. oauth_webhooksubscription declarations remain metadata-only until a server/relay-safe runner exists.
Accepted top-level shape:
"eventSubscriptions": [
{
"id": "hotkey_event",
"label": "Hotkey Event",
"type": "local_websocket",
"connectionId": "local",
"autoStart": true,
"reconnect": true,
"reconnectDelayMs": 5000,
"maxReconnectAttempts": 5,
"subscribe": {
"messageType": "SubscribeToHotkeyEvents"
},
"eventMatch": {
"$.messageType": "HotkeyTriggeredEvent"
},
"eventMap": {
"hotkey_id": "$.data.hotkeyID",
"hotkey_name": "$.data.name"
},
"eventNamespace": "app_events",
"eventTypes": {
"hotkey_id": "string",
"hotkey_name": "string"
}
}
]
Accepted eventSubscriptions[].type values:
| Type | Meaning |
|---|---|
local_websocket | App can push events over a local WebSocket. Manual start/stop runner is available. |
local_http_poll | App has pollable event/state endpoints. Manual start/stop runner is available. |
local_udp | App can emit UDP messages to a local UDP listener. Manual start/stop runner is available. |
local_tcp | App can push events over a local TCP socket. Manual start/stop runner is available. |
local_ipc | App can emit events over an approved IPC protocol. Manual start/stop runner is available for supported presets. |
local_mqtt | App can publish events to an MQTT/MQTTS broker topic. Manual start/stop runner is available. |
oauth_webhook | Cloud API has webhook/subscription concepts. Metadata-only today. |
Fields:
| Field | Required | Notes |
|---|---|---|
id | yes | Stable subscription id. |
label | no | Human-readable name. |
description | no | Short explanation for future details views. |
type | no | Defaults to local_websocket. |
connectionId | yes for executable local subscriptions | local_websocket requires a local_websocket connection. local_http_poll requires a local_http connection. local_udp requires a local_udp connection. local_tcp requires a local_tcp connection. local_ipc requires a local_ipc connection using a supported protocol preset. local_mqtt requires a local_mqtt connection. |
autoStart | no | Defaults to false. When true, SheevChat starts the subscription shortly after the library loads, reloads, imports, or installs. Startup still respects enabled: false and connection/auth failures. |
reconnect / autoReconnect | no | Defaults to false. When true, persistent socket-style subscriptions reconnect after unexpected close until maxReconnectAttempts is reached. This is ignored for HTTP polling and UDP listeners. |
reconnectDelayMs | no | Base reconnect delay in milliseconds, clamped from 1000 to 60000. Defaults to 5000. Each retry uses a capped linear backoff. |
maxReconnectAttempts | no | Integer from 0 to 20. Defaults to 5. 0 means no retry attempts. |
subscribe | no | For local_websocket and local_tcp, request object sent when the runner starts. Uses the same message shape as that transport's actions. For local_http_poll, accepted as a legacy alias for the poll request. |
request | yes for local_http_poll unless poll/subscribe is used | HTTP request object sent on each poll. Uses the same shape as local HTTP actions, including method, path/url, headers, body, and bodyEncoding. |
poll | no | Alias for request in local_http_poll subscriptions. |
listen | yes for local_udp unless the connection defaultUrl supplies a port | UDP listener settings. host may be localhost, 127.0.0.1, or 0.0.0.0; port must be 1-65535. responseEncoding/parse can be json, text, hex, or base64. |
framing / responseFraming | no for local_tcp | raw or line. Defaults to connection.tcp.responseFraming, then connection.tcp.framing, then line for subscriptions. |
topic / subscribeTopic | yes for local_mqtt | MQTT topic filter to subscribe to. Must be allowed by connection.mqtt.topics.subscribe. |
responseEncoding / parse | no for local_mqtt | json, text, hex, or base64. Defaults to json, falling back to text if JSON parsing fails. |
intervalMs | no | Poll interval for local_http_poll. SheevChat clamps it between 1000ms and 60000ms. Defaults to 5000ms. |
unsubscribe | no | Best-effort request object sent before WebSocket or UDP runners stop. Not used by HTTP polling. |
eventMatch | no | Exact-match object keyed by JSON path. Non-matching frames are ignored. |
eventMap | no | Same extraction-map shape as responseMap, used to document likely event variables. |
eventNamespace | no | Stable namespace for future event variables. |
eventTypes | no | Optional type labels for mapped event values. |
eventTemplate | no | Optional compact text summary for diagnostics. Supports mapped event values. |
Use this section when an API documentation page clearly has a subscription/event endpoint. Do not replace normal query actions with event subscriptions. If the current pack needs values inside a command/action sequence now, create explicit query actions with awaitResponse, query, responseMap, and responseNamespace.
Manual runtime endpoints for development:
GET /api/action-libraries/subscriptions
POST /api/action-libraries/{libraryId}/subscriptions/{subscriptionId}/start
POST /api/action-libraries/{libraryId}/subscriptions/{subscriptionId}/stop
POST /api/action-libraries/{libraryId}/subscriptions/{subscriptionId}/test-event
POST /api/action-libraries/{libraryId}/subscriptions/{subscriptionId}/settings
GET /api/action-libraries/subscriptions returns developer diagnostics for every installed event stream:
| Response field | Notes |
|---|---|
libraryId / libraryName | Installed action library that owns the stream. |
id / label / description | Subscription identity from the manifest. |
type | Runtime transport, such as local_websocket, local_tcp, local_ipc, or local_mqtt. |
connectionId | Manifest connection used by the stream. |
enabled / runnable / status | Manifest availability. runnable: false means the stream is metadata-only or failed validation. |
autoStart | Whether SheevChat should attempt to start this stream after library load/reload/install. |
reconnect, reconnectDelayMs, maxReconnectAttempts | Manifest reconnect policy after normalization and clamping. |
outputs | Variables produced by eventMap / eventTypes, useful for future trigger and response wiring. |
runner.status | Current runtime state: not running, connecting, connected, listening, subscribed, reconnecting, or error. |
runner.detail / runner.error / runner.errorCode | Sanitized runtime diagnostics. These are intended for developers and advanced troubleshooting. |
runner.eventCount | Count of matching events observed by this runner. |
runner.lastEventAt | ISO timestamp for the most recent matched event. |
runner.lastEventValues | Last mapped values produced by eventMap. |
runner.lastEventText | Last rendered eventTemplate, if declared. |
runner.lastAutomationEventAt | ISO timestamp for the last imported app event routed into the Automation engine. |
runner.lastAutomationSkippedAt / runner.lastAutomationSkippedReason | Guard diagnostics when a mapped event was intentionally not routed, such as rapid duplicate throttling. |
runner.startedAt | ISO timestamp for the active or most recent runner session. |
runner.reconnectAttempts / runner.nextReconnectAt | Reconnect progress when reconnect: true is active. |
POST /test-event is a developer-only local test helper for Installed Apps. Send a JSON body with mapped sample values:
{
"values": {
"scene_name": "Starting Soon",
"source_visible": true
}
}
SheevChat wraps those values in a synthetic local payload, applies the subscription eventMatch where possible, maps through eventMap, stores the result in diagnostics, and routes one Imported app event Automation trigger. It does not call the third-party application and it does not allow action packs to run code.
POST /settings stores local user overrides for a runnable event stream:
{
"enabled": true,
"autoStart": false,
"reconnect": true,
"routeToAutomations": true
}
All fields are optional booleans, but at least one field must be supplied. Use this endpoint from SheevChat UI only; action packs cannot call it directly.
Installed Apps shows the same event-stream diagnostics in the Details panel, with Start/Stop controls, Send Sample, and local toggles for runnable local streams. Automations shows the Imported app event trigger when creating or editing an automation. Leave library/event-stream filters blank only when you intentionally want a broad catch-all; most packs should target one installed library and one event stream.
The Automation editor also shows an imported-app helper panel for this trigger. When no stream is selected, it summarizes available installed event streams. When a stream is selected, it shows the stream status, connection type, mapped eventMap outputs, suggested mapped-value filter keys, last runtime diagnostics, and the most recent mapped sample if one exists. This is intended to make local pack development easier without giving manifests arbitrary code execution.
Planned future work for production subscriptions still needs richer enablement controls and additional loop-guard UI, but action packs still cannot execute code. They only declare connection, parsing, and mapped event metadata; SheevChat owns the runtime and action execution.
Example local_http_poll subscription:
"eventSubscriptions": [
{
"id": "status_poll",
"label": "Status Poll",
"type": "local_http_poll",
"connectionId": "local",
"intervalMs": 5000,
"request": {
"method": "GET",
"path": "/api/status"
},
"eventMatch": {
"$.event": "status"
},
"eventMap": {
"state": "$.state",
"count": "$.count"
},
"eventTemplate": "{state}:{count}"
}
]
Example local_udp subscription:
"eventSubscriptions": [
{
"id": "status_events",
"label": "Status Events",
"type": "local_udp",
"connectionId": "local",
"listen": {
"host": "127.0.0.1",
"port": 45555,
"responseEncoding": "json"
},
"eventMatch": {
"$.event": "status"
},
"eventMap": {
"state": "$.state",
"count": "$.count"
},
"eventTemplate": "{state}:{count}"
}
]
UDP subscriptions are inbound local listeners, not arbitrary internet sockets. For safety, the listener host is restricted to localhost-style addresses. Use a normal local_udp action or source for outbound UDP request/response behavior.
Example local_tcp subscription:
"eventSubscriptions": [
{
"id": "status_events",
"label": "Status Events",
"type": "local_tcp",
"connectionId": "local",
"subscribe": {
"payload": { "event": "subscribe_status" },
"encoding": "json",
"framing": "line"
},
"responseEncoding": "json",
"responseFraming": "line",
"eventMatch": {
"$.event": "status"
},
"eventMap": {
"state": "$.state",
"count": "$.count"
},
"eventTemplate": "{state}:{count}"
}
]
TCP subscriptions are outbound local/LAN socket connections owned by SheevChat. They are best for apps that push newline-delimited status frames after a subscribe/hello message. TCP subscriptions support autoStart, capped reconnect, diagnostics, and Imported app event Automation routing when declared in the manifest.
Example local_ipc subscription:
"eventSubscriptions": [
{
"id": "voice_events",
"label": "Voice Events",
"type": "local_ipc",
"connectionId": "discord",
"subscribe": {
"cmd": "SUBSCRIBE"
},
"eventMatch": {
"$.evt": "VOICE_STATE"
},
"eventMap": {
"user": "$.data.user",
"mute": "$.data.mute"
},
"eventTemplate": "{user}:{mute}"
}
]
IPC subscriptions only run through approved protocol presets. Today that means Discord RPC dispatch events over safe named-pipe discovery. Manifests cannot define arbitrary IPC binary parsers.
Example local_mqtt subscription:
"eventSubscriptions": [
{
"id": "light_events",
"label": "Light Events",
"type": "local_mqtt",
"connectionId": "mqtt",
"topic": "lights/+/state",
"responseEncoding": "json",
"eventMatch": {
"$.event": "state"
},
"eventMap": {
"light": "$.light",
"enabled": "$.on"
},
"eventTemplate": "{light}:{enabled}"
}
]
MQTT subscriptions are broker connections, not internet relays. The broker must still pass the local/LAN URL policy, and the subscription topic must be declared in connection.mqtt.topics.subscribe. MQTT subscriptions support autoStart, capped reconnect, diagnostics, and Imported app event Automation routing when declared in the manifest.
Execute Types
Accepted executor type values:
| Type | Current behavior |
|---|---|
local_http | Sends an HTTP request. |
local_websocket | Sends a WebSocket message. |
local_ipc | Sends a request through an approved local IPC protocol preset. |
local_udp | Sends a bounded UDP packet. |
local_tcp | Sends a bounded TCP message. |
local_mqtt | Publishes a bounded MQTT message. |
sheevchat_command | Runs an existing SheevChat command. |
oauth_api | Sends an OAuth-authenticated HTTP request using the stored bearer token from an OAuth connection. |
hotkey | Sends a bounded Windows keystroke sequence through SheevChat's macro sender. |
launch_application | Launches a local app/file through SheevChat's bounded launcher. |
read_file | Reads a small file from the library's scoped SheevChat storage folder. |
write_file | Writes a small file inside the library's scoped SheevChat storage folder. |
For importable third-party app packs today, use:
"type": "local_websocket"
or:
"type": "local_http"
Use local_ipc only when the connection also uses type: "local_ipc" and the manifest declares the local_ipc permission.
or:
"type": "sheevchat_command"
or, for Windows macro-style packs:
"type": "hotkey"
or, for launching a user-selected local app/file:
"type": "launch_application"
or, for writing small scoped state/output files:
"type": "write_file"
or, for reading small scoped state/output files:
"type": "read_file"
Every execute object must include type.
Hotkey Execute Runtime
Use execute.type: "hotkey" when a pack needs to send a simple keyboard macro rather than call an app API. The manifest must declare the hotkey permission. A hotkey action does not need a connection; use no connection or a hotkey/none connection for status display only.
The action must provide execute.sequence or execute.keys. The sequence is interpolated with action fields and variables, capped to a short bounded string, and sent through the same Windows SendKeys runtime used by SheevChat's built-in keystroke action. {WAIT 250} or {DELAY 250} lines pause in milliseconds. Keystroke actions are Windows-only.
{
"permissions": ["hotkey"],
"connections": [],
"actions": [
{
"id": "send_macro",
"label": "Send Macro",
"fields": [
{
"key": "macro",
"label": "Macro",
"type": "hotkey",
"required": true
}
],
"execute": {
"type": "hotkey",
"sequence": "{macro}"
}
}
]
}
Do not use hotkey packs for app APIs that expose HTTP, WebSocket, IPC, or UDP. Prefer the real API runtime whenever possible because hotkeys are focus-sensitive and can affect whichever window currently has focus.
Launch Application Execute Runtime
Use execute.type: "launch_application" when a pack needs to open a local app, helper, or file that the user has selected on the SheevChat PC. The manifest must declare the launch_application permission. A launch action does not need a connection.
Launch actions use SheevChat's existing app launcher:
execute.pathis required and may use placeholders from fields or variables.execute.argsis optional. Strings are split like a normal command line; arrays are passed as argument lists.execute.workingDiris optional.- SheevChat starts the target with
shell: false. - On Windows, if the target is an
.exeand the exact executable path is already running, SheevChat skips launching a duplicate instance.
{
"permissions": ["launch_application"],
"connections": [],
"actions": [
{
"id": "launch_tool",
"label": "Launch Tool",
"fields": [
{
"key": "tool_path",
"label": "Application",
"type": "file",
"required": true
}
],
"execute": {
"type": "launch_application",
"path": "{tool_path}",
"args": "--profile \"{profile_name}\"",
"workingDir": "{working_dir}"
}
}
]
}
Do not use launch actions as a replacement for HTTP, WebSocket, IPC, UDP, or OAuth APIs. Prefer the real API runtime whenever possible because launch actions only start an app; they do not provide app state, dropdown discovery, or structured responses.
Write File Execute Runtime
Use execute.type: "write_file" when a pack needs to save a small state file, debug output, generated payload, or handoff file for its own library. The manifest must declare the write_file permission. A write action does not need a connection.
Write actions are intentionally scoped:
execute.pathis required.- The path must be relative.
- Absolute paths, drive letters, parent traversal, and null bytes are rejected.
- Files are written under SheevChat's managed per-library data folder, not arbitrary PC folders.
- Content is UTF-8 and capped to 256 KB at runtime.
- Use
execute.contentorexecute.textfor plain text. - Use
execute.jsonfor JSON output. execute.append: trueappends instead of replacing.execute.newline: trueadds a trailing newline when missing.
{
"permissions": ["write_file"],
"connections": [],
"actions": [
{
"id": "save_state",
"label": "Save State",
"execute": {
"type": "write_file",
"path": "state/{user}.json",
"json": {
"user": "{user}",
"value": "{value}"
}
}
}
]
}
Do not use write_file to edit application config files, system files, OBS files, or user documents. If a target app requires file-based integration outside SheevChat's library storage, that should become a reviewed/hardened runtime or a dedicated first-party integration instead of an arbitrary manifest path.
Read File Execute Runtime
Use execute.type: "read_file" when a pack needs to load a small state file, handoff file, debug output, or cached query result from its own library storage. The manifest must declare the read_file permission. A read action does not need a connection.
Read actions are intentionally scoped:
execute.pathis required.- The path must be relative.
- Absolute paths, drive letters, parent traversal, and null bytes are rejected.
- Files are read only from SheevChat's managed per-library data folder, not arbitrary PC folders.
- Content is read as UTF-8 and capped to 256 KB by default and at maximum.
execute.parsemay betextorjson; default istext.- Text reads return
text,content,relativePath, andbytes. - JSON reads return the parsed object; non-object JSON returns
{ "value": parsed }. - Use
query: true,awaitResponse: true,responseMap,responseNamespace, and output metadata when later actions should consume loaded values.
{
"permissions": ["read_file"],
"connections": [],
"actions": [
{
"id": "load_state",
"label": "Load State",
"execute": {
"type": "read_file",
"path": "state/{user}.json",
"parse": "json",
"query": true,
"awaitResponse": true,
"responseMap": {
"savedValue": "$.value"
},
"responseNamespace": "state",
"outputLabels": {
"savedValue": "Saved value"
},
"outputTypes": {
"savedValue": "string"
}
}
}
]
}
Use paired write_file and read_file actions only for pack-owned state. Do not use read_file to inspect application config files, secrets, OBS files, or user documents.
Incorrect:
"execute": {
"channel": "nodes",
"payload": {}
}
Correct:
"execute": {
"type": "local_websocket",
"connectionId": "local",
"channel": "nodes",
"payload": {}
}
Local WebSocket Execute
JSON-RPC Must Use message
For JSON-RPC over WebSocket, put the complete protocol envelope inside message:
"execute": {
"type": "local_websocket",
"connectionId": "local",
"message": {
"id": 101,
"jsonrpc": "2.0",
"method": "getChannels",
"params": null
},
"awaitResponse": true,
"responseMatch": {
"$.id": 101
}
}
Do not put a JSON-RPC method directly beside type and connectionId. At that level, method is transport metadata used by HTTP-style requests and is not part of the plain WebSocket message. The socket may connect successfully while the remote app receives a JSON-RPC object with no method and performs no command.
Use a distinct request id for each source or action and match the echoed response id. JSON-RPC notifications do not contain an id, so response matching prevents an unsolicited notification from being mistaken for the requested result.
For APIs that use normalized audio values, preserve the documented numeric range. Wave Link 3 volume and gain values use 0.0 through 1.0, not 0 through 100.
Plain JSON message:
"execute": {
"type": "local_websocket",
"connectionId": "local",
"message": {
"messageType": "TriggerHotkeyRequest",
"data": {
"hotkeyID": "{hotkey_id}"
}
}
}
Channel-style message:
"execute": {
"type": "local_websocket",
"connectionId": "local",
"channel": "nodes",
"payload": {
"event": "payload",
"type": "{state_node_type}",
"id": "{state_node_id}",
"payload": {
"event": "set",
"state": "{state_id}"
}
}
}
Optional:
"awaitResponse": true
If awaitResponse is true, SheevChat waits for a matching response. Otherwise it sends and closes shortly after.
Local HTTP Execute
"execute": {
"type": "local_http",
"connectionId": "local",
"url": "/api/action",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": {
"id": "{item_id}",
"mode": "{mode}"
}
}
Supported methods are GET, POST, and PUT. method defaults to GET when omitted.
If url is relative, it is resolved against the connection defaultUrl.
For non-GET actions, bodyEncoding accepts the same values as HTTP source requests: json, form, form_urlencoded, urlencoded, text, or raw.
Action Response Ingestion
Imported HTTP and WebSocket actions can map useful response values into the current action sequence.
Use execute.responseMap to extract values from the action response:
"execute": {
"type": "local_http",
"connectionId": "local",
"url": "/api/status",
"method": "GET",
"responseMap": {
"current_scene": "$.scene.name",
"streaming": "$.streaming"
}
}
Set query: true when the main purpose of the action is to read state and produce variables for later actions. Query actions do not behave differently at runtime yet; the flag helps SheevChat label the action clearly in builders and library details.
Each mapped value can be one of three shapes.
Simple path string:
"current_scene": "$.scene.name"
Fallback path array:
"current_scene": ["$.scene.name", "$.currentProgramSceneName", "$.name"]
Full extraction object:
"current_scene": {
"paths": ["$.scene.name", "$.currentProgramSceneName"],
"default": "Unknown",
"transform": "string"
}
Accepted extraction object fields:
| Field | Required | Type | Notes |
|---|---|---|---|
path | no | string | One JSON-style path to try. Use either path or paths. |
paths | no | array of strings | Fallback paths. SheevChat uses the first path that returns values. |
default | no | any JSON value | Used when no path returns a value. |
transform | no | string | One of first, string, text, number, boolean, join, json. Defaults to first. |
separator | no | string | Used by join; defaults to , . |
Transforms:
| Transform | Result |
|---|---|
first | Keep the first matched raw value. |
string / text | Convert the first matched value to text. |
number | Convert the first matched value to a number; uses default if conversion fails. |
boolean | Accepts true/false, 1/0, yes/no, on/off, connected/disconnected, ok/error-style values. |
join | Joins all matched values with separator. |
json | Serializes the first matched value as JSON text. |
Mapped values are available to later actions in the same command, automation, timer, or SheevPad button:
{last.current_scene}
{last.streaming}
SheevChat also stores a namespaced copy using the library id. If the library id contains hyphens, an underscore alias is also created:
{obs-tools.current_scene}
{obs_tools.current_scene}
The action id is also available as a namespace:
{get_status.current_scene}
You may override the library namespace with responseNamespace:
"responseNamespace": "obs"
Then later actions can use:
{obs.current_scene}
Namespace Best Practices
Prefer one stable, short namespace for the library or integration, such as obs, vts, or veadotube. Keep output keys descriptive enough to coexist in that namespace:
{veadotube.boolean_value}
{veadotube.number_value}
{veadotube.current_state}
Avoid a different namespace for every query when the values belong to the same integration:
{boolean_value.value}
{number_value.value}
{current_state.state_id}
Fragmented namespaces make sequences harder to remember, document, autocomplete, and refactor. Use a specialized namespace only when a library contains genuinely separate domains or when output-key collisions cannot be resolved clearly.
The {last.key} and {action_id.key} aliases remain useful for immediate chaining and action-specific access. A stable library namespace is the durable interface authors should document.
Use responseTemplate when the action itself should send a chat response after mapping values:
"responseTemplate": "Current scene: {current_scene}"
responseTemplate uses action-library placeholders. It can read mapped keys directly, such as {current_scene}, and sequence values such as {user}.
Use outputLabels when you want the action builder and Action Library details to show friendlier names for produced values:
"execute": {
"type": "local_http",
"connectionId": "local",
"url": "/api/status",
"method": "GET",
"query": true,
"responseNamespace": "obs",
"responseMap": {
"current_scene": "$.scene.name",
"streaming": "$.streaming"
},
"outputLabels": {
"current_scene": "Current scene",
"streaming": "Streaming state"
},
"outputDescriptions": {
"current_scene": "The current OBS scene name.",
"streaming": "Whether OBS is currently streaming."
},
"outputTypes": {
"current_scene": "string",
"streaming": "boolean"
}
}
Output metadata fields:
| Field | Type | Notes |
|---|---|---|
outputLabels | object | Friendly names keyed by responseMap key. |
outputDescriptions | object | Short human descriptions for produced variables. |
outputTypes | object | Human-readable value types such as string, number, boolean, array, or an app-specific type. |
query | boolean | Marks the action as a state/query action in SheevChat UI. |
Output metadata should agree with the value actually produced. If outputTypes.min is number, use a numeric default such as 0 or omit the default when absence is meaningful; do not use an empty-string default while advertising the output as a number.
Keep sequence outputs compact. Mapping a small object with transform: "json" can be appropriate for advanced workflows, but avoid capturing unbounded lists, logs, binary data, or large base64 payloads unless the action explicitly exists for advanced data retrieval. Prefer identifiers, counts, names, hashes, dimensions, URLs, or narrowly selected values. Large outputs increase memory use, make summaries noisy, and are rarely suitable for interpolation into later actions.
Outputs are displayed in SheevChat as produced variables. For the example above, users will see variables like:
{last.current_scene}
{obs.current_scene}
{get_status.current_scene}
After an imported action runs, SheevChat also sets sequence helper variables:
{lastActionId}
{lastActionLabel}
{lastActionNamespace}
{lastActionOutputKeys}
{lastActionOutputSummary}
{lastActionMissingOutputKeys}
{lastActionValueCount}
{lastActionValuesJson}
{lastActionResponseJson}
{lastActionRaw}
{lastActionOutputSummary} is useful for quick debug chat responses, but action packs should prefer named variables such as {last.current_scene} or {obs.current_scene} for stable workflows.
{lastActionValuesJson} contains the compact mapped values object. {lastActionResponseJson} / {lastActionRaw} contain a compact raw response snapshot capped for safety. These are intended for developer diagnostics, debug chat responses, and advanced workflow glue. Do not build normal user-facing workflows around raw response JSON when a named responseMap output can be used instead.
If a query/awaited action returns a response but maps no values, SheevChat emits a developer warning. If only some mapped values are missing and the missing specs do not define a default, SheevChat records their keys in {lastActionMissingOutputKeys} and logs a developer warning. Optional values should include a type-compatible default when absence is normal.
When an imported action is run through SheevChat's action-library test/run endpoint, the same warning is returned in result.developerWarning. This is intended for local pack development so authors can see broken responseMap paths without digging through server logs. Marketplace users should not see these diagnostics as chat responses.
SheevChat Command Execute
Use this when the imported action should run an existing SheevChat command.
"execute": {
"type": "sheevchat_command",
"command": "!points",
"platform": "event"
}
platform: "event" means use the triggering event platform.
Interpolation
SheevChat replaces placeholders in strings:
{field_key}
Fields, source extras, defaults, and runtime values are merged into one flat value object.
Good:
"id": "{state_node_id}"
Bad:
"id": "{state_node.state_node_id}"
Interpolation is recursive through objects and arrays.
Example:
{
"payload": {
"event": "{operation}",
"state": "{state_id}",
"nested": ["{one}", "{two}"]
}
}
An exact placeholder preserves the runtime value's native JSON type. This includes strings, booleans, numbers, arrays, and objects:
{
"enabled": "{enabled}",
"amount": "{last.amount}",
"payload": "{last.payload}"
}
Nested Parent and Child Identifiers
Some APIs return resources nested under parent devices and require both identifiers in later commands:
{
"inputDevices": [
{
"id": "device-id",
"name": "Wave:3",
"inputs": [
{"id": "input-id", "name": "Wave:3"}
]
}
]
}
Do not assume the child id equals the parent id. A source mapped to $.result.inputDevices[] can provide the device id and friendly device name, but a source mapped to $.result.inputDevices[].inputs[] cannot currently copy the parent device id into each child option. Until parent/child flattening is supported by the core source mapper, expose the recognized parent device as a dropdown and obtain the nested child id from a query action or an explicit advanced field. Document this limitation in the action instead of silently sending the wrong identifier.
When designing core support for this pattern, flatten each child row with selected parent fields before applying map.value, map.label, and map.extra. This permits one friendly option to carry both device_id and input_id safely.
If enabled is true, the sent value is the JSON boolean true, not the string "true". If last.amount is 4, the sent value is the JSON number 4.
Configured field values may themselves contain a sequence placeholder. Exact placeholders are resolved recursively, so a field configured as {last.amount} can feed a typed value from an earlier action into the execute payload.
Placeholders embedded inside surrounding text always produce text:
"message": "Current amount: {last.amount}"
Boolean APIs
Use a checkbox field and an exact placeholder when the API requires a real boolean:
{
"key": "enabled",
"label": "Enabled",
"type": "checkbox",
"default": true
}
Then use the field as the complete property value:
"value": "{enabled}"
Do not invent a toggle field type. Use separate fixed actions with literal true and false only when that is a deliberate UX choice, not as a workaround for interpolation.
Complete Local WebSocket Skeleton
{
"schemaVersion": 1,
"id": "example-app",
"name": "Example App",
"version": "1.0.0",
"publisher": "Example Publisher",
"category": "Example Controls",
"description": "Adds Example App actions to SheevChat.",
"homepage": "https://example.com/docs",
"trust": "third-party",
"permissions": ["local_websocket"],
"connections": [
{
"id": "local",
"label": "Example App",
"type": "local_websocket",
"defaultUrl": "ws://127.0.0.1:12345",
"test": {
"url": "ws://127.0.0.1:12345"
}
}
],
"sources": {
"items": {
"type": "connection_request",
"connectionId": "local",
"cacheMs": 3000,
"request": {
"messageType": "ListItemsRequest"
},
"map": {
"value": "$.data.items[].id",
"label": "$.data.items[].name"
}
}
},
"actions": [
{
"id": "trigger_item",
"label": "Trigger Item",
"description": "Triggers one item in Example App.",
"connectionId": "local",
"contexts": ["timer", "automation", "command", "pad"],
"fields": [
{
"key": "item_id",
"label": "Item",
"type": "dynamic_select",
"source": "items",
"labelKey": "item_name"
}
],
"execute": {
"type": "local_websocket",
"connectionId": "local",
"message": {
"messageType": "TriggerItemRequest",
"data": {
"itemID": "{item_id}"
}
}
}
}
]
}
Complete Instance Discovery Skeleton
{
"schemaVersion": 1,
"id": "example-dynamic-app",
"name": "Example Dynamic App",
"version": "1.0.0",
"publisher": "Example Publisher",
"category": "Example Controls",
"description": "Controls a local app with dynamic instance files.",
"homepage": "https://example.com/docs",
"trust": "third-party",
"permissions": ["local_websocket"],
"connections": [
{
"id": "local",
"label": "Example Dynamic App",
"type": "local_websocket",
"discovery": {
"type": "instance_file",
"directory": ".example-app/instances",
"map": {
"time": "$.time",
"id": "$.id",
"server": "$.server",
"name": "$.name",
"version": "$.version",
"language": "$.language"
},
"clientName": "SheevChat",
"staleMs": 10000
}
}
],
"sources": {
"instances": {
"type": "instance_file",
"connectionId": "local",
"cacheMs": 1000
},
"items": {
"type": "connection_request",
"connectionId": "local",
"dependsOn": ["instance"],
"cacheMs": 3000,
"request": {
"channel": "items",
"payload": {
"event": "list"
}
},
"map": {
"rows": "$.payload.entries[]",
"value": "{id}",
"label": "{name}",
"extra": {
"item_id": "$.id",
"item_name": "$.name"
}
}
}
},
"actions": [
{
"id": "trigger_item",
"label": "Trigger Item",
"description": "Triggers one item on the selected instance.",
"connectionId": "local",
"contexts": ["timer", "automation", "command", "pad"],
"fields": [
{
"key": "instance",
"label": "Instance",
"type": "dynamic_select",
"source": "instances",
"labelKey": "instance_name"
},
{
"key": "item",
"label": "Item",
"type": "dynamic_select",
"source": "items",
"dependsOn": ["instance"],
"labelKey": "item_name"
}
],
"execute": {
"type": "local_websocket",
"connectionId": "local",
"channel": "items",
"payload": {
"event": "trigger",
"id": "{item_id}"
}
}
}
]
}
API Analysis Checklist
When converting an app API into a SheevChat manifest, answer these first:
- Is the app controlled by local HTTP, local WebSocket, local UDP, OAuth/cloud API, hotkeys, files, or app launch?
- Does the app use a fixed port or dynamic port?
- If dynamic, does the app publish directory-scanned instance files, a single endpoint file, mDNS/DNS-SD service records, or another discoverable source?
- What request proves the app is connected and protocol-compatible?
- What API calls list user-selectable things?
- Which list fields are stable IDs?
- Which list fields are display names?
- Do any dropdowns depend on earlier dropdowns?
- Does the action payload require booleans/numbers as real JSON values?
- Does the API return channel-prefixed WebSocket messages?
- Can the WebSocket emit greetings, acknowledgements, events, or unrelated frames that require
responseMatch? - Does the WebSocket require an
Originor other safe custom handshake header? - Does the API need authentication, pairing, signing, or secrets?
- Are actions useful in commands, automations, timers, and SheevPad?
If the answer requires platform OAuth, request signing, binary protocol support, persistent subscriptions, or a custom SDK, do not fake it in JSON. Document the required SheevChat runtime feature. If the app uses local token/session/pairing authentication over HTTP, WebSocket, IPC, or UDP, use connection.auth.
AI Prompt Template
Use this with an AI agent and an app API link.
Create a SheevChat action library manifest for this app API:
[PASTE API DOC LINK]
Output a single valid JSON `.sheevactions` manifest using schemaVersion 1.
If I ask for a branded packaged library instead, create a `.sheevactionszip` package containing the same JSON as root-level `manifest.json` plus one optimized brand image file. The image file can have any name, but it must use `.png`, `.jpg`, `.jpeg`, `.webp`, `.svg`, or `.gif`. Do not place files outside the zip root tree.
Do not output Markdown. Do not output comments. Do not output explanations before or after the JSON.
Use only the SheevChat manifest schema below:
- Top-level `sources` must be an object keyed by source id, not an array.
- Every action `execute` must include `type`.
- Supported practical execute types are `local_websocket`, `local_http`, `local_ipc`, `local_udp`, `local_tcp`, `local_mqtt`, `oauth_api`, `hotkey`, `launch_application`, `read_file`, `write_file`, and `sheevchat_command`.
- Supported practical connection types are `local_websocket`, `local_http`, `local_ipc`, `local_udp`, `local_tcp`, `local_mqtt`, `hotkey`, `oauth`, and `none`.
- Supported field types are exactly: `text`, `textarea`, `number`, `select`, `dynamic_select`, `checkbox`, `color`, `file`, `hotkey`, `hidden`, and `info`. Never invent a field type. Use `checkbox`, not `toggle`, for a boolean input.
- Supported action contexts are exactly: `timer`, `automation`, `command`, and `pad`.
- Supported response transforms are exactly: `first`, `string`, `text`, `number`, `boolean`, `join`, and `json`.
- For `local_http`, use only `GET`, `POST`, or `PUT`. Do not use `PATCH`, `DELETE`, `HEAD`, or other methods.
- Use `instance_file`, `endpoint_file`, or `pipeline` discovery only inside `connection.discovery`.
- Instance discovery directories are relative to the user's home directory. Use values such as `.veadotube/instances`. Never use `%USERPROFILE%`, `$HOME`, `~`, an absolute path, or a drive letter.
- Endpoint-file discovery reads one safe relative JSON file. Use `root: "home"` or `root: "localAppData"` plus `file`, such as `Packages/AppName/LocalState/ws-info.json`; never use `%LOCALAPPDATA%`, an absolute path, a drive letter, or `..`.
- Endpoint-file URL templates must be WebSocket URLs such as `ws://127.0.0.1:{port}` and every placeholder must be mapped or built in.
- Pipeline discovery reads safe local JSON files and loopback discovery responses. Use `base: "programData"` for `C:\ProgramData` bootstrap files; never put absolute paths or environment variables in `file`.
- Pipeline HTTP steps must resolve to loopback `http://` or `https://` URLs. Use `allowSelfSignedLoopback: true` only for loopback HTTPS discovery endpoints with local certificates.
- Use `connection.websocket.origin` for WebSocket Origin requirements. Do not put `Origin`, `Host`, `Connection`, `Upgrade`, or `Sec-WebSocket-*` in `websocket.headers`.
- For OAuth packs, use `connection.type: "oauth"` plus `connection.oauth` with `authorizationUrl`, `tokenUrl`, optional `revokeUrl`, `clientId`, `authorization_code_pkce`, `redirectMode: "loopback"`, and the provider's `tokenAuthMethod`; never include client secrets, access tokens, or refresh tokens in the manifest. If the provider requires a client secret, declare it under `connection.credentials`.
- Use `connection.auth` for local app token/session authentication. Do not invent top-level auth fields.
- For auth, use `tokenRequest` + `tokenMap` to save local tokens, `sessionRequest` + `sessionMap` when each WebSocket/session must be authenticated before dropdowns or actions, and `pairing.startRequest` + `pairing.pollRequest` when a local app exposes a device-code/user-code pairing flow.
- Use `connection.preflight` for hello/register/client-key messages that must be sent before each source/action but do not return a token or authenticated status.
- For `local_mqtt`, use `connection.mqtt.topics.publish` / `connection.mqtt.topics.subscribe` allowlists, keep broker URLs local/LAN, put credentials in `connection.credentials`, and do not create MQTT query actions.
- For `local_tcp`, use `tcp://host:port` local/LAN URLs, declare the `local_tcp` permission, choose `raw` or `line` framing, cap payload/response sizes, and use `awaitResponse: false` only for fire-and-forget controls.
- Use `execute.type: "hotkey"` only for bounded Windows keystroke macros, declare the `hotkey` permission, and provide `execute.sequence` or `execute.keys`.
- Use `execute.type: "launch_application"` only for launching a user-selected local app/file, declare the `launch_application` permission, and provide `execute.path`.
- Use `execute.type: "write_file"` only for writing small UTF-8/JSON files in the library's scoped SheevChat storage folder, declare the `write_file` permission, and provide a safe relative `execute.path`.
- Use `execute.type: "read_file"` only for reading small UTF-8/JSON files from the library's scoped SheevChat storage folder, declare the `read_file` permission, provide a safe relative `execute.path`, and set `execute.parse` to `text` or `json`.
- Use `fallbackUrls` when a local desktop app can bind to one of several documented ports.
- Source response mapping must be inside `source.map`.
- `source.map.value`, `source.map.label`, `source.map.extra`, and `execute.responseMap` can use path strings, fallback path arrays, or extraction objects with `path`/`paths`, `default`, `transform`, and `separator`.
- Query-style actions can use `execute.responseMap` to store response values for later actions. Prefer `{last.key}` for the next action, or set `responseNamespace` for stable names like `{obs.current_scene}`.
- For query-style actions with `responseMap`, add `execute.outputLabels` for user-friendly produced-value names when labels are obvious from the API.
- Use `responseTemplate` only when the action should send its own chat response.
- Use flat placeholders like `{state_node_id}`, never nested placeholders like `{state_node.state_node_id}`.
- Auth values use `{auth.token}` or `{auth.your_key}` placeholders.
- Use `map.extra` to expose hidden stable values from dynamic dropdown selections.
- Use `source.map.childRows` when parent rows contain nested child arrays and the action needs both parent and child identifiers. Store parent values with paths such as `$.parent.id` in `map.extra`.
- When a source response contains multiple resource types, use `source.map.where` or separate typed sources so each action only offers compatible options.
- `source.map.where` must map row property/path keys directly to scalar exact-match values, such as `"where": { "type": "boolean" }`. Never invent filter operators or use shapes such as `{ "path": "$.type", "equals": "boolean" }`.
- Source option values must be unique. If ids may overlap across resource types, use a collision-safe value such as `{type}:{id}` and retain the raw id/type in `map.extra`.
- Include every field that can affect a source request or result in `source.dependsOn`. For per-instance cascading data, this normally includes both `instance` and the selected parent resource so cached options cannot leak across instances with identical child ids.
- If using WebSocket channel messages, remember SheevChat wraps responses as `{ "channel": "...", "payload": ... }`, so map paths usually start with `$.payload`.
- If using plain JSON WebSocket messages, do not add `channel`; SheevChat sends the JSON object as-is.
- For JSON-RPC over WebSocket, put the complete `{ id, jsonrpc, method, params }` envelope inside `request.message` or `execute.message`, never at the request/execute root.
- Awaited JSON-RPC sources and actions should use `responseMatch` against the echoed id, such as `{ "$.id": 101 }`.
- If the app uses dynamic ports or multiple instances, include an `instances` source and make every app action include an `instance` field first.
- Prefer stable IDs over display names for option values.
- Exact placeholders preserve their native JSON type. Use `"{enabled}"`, `"{last.amount}"`, or `"{last.payload}"` as the complete property value for booleans, numbers, arrays, or objects. A placeholder embedded in surrounding text always produces text.
- When the API supports useful read operations, add query actions rather than producing a write-only pack. Every query action must set `awaitResponse: true`, `query: true`, a stable `responseNamespace`, `responseMap`, `outputLabels`, `outputDescriptions`, and `outputTypes`.
- Prefer one stable integration namespace across related queries, with descriptive output keys such as `{veadotube.boolean_value}` and `{veadotube.current_state}`. Do not create a separate namespace for every action unless the domains are genuinely independent.
- Output types and defaults must agree. Do not describe an output as `number` while using an empty-string default.
- Keep produced sequence values compact. Do not map unbounded lists, logs, binary content, or large base64 payloads unless advanced bulk retrieval is the explicit purpose of the action.
- Prefer approachable combined actions with an operation dropdown for general users. Use separate set/push/pop/toggle-style actions when the requested audience is advanced or when separate actions materially improve clarity.
Before returning the JSON, perform a consistency check:
- Every enum value is in an allowlist above.
- Every `connectionId`, dynamic source, `dependsOn` key, `labelKey`, placeholder, and response path resolves to something defined by the manifest or API response.
- Every type-specific action uses a compatible filtered source.
- Every source option value is stable and collision-safe.
- Every source dependency that can alter the request/result is included in `dependsOn`, including `instance` for per-instance cascading sources.
- Every query waits for a response and documents all produced outputs.
- Related queries use a consistent namespace, output metadata agrees with defaults, and mapped values are reasonably bounded in size.
- No unsupported field, executor, connection, context, transform, HTTP method, path form, or invented manifest key is present.
Required output:
1. Valid import-ready JSON only.
2. At least one connection.
3. Any sources needed for dynamic dropdowns.
4. Useful actions for commands, automations, timers, and SheevPad.
5. Clear action labels and descriptions.
6. Query actions and reusable typed outputs when the API supports meaningful reads.
If the API cannot be represented with current SheevChat action library support, output a short JSON manifest with no unsupported invented fields and include only actions that can work.
DevTools Testing Commands
List installed libraries:
fetch('/api/action-libraries').then(r => r.json()).then(console.log)
Reload libraries:
fetch('/api/action-libraries/reload', { method: 'POST' }).then(r => r.json()).then(console.log)
Test a connection:
fetch('/api/action-libraries/example-app/connections/local/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
}).then(r => r.json()).then(console.log)
Resolve a source:
fetch('/api/action-libraries/example-app/sources/items/resolve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ values: {} })
}).then(r => r.json()).then(console.log)
Resolve a cascading source:
fetch('/api/action-libraries/example-app/sources/hotkeys/resolve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
values: {
model_id: 'abc123'
}
})
}).then(r => r.json()).then(console.log)
Run an imported action:
fetch('/api/action-libraries/example-app/actions/trigger_item/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
item_id: 'abc123'
})
}).then(r => r.json()).then(console.log)
Validation Checklist
Before sharing a manifest:
schemaVersionis1.idvalues use only lowercase letters, numbers, hyphens, and underscores.sourcesis an object, not an array.- Every
dynamic_select.sourceexists insources. - Every field
dependsOnreferences another field in the same action. - Every
connectionIdexists inconnections. - Every
executehas a validtype. - Every field uses one of the documented field types. Boolean inputs use
checkbox, nottoggle. - Every app action has the right
connectionId. - Source maps are inside
map. - Type-specific actions use filtered sources so incompatible resources cannot be selected.
- Every
whereentry maps a row property/path directly to a scalar exact-match value; it contains no invented operator objects. - Source option values are unique; potentially overlapping ids use a composite value such as
{type}:{id}. - Cascading sources include every cache-varying dependency, including
instancewhen results are instance-specific. - Channel WebSocket response paths include
$.payload. - Nested channel payload response paths use
$.payload.payloadwhen needed. - Placeholders are flat.
- Exact typed placeholders occupy the complete property value when the API requires a boolean, number, array, or object.
- Query actions use
awaitResponse,query,responseMap, a stable namespace, and complete output metadata. - Related query actions use one stable integration namespace unless separate domains justify otherwise.
- Output types agree with defaults, and mapped values avoid unnecessary bulk/base64 data.
- Instance discovery directories are home-relative and contain no environment variables, home aliases, drive letters, or absolute paths.
- Remote URLs include
remote_networkonly when intentionally needed.
Failure-Driven Troubleshooting
When a generated manifest fails, preserve the failing example and turn the cause into four things: a clearer validator message, a prompt constraint, a wiki entry, and a regression test. This keeps the builder guidance aligned with the actual runtime instead of relying on examples alone.
Local imports are the developer testing path. When a locally imported pack resolves a dynamic source to an empty option list, SheevChat surfaces a developer warning in the action builder with the likely failure area. Marketplace and catalog installs do not show these empty-list warnings because an end user may not have the target app connected, authenticated, or populated yet.
When an integration cannot be represented by the current manifest schema, classify the gap before giving up or making a fake pack:
- Existing manifest capability
- Reusable runtime transport/protocol capability
- Trusted native-plugin capability
- Application-specific core integration
- Unsupported or unsafe
Document the decision. If the answer is reusable runtime capability, expand the runtime once and keep the app-specific behavior in the manifest where possible.
| Failure | Cause | Correction |
|---|---|---|
fields[n].type is unsupported: toggle | toggle is not a SheevChat field type. | Use checkbox for a boolean input or select for named operations. |
Instance discovery finds nothing with %USERPROFILE% in directory | Discovery directories are already resolved under the user's home directory; environment variables are not manifest syntax. | Use .veadotube/instances. |
| A boolean action lists number or state nodes | One heterogeneous source was reused without filtering. | Add map.where, such as { "type": "boolean" }, or create separate typed sources. |
| Typed dropdowns are empty even though the API returned matching rows | where used an invented operator object such as { "path": "$.type", "equals": "boolean" }. | Use a direct scalar match: { "type": "boolean" } or { "$.type": "boolean" }. |
Local import warning says empty_rows_path | Either map.rows is wrong, or a WebSocket greeting, acknowledgement, status event, or unrelated response was selected before the intended reply. | Compare the reported top-level keys with the documented response. If they belong to another frame, add a stable responseMatch; otherwise update map.rows to the actual array path. |
Local import warning says empty_child_rows_path | Parent rows were found, but map.childRows returned no nested children. | Check the child array path relative to one parent row, such as $.inputs[] or $.outputs[]. |
Local import warning says empty_where_filter | Rows were found, but every row was removed by map.where. | Confirm each where key exists on the row and that the expected value exactly matches the app response. |
Local import warning says empty_value_mapping | Rows passed filtering, but map.value produced empty or duplicate values. | Point map.value at a stable non-empty id, or use a template such as {type}:{id} when ids can collide. |
Local import warning says empty_instance_file | Instance discovery found no active app instances. | Start the target app, confirm the instance files exist, and verify connection.discovery.directory plus discovery.map.server. |
| A dropdown loses or replaces an option with the same id | The API reuses ids across resource types and the source value is not unique. | Use {type}:{id} as the option value and keep raw fields in map.extra. |
| Cascading options come from the previously selected app instance | The source cache does not vary by every field that affects its result. | Add instance and every selected parent field to source.dependsOn. |
| A read action sends successfully but produces no sequence variables | The action does not wait for or map its response. | Add awaitResponse: true, query: true, responseNamespace, responseMap, and output metadata. |
| A JSON-RPC WebSocket connects but every command does nothing | The JSON-RPC method was placed at the execute root and treated as transport metadata, so it was not included in the socket message. | Put the entire { id, jsonrpc, method, params } envelope inside execute.message; for sources, put it inside request.message. |
| A JSON-RPC query consumes a notification instead of its response | The request did not correlate the echoed JSON-RPC id, and notifications can arrive on the same socket without an id. | Give the request a stable id and add responseMatch, such as { "$.id": 101 }. |
| A microphone dropdown knows the device but a command also needs a nested input id | The API requires parent and child identifiers. | Use map.childRows and store both identifiers in map.extra, such as device_id: "$.parent.id" and input_id: "$.id". |
| Related query variables are difficult to remember | Each query invented a separate namespace. | Reuse one short library namespace and choose descriptive output keys. |
| A value is documented as a number but sometimes becomes empty text | The extraction default conflicts with outputTypes. | Use a type-compatible default or omit it when missing data should remain absent. |
| Sequences or output summaries become unusually large | A query mapped an unbounded list, log, binary value, or base64 payload. | Map compact identifiers/metadata or make bulk retrieval an explicitly advanced action. |
An API receives "true" or "4" instead of typed JSON | The placeholder was embedded in surrounding text, or the configured value was text rather than a typed exact placeholder. | Use the exact placeholder as the complete property value, such as "{enabled}" or "{last.amount}". |
| A dynamic dropdown has no usable label or request id | labelKey or map.extra does not expose the selected row data needed by later fields/actions. | Map stable ids and display names in extra, then reference those flat keys. |
Do not merely patch the one generated manifest. Update this section whenever a new failure reveals a reusable rule.
Known Current Limits
The schema recognizes more values than the imported action runtime fully supports. Today, third-party packs should focus on:
- local HTTP
- local WebSocket
- local IPC with approved protocol presets
- local UDP
- generic instance file discovery
- endpoint-file, pipeline, and mDNS discovery
- static options
- connection-request options
- cascading dropdowns
- user-entered credentials and
{auth.<key>}substitution - local pairing/device-code auth flows through
connection.auth.pairing - declarative HMAC signing and Tuya-style canonical signing
- refresh-token lifecycle for manifest-expressed refresh requests
- OAuth loopback PKCE login/token storage, refresh-token renewal, best-effort token revocation, and
oauth_apibearer-token requests - SheevChat command execution
- bounded Windows hotkey/macro execution
- bounded local application/file launching
- scoped read/write state files inside SheevChat-managed per-library storage
- manual local WebSocket, local HTTP poll, local UDP, and approved local IPC event subscription listening for developer/runtime diagnostics
These need additional runtime work before they can be considered generally supported for imported packs:
- arbitrary binary protocols without an approved preset
- event subscription auto-start, reconnect policy, trigger routing, and server/relay-backed webhook subscriptions
- custom SDKs
- arbitrary filesystem read/write outside SheevChat-managed per-library storage
If an app requires one of those, the action pack should document the gap rather than invent unsupported manifest fields.