> ## Documentation Index
> Fetch the complete documentation index at: https://docs.truffle.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Building Truffle Apps

> An overview of how Truffle apps work and how to build your own

A Truffle app is a containerized Python program that runs on your Truffle and extends what the on-device agent can do. Apps come in three shapes:

| Shape          | When it runs              | What it does                                                |
| -------------- | ------------------------- | ----------------------------------------------------------- |
| **Foreground** | On demand                 | Exposes MCP tools the agent can call during a Convo request |
| **Background** | On a schedule             | Submits context to the proactive agent                      |
| **Hybrid**     | On demand + on a schedule | Both: tools *and* scheduled context                         |

You pick the shape by including a `foreground`, `background`, or both sections in your `truffile.yaml`.

The supported development loop is intentionally CLI-first: scaffold locally,
validate locally, dry-run the deploy plan, then deploy to a connected device.

The bundled examples live under `truffile/app-store/` in the `truffile` repo. Treat them as reference implementations:

| Example          | Shape      | Auth/install pattern                                                       | Foreground style                                                             |
| ---------------- | ---------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `exa`            | Foreground | `text` API key                                                             | Native `ForegroundApp` wrapper around a remote JSON-RPC MCP service          |
| `home-assistant` | Foreground | `text` URL + long-lived token                                              | Native `ForegroundApp` with safety wrappers over Home Assistant MCP          |
| `obsidian`       | Foreground | `text` bridge URL + token, usually prefilled by `truffile obsidian deploy` | Native `ForegroundApp` talking to a local bridge                             |
| `arxiv`          | Hybrid     | `text` research interests                                                  | Native `ForegroundApp` tools plus scheduled recommendations                  |
| `whoop`          | Hybrid     | Standard `oauth`                                                           | Native `ForegroundApp` and `BackgroundWorkerApp` sharing OAuth token helpers |
| `notion`         | Hybrid     | Remote MCP `oauth` with dynamic client registration                        | Remote MCP proxy/wrapper plus background digest worker                       |
| `viator`         | Hybrid     | No auth; `welcome` step                                                    | Remote MCP proxy/wrapper plus travel context background                      |

***

## The SDK import surface

App code imports everything it needs from **`truffile.app_runtime`**. This is the module that ships with the `truffile` package when you `pip install truffile`.

```python theme={null}
from truffile.app_runtime import (
    ForegroundApp,
    BackgroundWorkerApp,
    ToolSpec,
    Submission,
    ok,
    err,
    phosphor_icon_url,
    AppAuthError,
    AppRuntimeFailure,
)
```

The source lives in `truffile/app_runtime/` in the repo and ships with the installed package. Once you `pip install truffile`, the same runtime is importable from app code running on your device.

There are two other entry points that import the same names:

* `from truffile.sdk import ...` — a curated re-export module with nice grouping. Use this if you prefer one obvious "public API" import.
* `from truffile import ForegroundApp, BackgroundWorkerApp, ToolSpec, ok, err, OAuth, AppHarness` — the most common names are also re-exported at the top level.

All three forms resolve to the same objects. Pick whichever reads best for you. The examples in this guide use `truffile.app_runtime` because it is the clearest app-authoring import surface.

***

## Scaffold a new app

The fastest way to start:

```bash theme={null}
truffile create my-app
```

```text theme={null}
✓ Created app scaffold: ./my-app
  Files:
  → truffile.yaml
  → my_app_foreground.py
  → my_app_background.py
  → icon.png

  Next: truffile validate ./my-app
```

Flags:

* `truffile create <name>` — set the name, prompt for base path
* `truffile create <name> --path <dir>` — set name and base path
* `truffile create` — fully interactive

The generated `my_app_foreground.py` and `my_app_background.py` are stubs using the real SDK classes. Delete whichever you don't need — the app type is inferred from which `foreground` / `background` sections you keep in `truffile.yaml`.

***

## Let an agent build the app for you

If you're working in the truffile repo with a coding agent — **Claude Code**, **Codex**, or any other agent that can read skill files — there's a built-in skill it can follow to build a Truffle app end-to-end: **`truffle-app-creator`** (at `truffile/skills/truffle-app-creator/SKILL.md`).

It's the fastest path for things like:

* *"Create a Truffle app that talks to \<service>"*
* *"Port this MCP server to Truffle"*
* *"Build an integration with \<API> and run it in the background"*

The skill handles architecture decisions (foreground/background/hybrid, auth type, base image), uses `truffile create` to scaffold, implements the client/auth/tools, writes tests, and runs `truffile validate` + `truffile deploy` for you. Just describe the service you want to connect and it takes over from there.

***

## `truffile.yaml` structure

Every app needs a `truffile.yaml`. Minimum viable config:

```yaml theme={null}
metadata:
  name: My App
  bundle_id: org.example.myapp
  description: |
    What this app does, in one short paragraph.
  icon_file: ./icon.png

  foreground:
    process:
      cmd: ["python", "my_app_foreground.py"]
      working_directory: /
      environment:
        PYTHONUNBUFFERED: "1"

  background:
    process:
      cmd: ["python", "my_app_background.py"]
      working_directory: /
      environment:
        PYTHONUNBUFFERED: "1"
    default_schedule:
      type: interval
      interval:
        duration: 30m
        daily_window: "00:00-23:59"

steps:
  - name: Install dependencies
    type: bash
    run: |
      pip install --no-cache-dir httpx

  - name: Copy app files
    type: files
    files:
      - source: ./my_app_foreground.py
      - source: ./my_app_background.py

  - name: Configure access token
    type: text
    content: |
      Enter an API key from https://example.com/settings/tokens
    fields:
      - name: api_key
        label: API Key
        type: password
        env: MY_APP_API_KEY
    validator:
      type: bash
      run: python ./my_app_foreground.py --verify
      timeout: 90
      error_message: |
        Could not verify MY_APP_API_KEY. Check the key is valid.
```

### Supported step types

Steps run during installation on the device, in order. The manifest supports:

| Type      | What it does                                                         |
| --------- | -------------------------------------------------------------------- |
| `bash`    | Run a shell command in the app's container (for dependencies, setup) |
| `files`   | Copy files from your repo into the container                         |
| `text`    | Collect input from the user and expose it as env vars on the process |
| `oauth`   | Run an OAuth install flow and persist token state for the app        |
| `welcome` | Show a simple informational install step                             |

See the [Install steps reference](/sdk/install-steps) for every option on each type — field shapes, validators, exit code semantics, and multi-field examples.

<Note>
  `vnc` / browser setup steps are not supported through `truffile`. `truffile validate` rejects them with an explanation and tells users to install those apps through Symphony Settings.
</Note>

***

## Auth patterns

Pick the smallest auth surface that matches the upstream service:

| Pattern          | Use it when                                                                  | Step type                                           | Example                                      |
| ---------------- | ---------------------------------------------------------------------------- | --------------------------------------------------- | -------------------------------------------- |
| No auth          | The upstream service is public or the app only needs static defaults         | `welcome` or no auth step                           | `viator`                                     |
| Text/config      | The user can paste an API key, token, URL, or local bridge value             | `text`                                              | `exa`, `home-assistant`, `obsidian`, `arxiv` |
| Standard OAuth   | The provider has normal auth/token endpoints and fixed client metadata       | `oauth`                                             | `whoop`                                      |
| Remote MCP OAuth | The upstream MCP server owns OAuth metadata or dynamic client registration   | `oauth` with resource metadata / DCR fields         | `notion`                                     |
| Local bridge     | The app on the device needs to talk back to a process on the user's computer | usually `text`, often prefilled by a helper command | `obsidian`                                   |

For `text` auth, add a validator that calls `python ./foreground.py --verify` or `python ./background.py --verify`. For OAuth, write a tiny auth helper around `OAuth` or `RemoteMcpOAuth`, store the token file path in an env var, and verify it in a follow-up `bash` step.

If an update reaches an auth step that cannot be refreshed non-interactively, fail clearly and tell the user to reauthenticate from Settings.

***

## Build and deploy flow

```bash theme={null}
truffile validate ./my-app
truffile deploy --dry-run ./my-app
truffile deploy ./my-app
```

* **`validate`** checks the `truffile.yaml` structure, confirms referenced files exist, and parses all Python files for syntax errors.
* **`deploy --dry-run`** builds the deploy plan without changing anything on the device, so you can see what files will be uploaded, what bash steps will run, and how the process config will look.
* **`deploy`** opens a build session on the device, uploads files, runs your steps in order, and registers the app.

Add `--interactive` to `deploy` to open a shell inside the app's container when the build steps finish — useful for installing extra packages or debugging before finalizing.

After deployment, use an agent-safe one-shot request as a routing smoke test:

```bash theme={null}
truffile convo --new --json --quiet --timeout 120 \
  "Use <tool behavior> and summarize the result"
```

Inspect `tool_calls` in the JSON result. This tests normal agent discovery and
tool routing only. Convo has no per-thread app allowlist, so the CLI cannot
attach the newly deployed app or guarantee that this request uses it. Use
`truffile infer --mcp <url>` before deployment when you need deterministic
local selection of the app's MCP server.

***

## Runtime stability pattern

Whatever network clients your app opens — HTTP sessions, database connections, MCP subprocesses — close them on shutdown:

```python theme={null}
import atexit

def _cleanup() -> None:
    try:
        client.close()
    except Exception:
        pass

atexit.register(_cleanup)
```

Leaving outbound connections open at process exit is the most common source of flaky redeploys and container runtime crashes. Every example app in the repo does this.

***

## Next steps

<CardGroup cols={2}>
  <Card title="Build a Foreground App" icon="bullseye" href="/sdk/focus-app">
    Use `ForegroundApp` to expose MCP tools the agent can call on demand.
  </Card>

  <Card title="Build a Background App" icon="clock" href="/sdk/ambient-app">
    Use `BackgroundWorkerApp` to run on a schedule and submit context to the proactive agent.
  </Card>
</CardGroup>
