# Extending Apps

> Build and deploy your own tools.

**URL:** https://inference.sh/docs/extend/extending-apps
**Last updated:** 2026-10-12

---

**Build and deploy your own tools.**

The Grid is open for extension. If you need something that doesn't exist, create it.

→ [Extend overview](/docs/extend/overview) — developer path and related guides

---

## Overview

Apps are Python scripts with:
- **Typed inputs** — What the app accepts
- **Typed outputs** — What it returns
- **Lifecycle methods** — Setup, run, cleanup

The CLI helps you create, test, and deploy.

---

## Quick Start

### Install the CLI

```bash
curl -fsSL https://cli.inference.sh | sh
```

### Login

```bash
belt login
```

### Create an App

```bash
mkdir my-app && cd my-app
belt init
```

Answer the prompts:
- **Name**: `my-app`
- **Description**: "What my app does"
- **Category**: image, text, audio, etc.
- **Python version**: 3.10, 3.11, or 3.12
- **GPU required**: Yes or No

This creates:

```
my-app/
├── inf.yml           # Configuration
├── inference.py      # Your code
├── requirements.txt  # Dependencies
└── ...
```

---

## The App Template

### `inference.py`

```python
from inferencesh import BaseApp, BaseAppInput, BaseAppOutput, File
from pydantic import Field
from typing import Optional

class AppInput(BaseAppInput):
    prompt: str = Field(description="What to generate")
    style: str = Field(default="default", description="Output style")

class AppOutput(BaseAppOutput):
    result: str = Field(description="Generated text")
    image: Optional[File] = Field(default=None, description="Generated image")

class App(BaseApp):
    async def setup(self, metadata):
        """Called once when the worker starts.
        Load models, initialize resources here."""
        # self.model = load_your_model()
        pass
    
    async def run(self, input_data: AppInput, metadata) -> AppOutput:
        """Called for each request.
        Process input and return output."""
        
        # Access inputs
        prompt = input_data.prompt
        style = input_data.style
        
        # Log progress (visible in real-time)
        metadata.log("Processing...")
        
        # Do your work
        result = f"Generated: {prompt} in {style} style"
        
        # Return output
        return AppOutput(result=result)
    
    async def unload(self):
        """Called on shutdown. Cleanup resources."""
        pass
```

### Input/Output Types

| Type | Example | Description |
|------|---------|-------------|
| `str` | `"hello"` | Text |
| `int` | `42` | Integer |
| `float` | `3.14` | Decimal |
| `bool` | `true` | Boolean |
| `List[T]` | `[1, 2, 3]` | Array |
| `Optional[T]` | `null` or value | Nullable |
| `File` | `{"uri": "..."}` | File reference |

### Working with Files

**Input files** are downloaded automatically:

```python
async def run(self, input_data: AppInput, metadata) -> AppOutput:
    # File is ready to use
    image_path = input_data.image.path
    
    with open(image_path, "rb") as f:
        data = f.read()
```

**Output files** are uploaded automatically:

```python
async def run(self, input_data: AppInput, metadata) -> AppOutput:
    # Write to a file
    output_path = "/tmp/result.png"
    save_image(output_path)
    
    # Return it
    return AppOutput(image=File(path=output_path))
```

---

## Configuration

### `inf.yml`

```yaml
namespace: myteam
name: my-app
title: My App
description: What my app does
category: image
kernel: python-3.11

resources:
  gpu:
    count: 1
    vram: 8000       # MB
    type: nvidia
  ram: 16000         # MB
```

### Setup Parameters

Use an `AppSetup` class to define configuration that is fixed while the app is loaded, such as which model to load. Setup parameters replaced app variants.
See [Setup Parameters](/docs/apps/setup-parameters).

### Dependencies

**`requirements.txt`** — Python packages:

```
torch>=2.0.0
transformers>=4.30.0
accelerate
huggingface_hub
pillow>=9.0.0
```

List every package your code imports. The Python base environment includes only build tools, `pydantic` and `inferencesh`. See [Python environment](/docs/extend/configuration#python-environment).

**`packages.txt`** — System packages (apt):

```
ffmpeg
libgl1-mesa-glx
```

---

## Requirements

If your app needs external API keys or OAuth credentials, declare them:

```yaml
# inf.yml
secrets:
  - key: OPENAI_API_KEY
    description: For GPT-4 API calls

  - key: REPLICATE_API_TOKEN
    description: For model inference
    optional: true

credentials:
  - key: google.sheets
    description: Read/write spreadsheets

  - key: x.tweet.write
    description: Post tweets
    optional: true
```

**Required** items must be configured before the app runs.

**Optional** items won't block execution if missing.

→ [Learn more about Secrets & Credentials](/docs/secrets/overview)

---

## Multi-Function Apps

By default, apps expose a single `run` method. You can define multiple functions to handle different operations in one app:

```python
from inferencesh import BaseApp, BaseAppInput, BaseAppOutput, File
from pydantic import Field

class UpscaleInput(BaseAppInput):
    image: File = Field(description="Image to upscale")
    scale: int = Field(default=2, description="Scale factor")

class UpscaleOutput(BaseAppOutput):
    image: File = Field(description="Upscaled image")

class RemoveBgInput(BaseAppInput):
    image: File = Field(description="Image to process")

class RemoveBgOutput(BaseAppOutput):
    image: File = Field(description="Image with background removed")

class App(BaseApp):
    async def setup(self, metadata):
        # Load models for all functions
        pass

    async def upscale(self, input_data: UpscaleInput, metadata) -> UpscaleOutput:
        # Upscale logic
        ...

    async def remove_bg(self, input_data: RemoveBgInput, metadata) -> RemoveBgOutput:
        # Background removal logic
        ...
```

Each public method (other than `setup`/`unload`) becomes a callable function with its own input/output schema.

### Testing Multi-Function Apps

```bash
# Test a specific function
belt app test --function upscale --input upscale_input.json

# Test another function
belt app test --function remove_bg --input removebg_input.json
```

### Running Multi-Function Apps in the Cloud

```bash
belt app run user/image-tools --function upscale --input input.json
```

When used as an agent tool, each function appears as a separate tool the agent can call.

---

## Deploy

```bash
belt app deploy
```

The CLI:
1. Validates your app
2. Installs dependencies locally
3. Tests the structure
4. Packages everything
5. Uploads to inference shell

Output:

```
┌──────────────────────────────────────┐
│         deployment summary           │
├──────────────────────────────────────┤
│                                      │
│ final status: success                │
│ duration: 45s                        │
│ user: yourname                       │
│ app: my-app                          │
│ link: https://app.inference.sh/...   │
│                                      │
└──────────────────────────────────────┘
```

---

## Testing Locally

### Generate Example Input

```bash
belt app test --save-example
# Creates input.json
```

### Run Locally

```bash
belt app test --input input.json

# Multi-function apps: specify which function to call
belt app test --function upscale --input input.json
```

### Debug Mode

```bash
belt app test --input input.json --debug
```

---

## AI-Friendly Development

The CLI creates a `CLAUDE.md` file with instructions for AI assistants. This means you can:

1. Describe what you want to an AI (ChatGPT, Claude, etc.)
2. Share your app structure
3. Get working code
4. Deploy with `belt app deploy`

The template and structure are designed for AI code generation.

---

## Editing in the Workspace

After deploying, edit settings in the web interface:

- **Setup Parameters** — Configure initialization defaults
- **Images** — Card, thumbnail, banner
- **Description** — Help others find your app
- **Visibility** — Public or private
- **Lifecycle status** — Whether the app accepts new runs (`active`, `maintenance`, `deprecated`, or `retired`)

Set lifecycle status and an optional status message in the **Visibility** section on your app's detail page. Visitors see a banner when status is not `active`. `maintenance` and `retired` block new runs; `deprecated` still runs but signals that callers should migrate.

→ [Apps API — App lifecycle status](/docs/api/rest/apps#app-lifecycle-status)

These changes don't require the CLI.

---

## Best Practices

### Keep Apps Focused

```
❌ Don't: One app that does everything
✓ Do: Single-purpose apps that compose well
```

### Download Models in Setup

```python
async def setup(self, metadata):
    # Good: Download on first run, cached afterward
    # (huggingface_hub must be in requirements.txt)
    from huggingface_hub import hf_hub_download
    self.model_path = hf_hub_download("model-name", "model.bin")
```

Don't bundle large models — they're cached after first download.

### Log Progress

```python
async def run(self, input_data: AppInput, metadata) -> AppOutput:
    metadata.log("Step 1: Loading image...")
    # ...
    metadata.log("Step 2: Processing...")
    # ...
    metadata.log("Step 3: Saving result...")
```

Users see these in real-time.

### Handle Errors Gracefully

```python
async def run(self, input_data: AppInput, metadata) -> AppOutput:
    if not input_data.image.exists():
        raise ValueError("Image file not found")
    
    try:
        result = process(input_data)
    except ProcessingError as e:
        metadata.log(f"Error: {e}")
        raise RuntimeError(f"Processing failed: {e}")
```

---

## What's Next?

- [**Apps**](/docs/apps/overview) — See your app in the Grid
- [**Flows**](/docs/flows/overview) — Use your app in workflows
- [**Agents**](/docs/agents/overview) — Add your app as an agent tool
- [**Secrets & Credentials**](/docs/secrets/overview) — Declare external requirements
- [**API & SDK**](/docs/api/overview) — Run your app programmatically
