JavaScript and TypeScript Plugins
For a simple, fast, installable Stream Deck plugin, use the official Elgato SDK with TypeScript.
1. Prerequisites​
Current Elgato SDK 2.0.0 docs list:
- Node.js 24 or newer.
- Stream Deck app 7.1 or newer.
- Stream Deck hardware, or Stream Deck Mobile for basic testing.
- The Stream Deck CLI from npm.
- A text editor with TypeScript support.
Install the CLI:
npm install -g @elgato/cli@latest
streamdeck -v
The CLI also has the shorthand alias sd.
2. Create a plugin​
streamdeck create
Use a reverse-DNS UUID:
com.yourcompany.timer
com.yourcompany.deployment-panel
de.example.obs-tools
Keep UUIDs lowercase and stable. Once users have actions in their profiles, changing plugin or action UUIDs breaks those assignments.
3. Generated project shape​
.
├── com.example.timer.sdPlugin/
│ ├── bin/
│ ├── imgs/
│ ├── logs/
│ ├── ui/
│ └── manifest.json
├── src/
│ ├── actions/
│ └── plugin.ts
├── package.json
├── rollup.config.mjs
└── tsconfig.json
The important split:
src/contains source code.*.sdPlugin/is the compiled plugin directory Stream Deck loads.manifest.jsondescribes plugin metadata, actions, icons, supported OS versions, Node settings, and UI paths.ui/contains property inspector HTML.imgs/contains plugin/action imagery.logs/is used by the SDK logger.
4. Development loop​
npm run watch
The scaffolded watch script builds the plugin and restarts it in Stream Deck when source or manifest files change.
Manual loop:
npm run build
streamdeck restart com.example.timer
Useful CLI commands:
streamdeck link path/to/com.example.timer.sdPlugin
streamdeck list
streamdeck restart com.example.timer
streamdeck stop com.example.timer
streamdeck validate path/to/com.example.timer.sdPlugin
streamdeck pack path/to/com.example.timer.sdPlugin
5. Minimal action​
import { action, KeyDownEvent, SingletonAction, WillAppearEvent } from '@elgato/streamdeck';
type CounterSettings = {
count?: number;
};
@action({ UUID: 'com.example.timer.counter' })
export class CounterAction extends SingletonAction<CounterSettings> {
override async onWillAppear(ev: WillAppearEvent<CounterSettings>): Promise<void> {
await ev.action.setTitle(`${ev.payload.settings.count ?? 0}`);
}
override async onKeyDown(ev: KeyDownEvent<CounterSettings>): Promise<void> {
const count = (ev.payload.settings.count ?? 0) + 1;
await ev.action.setSettings({ count });
await ev.action.setTitle(`${count}`);
}
}
This pattern is enough for many simple plugins:
- Read settings from the event payload.
- Perform one short action.
- Persist changed settings.
- Update the visual state.
6. Register the action​
The action needs both implementation and manifest metadata.
{
"$schema": "https://schemas.elgato.com/streamdeck/plugins/manifest.json",
"Author": "Example",
"Name": "Timer Tools",
"Description": "Small Stream Deck timer utilities.",
"Version": "1.0.0.0",
"UUID": "com.example.timer",
"CodePath": "bin/plugin.js",
"SDKVersion": 2,
"Software": {
"MinimumVersion": "7.1"
},
"Nodejs": {
"Version": "24"
},
"OS": [
{ "Platform": "mac", "MinimumVersion": "13" },
{ "Platform": "windows", "MinimumVersion": "10" }
],
"Actions": [
{
"UUID": "com.example.timer.counter",
"Name": "Counter",
"Icon": "imgs/action-icon",
"States": [
{
"Image": "imgs/action-state"
}
]
}
]
}
Use the current schema URL in manifest.json for editor validation.
7. Common action events​
| Event | Use it for |
|---|---|
onWillAppear | Initialize title/image when action appears on a profile |
onWillDisappear | Stop timers, unsubscribe, cleanup |
onKeyDown | Respond immediately to a key press |
onKeyUp | Trigger only after release, useful for click-like behavior |
onDialRotate | Handle Stream Deck + dial rotation |
onDialPress | Handle dial button press |
onTouchTap | Handle Stream Deck + touch strip interactions |
8. Settings pattern​
Use settings for user configuration and small per-action state.
Good settings:
- selected profile or target,
- API base URL,
- display mode,
- count/toggle state,
- non-secret IDs.
Bad settings:
- long caches,
- private tokens,
- high-frequency telemetry,
- generated files that change after packaging.
For secrets, use the operating system credential store, a local companion service, or an explicit user-managed configuration outside the distributable plugin.
9. Debugging​
Elgato's SDK supports Node.js debugging. In VS Code, attach to the Stream Deck plugin Node process while the plugin is running.
The manifest can configure Node debugging:
{
"Nodejs": {
"Version": "24",
"Debug": "enabled"
}
}
Use fixed ports only when necessary:
{
"Nodejs": {
"Version": "24",
"Debug": "--inspect=127.0.0.1:12345"
}
}
10. When JavaScript is enough​
Plain JavaScript is acceptable when:
- the plugin has one or two actions,
- the action settings are trivial,
- no shared domain model exists,
- you want the lowest possible friction.
11. When TypeScript is worth it​
Use TypeScript when:
- actions share settings or payload models,
- the plugin supports keys and dials,
- a property inspector talks to the plugin,
- async state matters,
- users depend on stable behavior.
For new work, TypeScript is the better default.