Install scripts are a curated, turnkey solution for installing applications through TrueNAS in an opinionated "one-click" way. They eliminate the need for users to manually configure networking, resources, folders, and other technical settings by providing pre-configured, best-practice templates.
V5 install scripts introduce lifecycle hooks — TypeScript functions that execute at specific points during app install and upgrade. Hooks enable post-install automation like health checks, service configuration, OAuth login, and more — all without the user needing to open the app's own UI.
V5 is a strict superset of V4. The only new field is hooks. An existing V4 script can be promoted to V5 by changing "version": 4 to "version": 5 and optionally adding a hooks array.
For full details, see the Hooks Reference.
V6 declares everything through one shape — a hook. Its events decide when it runs; a hook whose events include userAction is user-triggerable, and its surfaces decide where the user can fire it.
Users see user-triggerable hooks as the app's Actions in the interface.
One hooks array now covers all of it:
| What you want | How you declare it |
|---|---|
| Automation during install or upgrade | "events": ["onAfterInstall"] |
| A button on the app card | no events at all (absent means ["userAction"]) |
| A cross-app link offered at install time | a userAction hook whose conditions reference another app (the pairing target) |
| A verb in the file browser | a userAction hook with a target of type files |
| A button on a dashboard widget | a userAction hook, referenced by id from the widget's buttons |
events is an array, and each entry is either a string or an object. The object form carries per-trigger configuration — from and to are semver ranges that gate an upgrade transition:
{
"id": "migrate-config",
"title": "Migrate configuration",
"events": [{ "event": "onBeforeUpgrade", "from": "< 2.0.0", "to": ">= 2.0.0" }],
"script": "myapp/migrate.ts",
"entrypoint": "migrateConfig"
}
conditions gate a hook against live system state. Each condition carries a role: visibility hides the hook entirely, availability shows it but disabled with a reason. Conditions that reference another app are also what HexOS reads to build cross-app pairings — no catalog code runs to produce them.
{
"id": "connect-tautulli",
"title": "Connect to Tautulli",
"description": "Link Tautulli to this Plex server for watch history and stats.",
"kind": "connect",
"conditions": [
{ "role": "visibility", "type": "appInstalled", "app": "tautulli" },
{ "role": "availability", "type": "appRunning", "app": "tautulli" },
{ "role": "availability", "type": "appRunning", "app": "plex" }
],
"rerun": "converge",
"script": "plex/connect_tautulli.ts",
"entrypoint": "run",
"timeout": 300
}
surfaces narrows where a user-triggerable hook appears — installPicker, card, fileBrowser, widget. Most scripts omit it, because HexOS derives the right answer from the declaration itself: a hook with a file target belongs to the file browser, a hook whose conditions reference another app (a pairing target) belongs to the install picker and the app card, and everything else is an app-card verb.
A V6 dictionary also declares dashboard widgets — read-only cached queries — with widgetsSchema: 2 and a widgets array. A widget can reference the app's own user-triggerable hooks by id in its buttons, so one hook declaration serves the card and the widget.
V6 is a superset of V5. A V5 script becomes V6 by changing "version": 5 to "version": 6; its event field becomes an events array.
V5:
{
"id": "configure-plex",
"event": "onAfterInstall",
"description": "Setting up Plex server",
"script": "plex/plex_hook.ts",
"entrypoint": "afterInstall"
}
V6:
{
"id": "configure-plex",
"title": "Pre-configure Plex",
"events": ["onAfterInstall"],
"description": "Setting up Plex server",
"script": "plex/plex_hook.ts",
"entrypoint": "afterInstall"
}
Three details go with the rename:
title — the user-facing name. A userOptional consent checkbox labels itself with it, so V5's userOptional.label moves up to titleevent field is rejected inside a V6 entry rather than silently ignored, so a half-converted hook fails loudlycondition moves onto the event object: "condition": { "fromVersionRange": "< 2.0.0" } becomes "events": [{ "event": "onBeforeUpgrade", "from": "< 2.0.0" }]. A leftover condition field is dropped, and the hook would then fire on every upgradeEverything else — script, scriptContent, entrypoint, timeout, retries, optional, inputs, and the rest of userOptional — is unchanged, field for field.
V5 scripts remain fully supported. Nothing forces the move; a V5 dictionary keeps parsing and running exactly as it does today.
For full details, see the Hooks Reference, Surfaces Reference, Widgets Reference, and Pairings Reference.
For supported applications, the installation process is streamlined:
For apps not yet curated or when you need to customize the configuration:
$LOCATION() macros for paths instead of hardcoded paths$HOST_PATH() and $MOUNTED_HOST_PATH() for storage configuration instead of manual object creationensure_directories_exists are no longer supportedowner field for apps that require specific user/group ownership (e.g., "postgres", "apps")snapshot config on data and config directories to enable automatic pre-update ZFS snapshots$MEMORY() for dynamic memory allocation to ensure apps work across different system configurationsapp_values structureoptional: true on hooks that are nice-to-have but shouldn't block app installation if they failsurfaces when you need to narrow the default placementvisibility for "this app isn't installed", availability for "it's installed but not running"app_values structure for the specific TrueNAS app versionawait in hook scripts — all ctx methods are async and must be awaitedctx.waitForApp() with appropriate timeouts