truffile deploy builds your app. They’re declared under the top-level steps: array in truffile.yaml:
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:- Install dependencies before anything that needs them. Put your
bashstep that runspip installearly. - Copy files before running them. Any
bashstep ortextvalidator that invokes a Python file needs a precedingfilesstep that uploads it. - Collect credentials after files are in place.
textvalidators and OAuth verify steps typically call one of your own Python scripts, so those scripts must already be on disk. - Verify OAuth after the OAuth step. Put a
bashstep afteroauththat callspython ./foreground.py --verifyand any background verify script.
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: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 /).
files
Uploads files from your app directory into the build container.
Options
Each entry in
files:
File vs directory
Ifsource 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:
./skills/foo.py → ./skills/foo.py, ./skills/subdir/bar.py → ./skills/subdir/bar.py, and so on.
Missing files
Ifsource 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 infields:
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 yourerror_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.
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
imap.gmail.com.
How env vars reach your process
Values collected bytext 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:
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, subclassOAuth 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.
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,runon bash,fileson files) produce errors. - Missing
sourcefiles infilessteps are caught. - Python syntax errors in any uploaded
.pyfile are caught.
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.