RoT Terminal packages run entirely on the server via loadstring. The client never sees your code — only the text you print through api. Build a command, register it, publish it.
Your package never touches the client directly. It runs on the server, and the only thing that ever reaches a player's screen is text your code prints, or a UI description your code sends.
A single function that receives args and an api table. This is the entire surface of your package.
Every call runs server-side via loadstring. No client ever sees or downloads your source.
Only what you print with api.println, or a window built with api.ui, reaches the terminal.
Two SDKs cover the whole package surface: the Basic SDK for commands and output, and RoT UI for building windows.
Core rule: package code runs only on the server (via loadstring). The client never receives your code — only the text you print through api.
return { command = "myapp", -- the command name in the terminal run = function(args, api) api.println("Hello!", api.COLOR_OK) end, }
| Member | Description |
|---|---|
| api.print(text, level) | Print without a line break. |
| api.println(text, level) | Print a line. level: nil / "ok" / "err" / "info" / "dim" |
| api.player | The Player who ran the command. |
| api.require(fileName) | Load another file from this same package. |
| api.reg(cmdName, argsTable) | Register a PATH alias (see below). |
| api.ui(description) | Show a RoT UI window (see RoT UI SDK tab). |
Paste the first file's code (main), then end it with:
END1 Main
Paste the second file, end it with:
END2 Helper
…and so on for files in the middle of the upload.
The last file (or the only file, if there's just one) ends the entire submission. For that one, write simply:
END
or with a name:
END main.lua
Either form sends the package — no number is needed here, this is not "END3".
Inside main, pull in other files like this:
local helper = api.require("Helper")
A package can register itself an additional command name, but only from inside its own run():
api.reg("mytool", {"-inject", "pkg", "run", "myapp"})
After this, a player can type mytool instead of pkg run myapp.
reg only works from inside package code. If a player types reg -path ... directly into the terminal themselves, they get "Insufficient permissions." That's not a bug — it's the protection working as intended.| Command | Description |
|---|---|
| pkg create <name> <version> <short description> | Publish a new package. |
| pkg create -update <name> <version> <short description> | Publish a new version. |
| pkg create -delete <name> [version] | Remove globally (author / moderator only). |
| pkg delete <name> [version] | Remove from your own account. |
| pkg install <name> [version] | Install permanently. |
| pkg run <name> [version] | Execute (or just run the command directly). |
| pkg ready | List what you have installed. |
| pkg list | List everything in the catalog. |
RoT UI is the equivalent of WinUI for Windows apps, but for terminal packages. Since your code runs on the server, you can't call Instance.new("Frame") yourself — instead you describe a window as a plain Lua table, and api.ui() sends that description to the player. The terminal client does the actual drawing, the same way the -gui moderation panel renders package cards: you don't build the Frame by hand, you say "here's what should exist," and the terminal draws it.
A package must require rotui if it wants to build UI. Check this yourself at the start of run() — if the player doesn't have rotui installed, tell them clearly (see example below).
For run this package: "<pkg name>", run with -rotui[_version] or install rotui (1)return { command = "myapp", run = function(args, api) if args[1] ~= "-rotui" and args[1] ~= "-rotui_1.22" then api.println( 'For run this package: "myapp", run with -rotui[_version] or install rotui (1)', api.COLOR_ERR ) return end local rotui = api.require("rotui") -- if installed as a dependency api.ui({ type = "window", title = "My App", children = { { type = "label", text = "Hello!" }, { type = "button", text = "Click", onClick = "onPress" }, }, }) end, }
{
type = "window",
title = "Window title",
children = { <widget>, <widget>, ... }
}
{ type = "label", text = "text" }
{ type = "button", text = "text", onClick = "handlerName" }
onClick is a string (a handler name), not a function! On click, the server calls your run() again with:
args = {"-rotui-event", "handlerName"}
Handle it yourself:
run = function(args, api) if args[1] == "-rotui-event" then if args[2] == "onPress" then api.println("Button clicked!", api.COLOR_OK) end return end -- ...normal run continues here... end
| pkg install rotui | Once, ahead of time. |
| pkg run <app> -rotui_1.22 | Run a specific version. |
| pkg run <app> -rotui | Run any available version. |
pkg run rotui -sdk -- the same cheat sheet, straight from the game
The sandbox is checked before moderation ever looks at your package. Trip one of these and the upload never goes out: DataStoreService, HttpService, remote events of any kind, replicated/server storage, and raw loadstring. Your package speaks to players through api, and nothing else.