# Creating an App

> Scaffold a new app with the CLI.

**URL:** https://inference.sh/docs/extend/creating-app
**Last updated:** 2026-10-15

---

Scaffold a new app with the CLI.

---

## Create App

```bash
# Python app (default)
belt app init my-app

# Node.js app
belt app init my-app --lang node

# Set the title shown on the app's page (default: made from the name)
belt app init my-app --title "My App"
```

Or interactive mode:

```bash
belt app init
```

The interactive wizard asks for name, title, description, **category**, kernel, and language. For **category**, the CLI loads the public store list from [`GET /store/categories?type=app`](/docs/api/rest/store#list-store-categories) (sorted by `rank`, same slugs as the Grid). Pick a slug from the menu; it is written to `inf.yml` as `category`.

If the category list cannot be loaded (offline, API error, or empty response), the CLI prompts for a category **slug** as plain text (default placeholder `other`). Deploy does not check the slug against that list; fix `category` in `inf.yml` later or run [`belt app deploy --meta`](/docs/extend/configuration#store-images) after the app exists.

Non-interactive `belt app init my-app` still uses the default category from the scaffold template until you edit `inf.yml`.

---

## Vendor API apps

A wrapper around a vendor's HTTP API is an ordinary Python app with one function per upstream operation. The `api` template scaffolds one by hand:

```bash
belt app init my-vendor --template api
```

It gives you an httpx client with retries, a credential declaration for the key, `test_requests/` per function, a `pricing.md` skeleton and a README with the finishing steps. (belt 1.22+)

### From an OpenAPI spec

When the vendor publishes an OpenAPI 2.0, 3.0 or 3.1 document, belt generates the apps from it:

```bash
belt app init --openapi https://docs.tavily.com/documentation/api-reference/openapi.json
belt app init --openapi ./spec.yaml --vendor acme
belt app init --openapi https://api.acme.com/openapi.json --openapi-header "Authorization: Bearer $ACME_KEY"
```

Only a parseable document is accepted. A docs page is not a spec: find the JSON or YAML file it is built from (the vendor's docs often link it, as do their SDK repositories and [APIs.guru](https://apis.guru)).

What it writes, under `<vendor>/`:

| File | Owner | Contents |
|------|-------|----------|
| `openapi.plan.yaml` | you | How the spec becomes apps: operations grouped into apps (by tag, else by first path segment, else one app named `core`) with their function names, the security schemes bound to secret names, the credential slug and provider seed. Edit and rerun. |
| `<app>/_generated.py` | belt | Pydantic models for every request and response, one `Operation` descriptor per function (method, path, where each field goes, auth, MCP annotations from the HTTP method, per-function credentials from the operation's security) and an Input/Output class per function. Rewritten on every rerun. |
| `<app>/inference.py` | you | One thin function per operation calling through [`inferencesh.upstream`](/docs/extend/app-code#calling-a-vendor-api). Written once. Quirks, pagination, reshaped responses and `output_meta` go here. |
| `<app>/inf.yml` | you | `credentials:` keyed by the vendor slug with every secret the schemes need and a `company:` seed, `default_function` set to the cheapest read, the spec's description. Written once; only the `credentials:` block follows the plan on reruns. |
| `<app>/test_requests/*.json` | belt | One request per function from the spec's examples, placeholders for required fields without one. |
| `<app>/requirements.txt`, `pricing.md` | you | Pinned `inferencesh`, one pricing row per function. Written once. |

Rerun with the plan file to regenerate after a spec change or a plan edit:

```bash
belt app init --openapi acme/openapi.plan.yaml
```

A regeneration rewrites `_generated.py`, `test_requests/` and the `credentials:` block of `inf.yml`. An operation the spec no longer has is refused by name, so a renamed `operationId` surfaces instead of silently dropping a function; operations the plan does not list are reported so you can add them to an app. A function you drop from the plan keeps its method in `inference.py` and its row in `pricing.md` until you remove them; the rerun names both.

The vendor credential must hold every secret of every scheme the plan lists, or the app stays "setup required" for everyone. Drop a scheme nobody can provide, and the functions that need it, rather than inventing a second key for the vendor.

Response models are lenient (every field optional), so a vendor answer that strays from its spec does not fail the call; the unchanged response is always on the output's `raw`. Request models are strict: required input is required.

What the CLI leaves to you, in order: check the secret names in the plan against `belt credentials list` (an existing credential for the vendor already names its keys), regroup or rename functions in the plan, test each function (`belt app test --function <name> --input test_requests/<name>.json`), finish `inference.py`, fill `pricing.md` and `metadata.tags`, deploy.

Models come from [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator), run through [uv](https://docs.astral.sh/uv/) (`uvx`). Without uv, install it yourself and point `BELT_DATAMODEL_CODEGEN` at the `datamodel-codegen` command. (belt 1.25+, inferencesh 0.29+)

---

## What's Created

<div data-type="codegroup">

```text Python
my-app/
├── inf.yml           # Configuration
├── inference.py      # Your code
├── requirements.txt  # Python dependencies
├── packages.txt      # System dependencies (optional)
├── skills/           # AI coding agent guidance
└── README.md
```

```text Node.js
my-app/
├── inf.yml           # Configuration
├── src/
│   └── inference.js  # Your code
├── package.json      # Node.js dependencies
├── packages.txt      # System dependencies (optional)
├── skills/           # AI coding agent guidance
└── README.md
```

</div>

---

## Files Explained

| File | Python | Node.js | Purpose |
|------|--------|---------|---------|
| `inf.yml` | Yes | Yes | App settings, resources |
| `inference.py` | Yes | — | App logic (Python) |
| `src/inference.js` | — | Yes | App logic (Node.js) |
| `requirements.txt` | Yes | — | Python packages (pip) |
| `package.json` | — | Yes | Node.js packages (npm/pnpm) |
| `packages.txt` | Yes | Yes | System packages (apt) — optional |
| `skills/` | Yes | Yes | Guidance for AI coding assistants |

---

## Dependencies

### App Packages

<div data-type="codegroup">

```text requirements.txt (Python)
torch>=2.0
transformers
accelerate
```

```json package.json (Node.js)
{
  "type": "module",
  "dependencies": {
    "zod": "^3.23.0"
  }
}
```

</div>

### System Packages (packages.txt)

For system-level dependencies (installed via apt):

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

---

## Base Images

Apps run in containers with these base images:

| Type | Image | Use Case |
|------|-------|----------|
| GPU | `docker.inference.sh/gpu:latest-cuda` | CUDA GPU apps |
| CPU | `docker.inference.sh/cpu:latest` | CPU-only apps |

> **Note:** Currently only NVIDIA CUDA GPUs are supported for GPU apps.

### Python environment

Each Python app version gets its own venv. It starts with build tools (`setuptools`, `setuptools-scm`, `wheel`, `wheel-stub`, `build`, `packaging`, `ninja`, `scikit-build-core`, `cython`, `flit-core`, `numpy`) plus `pydantic` and `inferencesh`.

`torch` and its CUDA wheels, `accelerate`, `huggingface_hub`, `transformers` and `diffusers` are not preinstalled. List every package your code imports in `requirements.txt`. `transformers` and `diffusers` need `accelerate` at runtime for `device_map`, `low_cpu_mem_usage` and `enable_model_cpu_offload()`.

App versions that already have an environment keep it. New versions, and versions set up on a new worker, get this base.

---

## Next

→ [App Code](/docs/extend/app-code)
