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_typescript claim, which allows the device to sideload apps built with the SDK
  • Node.js 24 or later
  • The pnpm package 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:

  1. Run rk create with a name for your app. The name uses lowercase letters, digits, and hyphens:

    npx --package @roku-sdk/tooling rk create my-first-app

    The command creates the app files and installs dependencies from npmjs.com. You can use --no-install to skip the install, --title to set the app title, --component to name the main component, and --output to choose where the app is created. See rk create for more information.

  2. Change to the new directory:

    cd my-first-app
  3. Optional: Install the launcher once to run rk directly and skip the npx startup cost. Without the launcher, run the tool inside the app with npx rk <command>:

    npx rk setup --install-cli

    The launcher writes ~/.local/bin/rk. If that folder is not on your PATH, the command prints the line to add to your shell profile. The remaining steps use rk; add npx in front if you skip the launcher.

The app generated by rk create contains the following files:

FilePurpose
package.jsonThe SDK dependencies and the build, test, lint, and type-check scripts
pnpm-workspace.yamlThe install-script decisions that pnpm 11 requires before it installs anything; keep this file
roku-config.tsBuild configuration: entry point, image processing, and the app manifest
src/index.tsApp entry point that registers the anchor node
src/components/my-first-app.tsxThe anchor node, with placeholder content, and its test
src/compiled-components/BaseAppScene.xmlThe scene that holds the anchor node
src/compiled-components/BaseAppScene.brsThe scene's BrightScript, which sets initial focus
pkg/source/Main.brsThe BrightScript entry point that creates the scene
pkg/images/The app icon and splash screen
.gitignoreIgnores .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:

  1. Add your device by its IP address, and give it a name:

    rk device add 192.168.1.42 --name living-room
  2. Store the device's developer password. rk device add does 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-password sets it once for all of them.

  3. 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
  4. 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 dev

The 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.
  • --watch rebuilds and redeploys when files change.
  • --console streams the device debug console to your terminal.
  • --debug runs 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:

  1. Run the app on your active device. See Running your app on a device for more information.

  2. Capture the screen:

    rk ecp capture-screen --output ./screenshots/my-first-app.png

    The command saves the image to the path you give. If you pass a folder instead (for example, --output ./screenshots/), the command names the file screenshot-<timestamp>.png. You can use --format jpeg for a smaller JPEG file, and --device <id|ip|name> to capture from a device other than the active device.

  3. 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 function

Using 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 function

When 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 function

The 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 dev

See Two cowboys example for the full walkthrough of the files, and Examples for the other apps that ship with the SDK.

Next steps


Did this page help you?