A package is a directory a Robot can execute without the repository, the Studio or the TypeScript sources. You build it once, inspect the manifest it carries and hand the directory to any Robot host.
#Building a package
tsx robot/cli.ts package process-orders --out ./out
buildPackage loads the automation from the registry, bundles the automation, the framework runner and the targets into one CommonJS file with esbuild and writes four files:
out/process-orders/
automation.cjs bundled automation, framework and targets
manifest.json the machine-readable contract
config.json runtime defaults (url, retry)
package.json declares the single host dependency, playwright
Playwright is not bundled. It is a host capability, the way a Docker base image provides Node. The package only declares the version it needs. That is why a package travels small and why the bundle keeps its dynamic requires native as CommonJS.
#What the manifest declares
manifest.json is the contract a Robot reads before it runs anything:
{
"formatVersion": 1,
"id": "process-orders",
"name": "tasksultan-process-orders-package",
"version": "0.1.0",
"main": "automation.cjs",
"configFile": "config.json",
"targetUrl": "http://127.0.0.1:4100",
"capabilities": ["web", "excel"],
"engines": { "node": ">=20", "playwright": "1.49.0" },
"contentHash": "sha256:...",
"builtAt": "2026-10-03T09:15:00.000Z",
"builtBy": "tasksultan robot package"
}
id is the product key and version is a label. The identity of the bytes is contentHash, a SHA-256 over automation.cjs. The package is immutable: the Robot reads it, executes it and writes evidence elsewhere. It never edits the package.
#Inspecting a package before you run it
Inspect the manifest as plain JSON. There is no build step and no Studio needed to read it. The fields worth a look are formatVersion, capabilities, engines and contentHash.
The Robot checks contentHash itself on every run-package call. If the bytes do not match the manifest, the Robot refuses:
# the Robot rebuilds the digest and compares; a mismatch is a refusal, not a warning
tsx robot/cli.ts run-package ./out/process-orders
# Package contentHash mismatch (...): refusing to execute a tampered or corrupted package.
robot list and robot show <automationId> <runId> show what runs and what a finished run recorded. A package directory is portable input, so it is worth copying it to a host, hashing automation.cjs yourself and comparing to contentHash before a first run.
#Running without the source tree
tsx robot/cli.ts run-package ./out/process-orders --artifacts ./evidence
The Robot reads manifest.json, requires formatVersion to be 1, verifies contentHash, loads the entry named by main and runs the same transaction lifecycle as run. No repository checkout is involved. The host provides node_modules with the declared Playwright and the browser binaries.
Machine-specific values never live in the package. Config precedence, from lowest to highest, is the automation's own defaults, then the package's config.json, then the Robot environment (TS_TARGET_URL, TS_DATA_DIR, TS_ROBOT_ID, TS_ENVIRONMENT, TS_CRED_*), then run or job parameters such as --url, --runId and --artifacts. Data files are referenced by relative name and resolved against TS_DATA_DIR, so the same package runs in dev against ./data and in prod against a provisioned directory without a rebuild.
#Things that catch people out
The Robot speaks formatVersion 1 only. A package from a newer format is refused, not guessed at.
A built package declares engines.node as >=20, which is what the packager emits and what a test pins, not the floor of the repository that produced it. Read it as what the package declares. Check your host's Node version against your own requirement rather than assuming the field is the tree's.
A package is a directory today, not a zip. Treat it as the unit to copy and version. Keep the sources in the repository as the single source of truth: automation.cjs is a build artifact, not something to edit.