Getting started with the RSG 2.0 SDK
The RSG 2.0 SDK lets you write Roku SceneGraph (RSG) components in TypeScript. In this guide, you create an app, run it on a Roku device, and then read through Two Cowboys, an example app that ships with the SDK and mixes a BrightScript component and a TypeScript anchor node in one scene.
Before you begin
You need the following:
- A Roku device running Roku OS 15.1 or later, in developer mode, on the same network as your computer
- A device authorization token with the
rsg_typescriptclaim, which allows the device to sideload apps built with the SDK - Node.js 24 or later
- The
pnpmpackage manager, version 11 - Access to registry.npmjs.org
- A code editor
See Activating developer mode for more information. The SDK packages are public npm packages; therefore, you do not need a registry override, a token, or an account.
The RSG 2.0 SDK public beta starts October 29, 2026. APIs and documentation may change during the beta, and you cannot publish an app built with the SDK to the Roku Streaming Store during the beta.
Creating an app
The rk command-line tool creates new apps. It comes from the @roku-sdk/tooling package, and rk create writes an app that already lists every SDK package it needs and installs them for you.
To create an app, follow these steps:
-
Run
rk createwith a name for your app. The name uses lowercase letters, digits, and hyphens:npx --package @roku-sdk/tooling rk create my-first-appThe command creates the app files and installs dependencies from npmjs.com. You can use
--no-installto skip the install,--titleto set the app title,--componentto name the main component, and--outputto choose where the app is created. Seerk createfor more information. -
Change to the new directory:
cd my-first-app -
Optional: Install the launcher once to run
rkdirectly and skip thenpxstartup cost. Without the launcher, run the tool inside the app withnpx rk <command>:npx rk setup --install-cliThe launcher writes
~/.local/bin/rk. If that folder is not on yourPATH, the command prints the line to add to your shell profile. The remaining steps userk; addnpxin front if you skip the launcher.
The app generated by rk create contains the following files:
| File | Purpose |
|---|---|
package.json | The SDK dependencies and the build, test, lint, and type-check scripts |
pnpm-workspace.yaml | The install-script decisions that pnpm 11 requires before it installs anything; keep this file |
roku-config.ts | Build configuration: entry point, image processing, and the app manifest |
src/index.ts | App entry point that registers the anchor node |
src/components/my-first-app.tsx | The anchor node, with placeholder content, and its test |
src/compiled-components/BaseAppScene.xml | The scene that holds the anchor node |
src/compiled-components/BaseAppScene.brs | The scene's BrightScript, which sets initial focus |
pkg/source/Main.brs | The BrightScript entry point that creates the scene |
pkg/images/ | The app icon and splash screen |
.gitignore | Ignores .roku/, where rk can store a device developer password |
To add the SDK to an app you already have instead, see Installing @roku-sdk/* packages from npmjs.com.
The first time you build on macOS, the system may block binaries the SDK uses. If the build fails, open System Settings, select Privacy & Security, and select Open Anyway for each blocked binary.
Connecting your Roku device
The rk tool stores your devices so it knows where to sideload your app. To add a device and make it active, follow these steps:
-
Add your device by its IP address, and give it a name:
rk device add 192.168.1.42 --name living-room -
Store the device's developer password.
rk device adddoes not take a password; therefore, you set it in a second step:rk device update living-room --password <PASSWORD>If all your devices share one developer password,
rk device set-default-passwordsets it once for all of them. -
Set the device as active. You can refer to a device by its name, its ID, or its IP address:
rk device set-active living-room -
Confirm the device appears as active:
rk device ls
Running your app on a device
To build your app and sideload it, run rk dev:
rk devThe command checks types, builds the app, installs it on the active device, and launches it. Use these common options:
--device <id|ip|name>targets a specific device.--watchrebuilds and redeploys when files change.--consolestreams the device debug console to your terminal.--debugruns the app in debug mode, so you can attach a debugger.
See rk dev for the full list of options.
Capturing a screenshot
You can capture what your app shows on the TV and save it as an image (for example, to include in a bug report or in your own documentation).
To capture a screenshot, follow these steps:
-
Run the app on your active device. See Running your app on a device for more information.
-
Capture the screen:
rk ecp capture-screen --output ./screenshots/my-first-app.pngThe command saves the image to the path you give. If you pass a folder instead (for example,
--output ./screenshots/), the command names the filescreenshot-<timestamp>.png. You can use--format jpegfor a smaller JPEG file, and--device <id|ip|name>to capture from a device other than the active device. -
Open the image and confirm it shows your app.
The capture command needs developer mode enabled on the device and a device on your local network. If the command returns an error, run rk device ls to confirm you have an active device. See rk ecp for more information on the error messages.
Result
Your app launches on your Roku device and shows a centered title and subtitle. If it does not, run rk dev --console and check the output for errors.
Understanding the anchor node pattern
An RSG 2.0 SDK app mounts its TypeScript UI into SceneGraph through anchor nodes: SceneGraph components whose contents you write in TypeScript. You register each anchor node when the TypeScript engine starts, and then use it in a scene by name, like any other SceneGraph component. Everything beneath an anchor node is plain TypeScript, written in TSX with SolidJS.
The Two Cowboys example shows this pattern. Its scene shows two cowboys side by side. Ace is a BrightScript component, and Bo is a TypeScript anchor node. Each cowboy randomly says "Howdy!" every 2 to 6 seconds, and when one speaks, the other waves back. The example lives in examples/two-cowboys in the SDK. See Two cowboys example for a summary of the app.
The app has this structure:
TwoCowboysScene <- BrightScript scene (XML and BrightScript)
|-- Sun <- anchor node (TypeScript)
|-- AceTheCowboy <- BrightScript component
`-- BoTheCowboy <- anchor node (TypeScript)Ace has the same fields as Bo, but it is written entirely in XML and BrightScript. The scene treats both cowboys the same way, because a field is a field whichever language implements the component.
Registering the anchor nodes
The app entry point, src/index.ts, registers the anchor nodes when the TypeScript engine starts:
import { enableExceptionTagging, main, registerAnchorNode } from "@roku-sdk/rsg-ts";
import BoTheCowboy from "./components/bo-the-cowboy";
import Sun from "./components/sun";
main(() => {
enableExceptionTagging();
console.log("Registering Bo the Cowboy TypeScript component");
registerAnchorNode(BoTheCowboy);
console.log("Registering Sun TypeScript component");
registerAnchorNode(Sun);
});The scene, pkg/components/TwoCowboysScene.xml, then places the TypeScript and BrightScript components by name:
<AceTheCowboy id="aceTheCowboy" translation="[140, 80]" />
<BoTheCowboy id="boTheCowboy" wave="false" translation="[140, 80]" />Defining the fields
You define an anchor node with defineAnchorNode. The descriptor declares the SceneGraph fields the node exposes. Input fields carry values in, and output fields carry values out. This is the descriptor for Bo, from src/components/bo-the-cowboy.tsx:
const BoTheCowboyDescriptor = {
componentName: "BoTheCowboy",
extends: "Group" as const,
inputFields: {
wave: FieldType.Boolean()
},
outputFields: {
isSpeaking: FieldType.Boolean()
}
};
const BoTheCowboy: AnchorNodeImplementation<typeof BoTheCowboyDescriptor> = (inputs, outputs) => {
// ...
};
export default defineAnchorNode(BoTheCowboyDescriptor, BoTheCowboy);The component function receives inputs and outputs. You read an input by accessing it as a property, and you write an output by calling it as a function. Bo sets isSpeaking to true while the speech bubble is showing and back to false when it hides:
setShowSpeechBubble(true);
// Signal that Bo is speaking
outputs.isSpeaking(true);Read inputs values inside JSX, a memo, or an effect so that the value stays reactive. A bare inputs.wave at the top level of the component function is read one time and never updates.
Building the component
Bo is an ordinary SolidJS component. Import SolidJS APIs from @roku-sdk/solid, never from solid-js. Bo keeps the speech bubble state in a signal, and starts and stops its timer with onMount and onCleanup:
import { createSignal, onCleanup, onMount, Show } from "@roku-sdk/solid";
const [showSpeechBubble, setShowSpeechBubble] = createSignal(false);
onMount(() => {
console.log("Bo the Cowboy (TypeScript) mounted");
scheduleNextHowdy();
});
onCleanup(() => {
if (timeoutId) {
clearTimeout(timeoutId);
}
});The layout uses the coordinates of a 1920×1080 screen. The Show component adds the speech bubble to the tree only while showSpeechBubble() is true:
<Show when={showSpeechBubble()}>
<group translation={[160, 80]}>
<rectangle color="0xFFFFFFFF" width={200} height={60} />
<label
color="0x000000FF"
translation={[0, 0]}
font={SystemFont.Large}
text="Howdy!"
horizAlign="center"
vertAlign="center"
width={200}
height={60}
/>
</group>
</Show>Reacting to an input field
The wave input moves Bo's left arm. The arm's translation reads inputs.wave inside JSX, so SolidJS updates the node whenever the scene changes the field:
<rectangle color="0xFFDBACFF" width={18} height={50} translation={inputs.wave ? [17, 145] : [17, 180]} />Ace does the same job in BrightScript. It observes its own wave field and moves the arm in the callback:
m.top.observeField("wave", "handleWave")
function handleWave()
isWaving = m.top.wave
if isWaving
' Make Ace wave - move left arm up
if m.leftArm <> invalid
m.leftArm.translation = [17, 145]
end if
else
' Make him stop waving - move left arm back to resting position
if m.leftArm <> invalid
m.leftArm.translation = [17, 180]
end if
end if
end functionUsing the anchor node from BrightScript
You can host a TypeScript anchor node in a BrightScript scene, which lets you adopt the RSG 2.0 SDK one component at a time. The scene needs only ordinary SceneGraph. It declares the component in XML, as shown earlier, finds it by ID, observes its output fields, and sets its input fields. This is the scene's init() function in pkg/components/TwoCowboysScene.brs:
function init()
' Get references to the cowboy components
m.aceTheCowboy = m.top.findNode("aceTheCowboy")
m.boTheCowboy = m.top.findNode("boTheCowboy")
' Observe Ace's speaking status
m.aceTheCowboy.observeField("isSpeaking", "onAceSpeakingChanged")
' Observe Bo's speaking status
m.boTheCowboy.observeField("isSpeaking", "onBoSpeakingChanged")
end functionWhen Ace's isSpeaking output changes, the scene sets Bo's wave input, and the other way around:
function onAceSpeakingChanged()
isSpeaking = m.aceTheCowboy.isSpeaking
if isSpeaking
m.boTheCowboy.wave = true
else
m.boTheCowboy.wave = false
end if
end functionThe scene code is the same for Ace and Bo, so the scene does not need to know which language implements a component.
Running the example
To explore the example, open examples/two-cowboys in the SDK, install dependencies, and sideload it to your active device:
pnpm install
rk devSee Two cowboys example for the full walkthrough of the files, and Examples for the other apps that ship with the SDK.
Next steps
- See Two cowboys example for more information on the example in this guide.
- See Master sample for a larger app that ships with the SDK.
- See Importing and processing static images for more information on image imports.
- See Debugging Roku apps with AI for more information on connecting an AI assistant through
rk mcp. - See Directory layout for more information on project structure.
- See App configuration options for more information on
roku-config.ts. - See Focus Boundary for more information on focus and navigation.
Updated about 4 hours ago