Skip to main content
Install steps run on the device, in order, when truffile deploy builds your app. They’re declared under the top-level steps: array in truffile.yaml:
Valid app manifest step types are bash, files, text, oauth, and welcome. Browser/VNC steps are rejected by truffile validate.
The local CLI builder supports oauth with a model-friendly flow: it prints the authorization URL, accepts a pasted callback URL or authorization code, exchanges it for tokens, writes the token file into the app container, and injects the token file env var into later build steps and the final app process. If an update reaches an auth step that cannot complete non-interactively, fail clearly and ask the user to reauthenticate from Settings.

Step ordering

Steps execute top-to-bottom. The three practical rules:
  1. Install dependencies before anything that needs them. Put your bash step that runs pip install early.
  2. Copy files before running them. Any bash step or text validator that invokes a Python file needs a preceding files step that uploads it.
  3. Collect credentials after files are in place. text validators and OAuth verify steps typically call one of your own Python scripts, so those scripts must already be on disk.
  4. Verify OAuth after the OAuth step. Put a bash step after oauth that calls python ./foreground.py --verify and any background verify script.
A safe default order: bash (dependencies) → files (copy scripts) → text or oauth (credentials) → bash (verify/finalize).

Common step fields

Most examples also use these optional fields:

bash

Runs a shell command inside the app’s build container. Use it for installing dependencies, creating directories, pre-warming caches, etc.

Options

Exit codes

If the command exits non-zero, the deploy aborts and the step is reported as failed:
There’s no retry. Write commands that are idempotent and handle their own errors — e.g. mkdir -p instead of mkdir, pip install --no-cache-dir instead of rebuilding caches.

The container environment

The base image is Alpine-based (apk is the package manager, not apt). Python is preinstalled. Commands run with the working directory set to the app’s exec directory (usually /).
You can open a shell in the container mid-deploy to experiment with commands before codifying them. Run truffile deploy --interactive <path> — after your steps finish, you get an interactive shell inside the same container. Type exit or Ctrl+D to finalize the deploy.

files

Uploads files from your app directory into the build container.

Options

Each entry in files:

File vs directory

If source points to a file, it’s uploaded to destination. If source points to a directory, every file inside it is uploaded recursively. __pycache__ directories are skipped automatically. The destination is treated as a destination directory, and files are placed underneath it preserving the relative structure:
This uploads ./skills/foo.py./skills/foo.py, ./skills/subdir/bar.py./skills/subdir/bar.py, and so on.

Missing files

If source doesn’t exist, deploy fails immediately:
truffile validate catches this before you even try to deploy — it resolves every source against the app directory and errors if anything is missing.

text

Prompts the user for one or more values during deploy, stores them as environment variables on your foreground/background processes, and optionally runs a validator command to verify the inputs immediately. Use text for API keys, access tokens, and other credentials the user can paste directly. Use oauth when the provider needs a browser authorization flow.

Top-level options

Field options

Each entry in fields: The value the user enters is exposed to your foreground and background processes via the env var named in env. The injection happens at the process level — by the time your Python code runs, os.getenv("GITHUB_ACCESS_TOKEN") returns the token the user entered.

Validator options

The validator runs a bash command after all fields are collected, with the collected values already set in the environment. If the command exits non-zero, install fails with your error_message. The env vars from your fields are prepended to the command, so python ./foo.py --verify runs with MY_API_KEY='whatever the user typed' already set.
The standard pattern is to add a --verify flag to your foreground or background script and call it from the validator. The BackgroundWorkerApp.main() entry point handles --verify automatically — it calls verify_worker(), prints the result, and exits 0 or 1. For foreground apps you wire it up yourself with argparse.

Single-field example

Multi-field example

Multiple fields in a single step are all prompted in order, then a single validator runs with all values in the environment:

Text field with a default

If the user just presses Enter, the value is imap.gmail.com.

How env vars reach your process

Values collected by text steps are injected into your foreground/background processes on top of whatever’s already in the environment: block under metadata.foreground.process or metadata.background.process. For example, this yaml:
results in a process that sees both PYTHONUNBUFFERED=1 and MY_APP_API_KEY=<user input> in its environment. You read the user-supplied value the same way as any other env var:

oauth

Runs an OAuth authorization flow during install and writes the resulting token payload to a file in the app container. Use it when the provider requires browser authorization instead of a pasted API key. There are two real patterns in the bundled examples:
  • Standard OAuth: fixed authorization/token endpoints and client metadata, like whoop.
  • Remote MCP OAuth: an MCP server exposes protected-resource metadata or uses dynamic client registration, like notion.

Standard OAuth example

Remote MCP OAuth example

OAuth fields

App-side OAuth helper

In Python, subclass OAuth for normal providers or RemoteMcpOAuth for remote MCP providers. The token helper should load from the env var named by token_file_env_name, expose get_oauth_payload(), and provide a --verify path that exits non-zero when the token is missing or expired. The whoop example uses WhoopOAuth(OAuth) and stores tokens at WHOOP_TOKEN_STORE_PATH. The notion example uses NotionAuth(RemoteMcpOAuth) and stores tokens at NOTION_OAUTH_TOKEN_FILE. For public/dynamically registered remote MCP clients, the CLI uses PKCE automatically when no client secret is available. Token JSON also includes refresh metadata such as client_id, client_secret when present, redirect_uri, token_endpoint, and resource, so app-side helpers can refresh later without needing separate env vars.

welcome

Shows informational install content and requires no credentials. Use it when the app is ready to run after files are copied and optional verification has passed.
The viator example uses this pattern after a bash verify step because its upstream MCP server does not require user auth.

Validating before you deploy

truffile validate catches most step-level problems before you ever hit the device:
  • Unknown step types are rejected with a clear message that lists the supported manifest types.
  • vnc / browser setup steps are rejected with a clear message telling the user to install through Symphony Settings.
  • Missing required fields (type, run on bash, files on files) produce errors.
  • Missing source files in files steps are caught.
  • Python syntax errors in any uploaded .py file are caught.
Run truffile validate ./my-app before every truffile deploy — it’s nearly instant and catches everything except runtime failures in your bash commands and text validators.