Schema version 1 · Runtime reference

Action Library Manifest Wiki

The complete, AI-friendly reference for building import-ready .sheevactions integrations across local apps, LAN devices, OAuth APIs, discovery, authentication, events, and reusable action-sequence outputs.

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 sources an array. It must be an object keyed by source id.
  • Placing rows, value, label, or extra directly on a source. They must be inside source.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 discovery instead of inside it.
  • Inventing field types. The only accepted field types are text, textarea, number, select, dynamic_select, checkbox, color, file, hotkey, hidden, and info. Use checkbox, not toggle, for a boolean input.
  • Writing %USERPROFILE%, $HOME, ~, an absolute path, or a drive letter in discovery.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.json or one .sheevactions file must be at the package root.
  • If no manifest.json exists, do not include multiple root .sheevactions files.
  • 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 .sheevactions plain 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

FieldRequiredTypeAccepted / Notes
schemaVersionyesnumberMust be 1.
idyesstringLowercase letters, numbers, hyphens, or underscores. 3-64 chars. Must start and end with a letter or number.
nameyesstringUser-facing library name. Max 120 chars.
versionyesstringSemantic-ish version such as 1.0.0, 1.2, or 1.0.0-beta.1.
publishernostringDisplayed in action category as Category by Publisher. Max 120 chars.
categorynostringUser-facing action category. Defaults to Community. Max 80 chars.
descriptionnostringLibrary summary. Max 700 chars.
homepagenostringDocumentation/homepage URL. Max 500 chars.
iconnostringRelative packaged icon path. Max 500 chars. For .sheevactionszip, SheevChat can auto-fill this with the first compatible image file found in the package.
permissionsnoarraySee permissions below.
compatibilitynoobjectFreeform metadata. Currently stored, not enforced.
trustnostringFreeform label such as local, third-party, community, or example-pack.
connectionsnoarrayConnection definitions.
sourcesnoobjectDynamic option sources keyed by source id. Must be an object, not an array.
actionsyesarrayMust 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:

PermissionMeaning
local_httpManifest may call local HTTP APIs.
local_websocketManifest may call local WebSocket APIs.
local_ipcManifest may call approved local IPC/RPC transports such as hardened named-pipe protocol presets.
local_udpManifest may send bounded UDP request/response packets to localhost or private LAN targets.
local_tcpManifest may send bounded TCP request/response messages to localhost or private LAN targets.
local_mqttManifest may publish bounded MQTT/MQTTS messages to declared topics on localhost/private LAN brokers and manually subscribe to declared topics for diagnostics.
local_discoveryManifest may perform approved LAN/local service discovery such as mDNS/DNS-SD.
oauthManifest may declare a safe OAuth 2.0 authorization-code-with-PKCE profile. Loopback PKCE login and token storage are supported for public clients.
hotkeyManifest may send bounded Windows keystroke sequences through SheevChat's macro sender.
launch_applicationManifest may launch a user-selected local app/file through SheevChat's bounded launcher.
read_fileManifest may read small UTF-8/JSON files from its own SheevChat-managed library storage folder.
write_fileManifest may write small UTF-8/JSON files inside its own SheevChat-managed library storage folder.
sheevchat_commandManifest may trigger a SheevChat command.
remote_networkAllows 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

TypeCurrent behavior
noneNo app connection required. Status defaults to connected.
local_httpUses fetch against a local HTTP API.
local_websocketOpens a WebSocket, sends a message, optionally waits for response.
local_ipcOpens a validated local IPC endpoint and uses a hardened protocol preset.
local_udpSends bounded UDP packets to local/LAN targets.
local_tcpOpens a short-lived TCP socket to a local/LAN target, sends a bounded message, optionally waits for a bounded response.
local_mqttConnects to a local/LAN MQTT broker for bounded publish actions and manual subscriptions.
oauthOAuth profile connection. Status defaults to needs login until the user signs in through the loopback PKCE flow.
hotkeyNo app connection required. Status defaults to connected. Used with execute.type: "hotkey".

Connection Fields

FieldRequiredTypeNotes
idyesstringStable connection id. Usually local.
labelnostringUser-facing connection label. Defaults to id.
typenostringDefaults to none.
defaultUrlnostringBase URL such as http://127.0.0.1:3000 or ws://127.0.0.1:8001.
fallbackUrls / urlsnoarrayOrdered fallback base URLs. SheevChat tries defaultUrl first, then each fallback. Useful for apps that expose one of several documented local ports.
providernostringMetadata for OAuth/provider naming.
scopesnoarrayMetadata for OAuth scopes.
discoverynoobjectSupports generic instance file discovery.
authnoobjectDescribes local app authentication for APIs that require a token or session handshake.
remotenoobjectRemote/cloud HTTP safety policy. Use with remote_network for internet APIs.
signingnoobjectDeclarative request signing preset for HTTP APIs that require HMAC signatures. Secrets must come from saved credentials.
preflightnoobjectFirst-message or setup request sent before source dropdowns/actions. Use this for app hello/register messages that do not return a reusable token.
protocolnoobjectRequired for local_ipc. Selects a hardened IPC protocol preset such as discord_rpc.
tcpnoobjectOptional local_tcp defaults such as framing, response framing, timeout, and payload/response byte caps.
testnoobjectConnection 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

FieldRequiredTypeNotes
flow / typenostringOnly authorization_code_pkce is accepted. Hyphen form is normalized.
authorizationUrl / authorizeUrl / authUrlyesstringHTTPS authorize URL. Embedded credentials are rejected.
tokenUrlyesstringHTTPS token URL.
revokeUrl / revocationUrlnostringHTTPS revocation URL.
clientId / client_idyesstringPublic client id for desktop/PKCE use.
scopes / scopenoarray or stringArrays are preferred. Space-separated strings are split into scopes.
redirectMode / redirectnostringloopback or relay. Defaults to loopback.
pkcenobooleanDefaults to true.
tokenAuthMethod / authMethodnostringnone, client_secret_post, or client_secret_basic. Secret methods require a matching locally saved credential.
clientSecretKey / clientSecretCredential / client_secret_keynostringCredential 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 / authorizeParamsnoobjectScalar extra authorize parameters such as audience or prompt. Secrets/tokens are rejected.
extraTokenParams / tokenParamsnoobjectScalar extra token parameters. Secrets/tokens are rejected.
extraRevokeParams / revokeParams / revocationParamsnoobjectScalar 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=code
  • client_id
  • redirect_uri=http://localhost:{current-port}/api/action-libraries/oauth/callback
  • state
  • scope
  • code_challenge
  • code_challenge_method=S256
  • optional audience, resource, prompt, and extraAuthorizeParams

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-client client_secret_post and client_secret_basic methods when the secret is saved as a local credential.
  • oauth_api requests automatically add Authorization: Bearer {auth.access_token} unless the manifest explicitly provides an Authorization header.
  • Remote/cloud API hosts still use the normal remote HTTP safety policy. Include remote_network and connection.remote.allowedHosts when 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 to needs 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 revokeUrl is present, SheevChat first makes a best-effort revocation request with token, token_type_hint, client_id, and any extraRevokeParams; 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.allowedHosts or the remote host in defaultUrl / 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 429 responses are surfaced as rate-limit feedback, including Retry-After when 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

FieldRequiredTypeNotes
allowedHostsrecommendedarrayExact remote hostnames, such as api.example.com. Do not include protocol, paths, query strings, ports, or credentials. Alias: hosts.
allowInsecureHttpnobooleanDefaults to false. Only use true for a documented local/private API that cannot use TLS. Marketplace cloud packs should not use it.
maxResponseBytesnonumberMaximum response body size. Defaults to 1 MB and is capped at 5 MB.
maxRedirectsnonumberRedirect 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:

ValueBody behaviorDefault content type
jsonObject bodies are sent as JSON. String bodies pass through as-is.application/json
formObject bodies are sent as URL-encoded form data. Array values repeat the same key.application/x-www-form-urlencoded
form_urlencodedAlias for form.application/x-www-form-urlencoded
urlencodedAlias for form.application/x-www-form-urlencoded
textBody is sent as plain text.text/plain; charset=utf-8
rawAlias 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 .local hostnames.
  • Broadcast and multicast targets require explicit opt-in with udp.allowBroadcast or udp.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

FieldRequiredTypeNotes
defaultUrlusuallystringudp://host:port. The host must be local/LAN.
udp.retriesnonumberInteger from 0 to 5. Defaults to 0.
udp.timeoutMsnonumberTimeout from 250ms to 10000ms. Defaults to 2500ms.
udp.maxPacketBytesnonumberOutgoing packet cap from 64 to 65507 bytes. Defaults to 8192.
udp.maxResponseBytesnonumberIncoming response cap from 64 to 65507 bytes. Defaults to 65507.
udp.allowBroadcastnobooleanRequired for broadcast targets such as udp://255.255.255.255:4001 or subnet broadcasts ending in .255. Defaults to false.
udp.allowMulticastnobooleanRequired for multicast targets in 224.0.0.0/4. Defaults to false.
udp.multicastTtlnonumberMulticast 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.

FieldRequiredTypeNotes
url / pathnostringOptional udp://host:port override. Most packs use the connection defaultUrl.
packet / message / payload / bodyyesanyPacket contents. Objects are normally encoded as JSON.
encoding / payloadEncodingnostringjson, text, hex, or base64. Defaults to json for objects and text for strings.
responseEncoding / parsenostringjson, text, hex, or base64. Defaults to json.
awaitResponsenobooleanDefaults to true. Set false for fire-and-forget controls. Query actions must wait for a response.
retriesnonumberOverrides connection retry count for this request. 0-5.
timeoutMsnonumberOverrides request timeout. 250-10000ms.
maxPacketBytesnonumberOverrides outgoing packet cap.
maxResponseBytesnonumberOverrides incoming response cap.
responseFromAnynobooleanDefaults 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:// or mqtts://.
  • Broker hosts must be localhost, RFC1918 private LAN addresses, or .local hostnames.
  • Broker URLs cannot contain embedded credentials.
  • Credentials must be declared with connection.credentials and 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

FieldRequiredTypeNotes
defaultUrlyesstringmqtt://host:port or mqtts://host:port. The host must be local/LAN.
credentialsnoarrayDeclare username/password/API-key fields here. Never embed credential values in defaultUrl.
mqtt.clientIdnostringClient id sent to the broker. Supports placeholders such as {library.id}.
mqtt.usernamenostringBroker username. Usually a {auth.username} placeholder.
mqtt.passwordnostringBroker password/API key. Usually a {auth.password} placeholder.
mqtt.cleannobooleanDefaults to true.
mqtt.keepalivenonumberKeepalive seconds from 0 to 120.
mqtt.timeoutMsnonumberConnection timeout from 500ms to 30000ms. Defaults to 3000ms.
mqtt.protocolVersionnonumber4 or 5. Defaults to the MQTT library default.
mqtt.rejectUnauthorizednobooleanDefaults to true. Set false only for documented local self-signed brokers.
mqtt.qosnonumberDefault QoS from 0 to 2.
mqtt.allowRetainnobooleanRequired before any action can publish retained messages. Defaults to false.
mqtt.maxPayloadBytesnonumberPayload cap from 64 bytes to 1 MB. Defaults to 64 KB.
mqtt.topics.publishyes for publish actionsarrayTopic filters that publish actions may target. MQTT wildcards are allowed here as allowlist filters.
mqtt.topics.subscribeyes for subscriptionsarrayTopic 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.

FieldRequiredTypeNotes
topic / publishTopicyesstringConcrete publish topic. It cannot contain MQTT wildcards after placeholders resolve.
payload / message / body / jsonnoanyPayload contents. Objects are normally encoded as JSON.
encoding / payloadEncodingnostringjson, text, hex, or base64. Defaults to json for objects and text for strings.
qosnonumber0, 1, or 2. Defaults to connection.mqtt.qos or 0.
retainnobooleanOnly honored when connection.mqtt.allowRetain is true.
maxPayloadBytesnonumberOptional 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 .local hostnames.
  • 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 awaitResponse is 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

FieldRequiredTypeNotes
defaultUrlusuallystringtcp://host:port. The host must be local/LAN.
fallbackUrls / urlsnoarrayOrdered tcp://host:port fallbacks. Useful for apps that bind one of several documented ports.
tcp.timeoutMsnonumberTimeout from 250ms to 30000ms. Defaults to 3000ms.
tcp.framingnostringraw or line. Defaults to raw. line appends \n when the payload does not already end in a newline.
tcp.responseFramingnostringraw or line. Defaults to the request framing. line resolves after the first newline-delimited response frame.
tcp.maxPayloadBytesnonumberOutgoing payload cap from 64 bytes to 1 MB. Defaults to 64 KB.
tcp.maxResponseBytesnonumberIncoming 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.

FieldRequiredTypeNotes
url / pathnostringOptional tcp://host:port override. Most packs use the connection defaultUrl.
packet / message / payload / bodyyesanyMessage contents. Objects are normally encoded as JSON.
encoding / payloadEncodingnostringjson, text, hex, or base64. Defaults to json for objects and text for strings.
framingnostringraw or line. Overrides connection.tcp.framing.
responseEncoding / parsenostringjson, text, hex, or base64. Defaults to json.
responseFramingnostringraw or line. Overrides connection.tcp.responseFraming.
awaitResponsenobooleanDefaults to true. Set false for fire-and-forget controls. Query actions and dropdown sources must wait for a response.
timeoutMsnonumberOverrides request timeout. 250-30000ms.
maxPayloadBytesnonumberOverrides outgoing payload cap.
maxResponseBytesnonumberOverrides 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:

  • auth expects SheevChat to prove authentication succeeded through statusMap, tokenMap, or sessionMap.
  • preflight sends setup messages before sources/actions and does not require an authenticated response.

When To Use Preflight, Auth, Or Both

API behaviorUseWhy
The app requires a hello/register/client-key message as the first WebSocket message, then normal commands work.connection.preflightThere 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 + tokenMapSheevChat 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 + sessionMapSheevChat stores the token, then sends session auth before sources/actions.
The app can report whether the current session is already authenticated.connection.auth.statusRequest + statusMapSheevChat can show authenticated without requesting a new token.
The app requires both a first-message hello and later token/session auth.connection.preflight + connection.authPreflight 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:

  1. defaultUrl
  2. 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"
        }
      }
    }
  }
]
FieldRequiredTypeMeaning
requestyes, unless requests is usedobjectSingle preflight request.
requestsyes, unless request is usedarrayMultiple preflight requests in order.
beforeSourcesnobooleanDefaults to true. Sends preflight before dynamic dropdown source requests.
beforeActionsnobooleanDefaults to true. Sends preflight before action execution.
awaitResponsenobooleanDefaults 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.

FieldRequiredDescription
typeyesMust be named_pipe_range.
templateyesLocal pipe-name template containing {index}.
startnoFirst integer index. Defaults to 0.
endnoLast 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:

ProtocolPurpose
discord_rpcDiscord 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

FieldRequiredTypeNotes
keyyesstringPlaceholder key. Must be unique within the connection. Use lowercase snake case, such as govee_api_key.
labelnostringUser-facing field label. Defaults to the key.
typenostringpassword or text. Defaults to password. Use password for API keys, bearer tokens, webhook secrets, and session-like values.
requirednobooleanDefaults to true. Required credentials must be saved before tests, dropdowns, or actions can run.
placeholdernostringPlaceholder shown before a value is saved.
helpnostringShort 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:

PlaceholderMeaning
{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:

FieldRequiredTypeAccepted / Notes
typenostringCurrently hmac_sha256. Defaults to hmac_sha256.
secret / secretTemplateyesstringMust reference a locally saved credential such as {auth.client_secret}. Do not put literal secrets in manifests.
payload / templateusuallystringString to sign after placeholder substitution. Required unless canonical is configured.
canonicalnostring/objectApproved canonical request preset. Currently supports tuya_v2.
targetnostringheader, query, or body. Defaults to header.
nameyesstringHeader name, query parameter name, or object body field that receives the signature. Aliases: header, query, field.
encodingnostringhex, base64, or base64url. Defaults to hex.
case / outputCasenostringpreserve, upper, or lower. Defaults to preserve.
prefixnostringOptional text prepended to the signature value, such as sha256=.
bodyHashEncodingnostringEncoding for {signing.bodySha256} / {body.sha256}. Defaults to hex.
timestampnoboolean/objecttrue enables {signing.timestamp} using unix_ms. Object accepts { "format": "unix_ms" }, { "format": "unix" }, or { "format": "iso" }.
noncenoboolean/objecttrue 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:

PlaceholderMeaning
{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 / refreshMap in 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:

  1. User expands Action Libraries details.
  2. User clicks Test to confirm the app is reachable.
  3. User clicks Authenticate.
  4. SheevChat sends the token request, maps the returned token, stores it locally, then sends the session request.
  5. 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

FieldRequiredTypeNotes
typenostringMetadata. Use local_session unless a more specific label helps the library author.
labelnostringUser-facing auth label shown in details.
buttonLabelnostringButton text. Defaults to Authenticate.
instructionsnostringShort user-facing note, such as "Approve the prompt in the target app."
tokenKeynostringStored token field name. Defaults to token.
persistnobooleanDefaults to true. Set false for one-time session data that should not be saved.
statusRequestnoobjectOptional request to check whether the app/session is already authenticated.
statusMapnoobjectMaps response values from statusRequest, commonly { "authenticated": "$.data.authenticated" }.
tokenRequestnoobjectRequest used to obtain a token.
tokenMapnoobjectMaps response values from tokenRequest, commonly { "token": "$.data.authenticationToken" }.
tokenPathnostringShortcut for tokenMap.token.
pairingnoobjectDevice-code/local pairing flow with startRequest, startMap, pollRequest, pollMap, intervalMs, and timeoutMs.
refreshRequestnoobjectRequest used to refresh an expiring token before sources/actions run. Uses saved {auth.*} values.
refreshMapnoobjectMaps refreshed response values, commonly { "access_token": "$.access_token", "expires_in": "$.expires_in" }. Falls back to tokenMap if omitted.
refreshTokenKeynostringStored refresh token key. Defaults conceptually to refresh_token for generated manifests.
expiresInKeynostringResponse/storage key containing seconds until expiry. Defaults to expires_in.
expiresAtKeynostringStored absolute expiry key. Defaults to expires_at.
refreshSkewSecondsnonumberRefresh this many seconds before expiry. Defaults to 60. Max 3600.
sessionRequestnoobjectRequest used to authenticate the current session/socket with the saved token.
sessionMapnoobjectMaps response values from sessionRequest, commonly { "authenticated": "$.data.authenticated" }.
authenticatedPathnostringShortcut for sessionMap.authenticated.
injectnoobjectControls 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:

  1. User clicks Authenticate.
  2. SheevChat sends pairing.startRequest.
  3. SheevChat maps the response with pairing.startMap.
  4. If a user code or verification URL is mapped, SheevChat shows it in the connection status/toast.
  5. SheevChat polls pairing.pollRequest in the background until pairing.pollMap returns the configured token key.
  6. The mapped token values are stored locally and become {auth.token} / {auth.<key>} for future sources/actions.

Pairing object fields:

FieldRequiredTypeNotes
startRequestyesobjectRequest that begins pairing.
startMapnoobjectMaps code/display fields from the start response. Common keys: device_code, user_code, verification_uri, verification_url, expires_in, interval.
pollRequestyesobjectRequest repeated until a token is returned. Can use {pairing.device_code}, {pairing.user_code}, or any key mapped by startMap.
pollMapnoobjectMaps token values from the poll response. Should include the auth tokenKey, or token.
intervalMsnonumberPoll interval from 1000 to 30000 ms. Defaults to 5000.
timeoutMsnonumberPairing 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:

PlaceholderMeaning
{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

FieldRequiredTypeNotes
typeyesstringMust be instance_file.
providernostringMetadata only. Defaults to generic.
directorynostringRelative paths resolve under the user's home directory.
mapnoobjectMaps instance JSON fields to SheevChat fields.
preferredTypenostringChooses a matching instance type when multiple are active.
selectionModenostringHow SheevChat picks an instance. Defaults to exact_or_newest.
instanceSlotnonumberDefault 1-based ordinal slot used when selectionMode is ordinal.
clientNamenostringUsed in generated WebSocket query string. Defaults to SheevChat.
staleMsnonumberActive 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:

  • time
  • id
  • server

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.

ModeMeaning
exact_or_newestBackward-compatible default. Use the selected instance_id/instance_server if it is active; otherwise fall back to the newest matching instance.
exactRequire the selected instance_id/instance_server. If that instance is not active, the action fails instead of switching targets.
newestPick the newest active instance, filtered by preferredType when possible.
oldestPick the oldest active instance, filtered by preferredType when possible.
ordinalPick 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_file scans a directory of active instance files and requires mapped time, id, and server.
  • endpoint_file reads one specific JSON file and can generate a stable endpoint from a mapped port.

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

FieldRequiredTypeNotes
typeyesstringMust be endpoint_file.
rootnostringSafe root. Use home or localAppData. Defaults to home.
fileyesstringRelative JSON file path under the safe root.
mapnoobjectMaps port, optional host, protocol, path, url, id, name, version, and language.
urlTemplatenostringWebSocket URL template such as ws://127.0.0.1:{port}.
hostnostringDefault host when no url is mapped. Defaults to 127.0.0.1.
protocolnostringws or wss. Defaults to ws.
pathSuffix / endpointPathnostringOptional path appended when SheevChat builds a URL without urlTemplate.
selectionModenostringUsually 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 .local names.
  • 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

FieldRequiredTypeNotes
typeyesstringMust be mdns_service.
serviceType / serviceyesstringDNS-SD service type. Must look like _service._tcp.local or _service._udp.local.
providernostringMetadata label for the app/device family.
protocolnostringhttp, https, ws, or wss. Defaults to http.
path / pathSuffixnostringOptional endpoint path appended to the discovered host and port.
selectionMode / modenostringnewest, oldest, ordinal, exact, or exact_or_newest. Defaults to newest.
instanceSlot / slotnonumberUsed with selectionMode: "ordinal" to select the Nth discovered service after stable ordering.
timeoutMsnonumberDiscovery wait time. Clamped from 250ms to 5000ms. Defaults to 1200ms.
cacheMsnonumberRuntime 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

FieldRequiredTypeNotes
typeyesstringMust be pipeline.
cacheMsnonumberShort-lived resolved endpoint cache. Defaults to 3000 ms.
stepsyesarrayOrdered json_file and http_request steps.
resultyesstringTemplate 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.

FieldRequiredTypeNotes
typeyesstringMust be json_file.
base / rootnostringSafe root. Use home, localAppData, or programData.
fileyesstringRelative JSON file path under the safe root.
mapyesobjectMaps variable names to JSON paths.

http_request calls a loopback discovery endpoint and maps the response.

FieldRequiredTypeNotes
typeyesstringMust be http_request.
methodnostringGET, POST, or PUT. Defaults to GET.
urlTemplateyesstringhttp:// or https:// loopback URL template.
allowSelfSignedLoopbacknobooleanAllows a self-signed certificate only for this loopback HTTPS discovery request.
mapnoobjectMaps response values for later steps or result.
readinessnoarrayOptional 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:// or https://; final app service URLs may use http://, https://, ws://, or wss://.
  • URLs with embedded credentials are rejected.
  • Redirects must stay on loopback.
  • allowSelfSignedLoopback never 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

TypePurpose
static_optionsFixed dropdown options defined in the manifest.
instance_fileDropdown of currently discovered app instances.
connection_requestCalls the app/API and maps the response into dropdown options.

Source Fields

FieldRequiredTypeNotes
typenostringDefaults to connection_request.
connectionIddependsstringRequired for instance_file and connection_request.
dependsOnnoarraySource-level dependencies. Used with cascading fields.
requestnoobjectHTTP/WebSocket request definition for connection_request.
mapnoobjectMaps API response into dropdown options.
optionsnoarrayFixed options for static_options.
cacheMsnonumberCache 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 keyMeaning
instance_idDiscovered instance id.
instance_nameDiscovered instance name.
instance_serverDiscovered host/port or URL.
instance_typeInferred instance type.
instance_versionDiscovered app version.
instance_indexIndex in the discovered instance list.
instance_ordinal1-based slot when instances are ordered oldest-to-newest.
instance_selection_modeSet 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

TypePurpose
textSingle-line text input.
textareaMulti-line text input.
numberNumeric input.
selectFixed dropdown defined directly on the field.
dynamic_selectDropdown populated by a source.
checkboxBoolean field.
colorColor picker field.
fileFile path field.
hotkeyHotkey capture field.
hiddenHidden value field.
infoInformational field.

Field Fields

FieldRequiredTypeNotes
keyyesstringFlat placeholder key.
labelyesstringUser-facing label.
typenostringDefaults to text.
placeholdernostringPlaceholder text.
defaultnoanyDefault value.
optionsnoarrayRequired for useful select fields.
sourcenostringSource id for dynamic_select.
dependsOnnoarrayField keys this field depends on.
labelKeynostringExtra key that stores the selected option label.
minnonumberNumeric minimum.
maxnonumberNumeric maximum.
hintnostringHelp 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 Toggle and SheevChat has never seen that exact action target before, SheevChat sends true first.
  • After that first toggle, SheevChat remembers the target state and alternates between false and true.
  • 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:

  1. Select model.
  2. Fetch hotkeys for selected model.
  3. 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

FieldRequiredTypeNotes
idyesstringStable action id. Runtime type becomes <library-id>.<action-id>.
labelyesstringUser-facing action name.
descriptionnostringUser-facing action description.
contextsnoarrayDefaults to all contexts.
connectionIdnostringConnection used by the action.
fieldsnoarrayConfig fields.
executeyesobjectExecutor definition. Must include type.

Contexts

Accepted action contexts:

ContextMeaning
timerAvailable to timers.
automationAvailable to automations.
commandAvailable to custom commands.
padAvailable 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_websocket subscriptions 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, applies eventMatch, maps values with eventMap, and reports last-event diagnostics.
  • local_http_poll subscriptions can be started and stopped explicitly through the same API. SheevChat owns the timer, sends the declared HTTP request on a capped interval, applies eventMatch, maps values with eventMap, and reports last-event diagnostics.
  • local_udp subscriptions can be started and stopped explicitly through the same API. SheevChat owns a bounded local UDP listener, receives JSON/text/channel packets, applies eventMatch, maps values with eventMap, and reports last-event diagnostics.
  • local_tcp subscriptions 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, applies eventMatch, maps values with eventMap, and reports last-event diagnostics.
  • local_ipc subscriptions 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_mqtt subscriptions 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, applies eventMatch, maps values with eventMap, and reports last-event diagnostics.
  • Local event subscriptions start manually by default. Add autoStart: true when 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 with reconnect: true.
  • Local event subscriptions can emit SheevChat Automations through the explicit Imported app event trigger. Automation matching uses the installed library id, event stream id, and mapped eventMap values. 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 event automations from firing.
  • oauth_webhook subscription 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:

TypeMeaning
local_websocketApp can push events over a local WebSocket. Manual start/stop runner is available.
local_http_pollApp has pollable event/state endpoints. Manual start/stop runner is available.
local_udpApp can emit UDP messages to a local UDP listener. Manual start/stop runner is available.
local_tcpApp can push events over a local TCP socket. Manual start/stop runner is available.
local_ipcApp can emit events over an approved IPC protocol. Manual start/stop runner is available for supported presets.
local_mqttApp can publish events to an MQTT/MQTTS broker topic. Manual start/stop runner is available.
oauth_webhookCloud API has webhook/subscription concepts. Metadata-only today.

Fields:

FieldRequiredNotes
idyesStable subscription id.
labelnoHuman-readable name.
descriptionnoShort explanation for future details views.
typenoDefaults to local_websocket.
connectionIdyes for executable local subscriptionslocal_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.
autoStartnoDefaults 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 / autoReconnectnoDefaults 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.
reconnectDelayMsnoBase reconnect delay in milliseconds, clamped from 1000 to 60000. Defaults to 5000. Each retry uses a capped linear backoff.
maxReconnectAttemptsnoInteger from 0 to 20. Defaults to 5. 0 means no retry attempts.
subscribenoFor 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.
requestyes for local_http_poll unless poll/subscribe is usedHTTP request object sent on each poll. Uses the same shape as local HTTP actions, including method, path/url, headers, body, and bodyEncoding.
pollnoAlias for request in local_http_poll subscriptions.
listenyes for local_udp unless the connection defaultUrl supplies a portUDP 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 / responseFramingno for local_tcpraw or line. Defaults to connection.tcp.responseFraming, then connection.tcp.framing, then line for subscriptions.
topic / subscribeTopicyes for local_mqttMQTT topic filter to subscribe to. Must be allowed by connection.mqtt.topics.subscribe.
responseEncoding / parseno for local_mqttjson, text, hex, or base64. Defaults to json, falling back to text if JSON parsing fails.
intervalMsnoPoll interval for local_http_poll. SheevChat clamps it between 1000ms and 60000ms. Defaults to 5000ms.
unsubscribenoBest-effort request object sent before WebSocket or UDP runners stop. Not used by HTTP polling.
eventMatchnoExact-match object keyed by JSON path. Non-matching frames are ignored.
eventMapnoSame extraction-map shape as responseMap, used to document likely event variables.
eventNamespacenoStable namespace for future event variables.
eventTypesnoOptional type labels for mapped event values.
eventTemplatenoOptional 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 fieldNotes
libraryId / libraryNameInstalled action library that owns the stream.
id / label / descriptionSubscription identity from the manifest.
typeRuntime transport, such as local_websocket, local_tcp, local_ipc, or local_mqtt.
connectionIdManifest connection used by the stream.
enabled / runnable / statusManifest availability. runnable: false means the stream is metadata-only or failed validation.
autoStartWhether SheevChat should attempt to start this stream after library load/reload/install.
reconnect, reconnectDelayMs, maxReconnectAttemptsManifest reconnect policy after normalization and clamping.
outputsVariables produced by eventMap / eventTypes, useful for future trigger and response wiring.
runner.statusCurrent runtime state: not running, connecting, connected, listening, subscribed, reconnecting, or error.
runner.detail / runner.error / runner.errorCodeSanitized runtime diagnostics. These are intended for developers and advanced troubleshooting.
runner.eventCountCount of matching events observed by this runner.
runner.lastEventAtISO timestamp for the most recent matched event.
runner.lastEventValuesLast mapped values produced by eventMap.
runner.lastEventTextLast rendered eventTemplate, if declared.
runner.lastAutomationEventAtISO timestamp for the last imported app event routed into the Automation engine.
runner.lastAutomationSkippedAt / runner.lastAutomationSkippedReasonGuard diagnostics when a mapped event was intentionally not routed, such as rapid duplicate throttling.
runner.startedAtISO timestamp for the active or most recent runner session.
runner.reconnectAttempts / runner.nextReconnectAtReconnect 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:

TypeCurrent behavior
local_httpSends an HTTP request.
local_websocketSends a WebSocket message.
local_ipcSends a request through an approved local IPC protocol preset.
local_udpSends a bounded UDP packet.
local_tcpSends a bounded TCP message.
local_mqttPublishes a bounded MQTT message.
sheevchat_commandRuns an existing SheevChat command.
oauth_apiSends an OAuth-authenticated HTTP request using the stored bearer token from an OAuth connection.
hotkeySends a bounded Windows keystroke sequence through SheevChat's macro sender.
launch_applicationLaunches a local app/file through SheevChat's bounded launcher.
read_fileReads a small file from the library's scoped SheevChat storage folder.
write_fileWrites 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.path is required and may use placeholders from fields or variables.
  • execute.args is optional. Strings are split like a normal command line; arrays are passed as argument lists.
  • execute.workingDir is optional.
  • SheevChat starts the target with shell: false.
  • On Windows, if the target is an .exe and 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.path is 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.content or execute.text for plain text.
  • Use execute.json for JSON output.
  • execute.append: true appends instead of replacing.
  • execute.newline: true adds 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.path is 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.parse may be text or json; default is text.
  • Text reads return text, content, relativePath, and bytes.
  • 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:

FieldRequiredTypeNotes
pathnostringOne JSON-style path to try. Use either path or paths.
pathsnoarray of stringsFallback paths. SheevChat uses the first path that returns values.
defaultnoany JSON valueUsed when no path returns a value.
transformnostringOne of first, string, text, number, boolean, join, json. Defaults to first.
separatornostringUsed by join; defaults to , .

Transforms:

TransformResult
firstKeep the first matched raw value.
string / textConvert the first matched value to text.
numberConvert the first matched value to a number; uses default if conversion fails.
booleanAccepts true/false, 1/0, yes/no, on/off, connected/disconnected, ok/error-style values.
joinJoins all matched values with separator.
jsonSerializes 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:

FieldTypeNotes
outputLabelsobjectFriendly names keyed by responseMap key.
outputDescriptionsobjectShort human descriptions for produced variables.
outputTypesobjectHuman-readable value types such as string, number, boolean, array, or an app-specific type.
querybooleanMarks 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:

  1. Is the app controlled by local HTTP, local WebSocket, local UDP, OAuth/cloud API, hotkeys, files, or app launch?
  2. Does the app use a fixed port or dynamic port?
  3. If dynamic, does the app publish directory-scanned instance files, a single endpoint file, mDNS/DNS-SD service records, or another discoverable source?
  4. What request proves the app is connected and protocol-compatible?
  5. What API calls list user-selectable things?
  6. Which list fields are stable IDs?
  7. Which list fields are display names?
  8. Do any dropdowns depend on earlier dropdowns?
  9. Does the action payload require booleans/numbers as real JSON values?
  10. Does the API return channel-prefixed WebSocket messages?
  11. Can the WebSocket emit greetings, acknowledgements, events, or unrelated frames that require responseMatch?
  12. Does the WebSocket require an Origin or other safe custom handshake header?
  13. Does the API need authentication, pairing, signing, or secrets?
  14. 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:

  • schemaVersion is 1.
  • id values use only lowercase letters, numbers, hyphens, and underscores.
  • sources is an object, not an array.
  • Every dynamic_select.source exists in sources.
  • Every field dependsOn references another field in the same action.
  • Every connectionId exists in connections.
  • Every execute has a valid type.
  • Every field uses one of the documented field types. Boolean inputs use checkbox, not toggle.
  • 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 where entry 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 instance when results are instance-specific.
  • Channel WebSocket response paths include $.payload.
  • Nested channel payload response paths use $.payload.payload when 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_network only 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:

  1. Existing manifest capability
  2. Reusable runtime transport/protocol capability
  3. Trusted native-plugin capability
  4. Application-specific core integration
  5. 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.

FailureCauseCorrection
fields[n].type is unsupported: toggletoggle is not a SheevChat field type.Use checkbox for a boolean input or select for named operations.
Instance discovery finds nothing with %USERPROFILE% in directoryDiscovery 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 nodesOne 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 rowswhere 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_pathEither 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_pathParent 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_filterRows 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_mappingRows 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_fileInstance 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 idThe 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 instanceThe 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 variablesThe 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 nothingThe 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 responseThe 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 idThe 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 rememberEach 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 textThe 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 largeA 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 JSONThe 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 idlabelKey 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_api bearer-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.