# Multi-Function Apps

> Expose multiple entry points from a single app to share resources and reduce cold starts.

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

---

Expose multiple entry points from a single app to share resources and reduce cold starts.

---

## Why Multi-Function?

Instead of deploying separate apps for related operations, multi-function apps let you:

- **Share model loading** — Load heavy models once, use across functions
- **Reduce cold starts** — Warm workers serve any function immediately
- **Logical grouping** — Keep related operations together

**Example use cases:**
- Image models: `generate`, `upscale`, `inpaint`
- LLMs: `chat` (history-aware) vs `completion` (raw text)
- Data pipelines: `process`, `validate`, `export`

---

## Function Discovery

Functions are automatically discovered from public methods on your `App` class.

### Discovery Rules

#### Python

A method becomes a function if it meets **all** these criteria:

| Rule | Description |
|------|-------------|
| Public | Name doesn't start with `_` |
| Not reserved | Not `setup` or `unload` (lifecycle methods) |
| Has type hints | Both input parameter and return type annotated |
| Pydantic models | Input/output types are Pydantic models |
| Single parameter | Exactly one parameter (besides `self`) |

#### Node.js

A method becomes a function if:

| Rule | Description |
|------|-------------|
| Public | Name doesn't start with `_` |
| Not reserved | Not `setup`, `unload`, `onCancel`, `constructor` |
| Has schema exports | `{PascalName}Input` and `{PascalName}Output` Zod schemas exported |

An optional `{PascalName}Meta` export adds [function metadata](#function-metadata).

### Example

<div data-type="codegroup">

```python Python
from inferencesh import BaseApp, BaseAppInput, BaseAppOutput

class GenerateInput(BaseAppInput):
    prompt: str
    steps: int = 30

class UpscaleInput(BaseAppInput):
    image_uri: str
    scale: float = 2.0

class ImageOutput(BaseAppOutput):
    uri: str
    width: int
    height: int

class App(BaseApp):
    async def setup(self, config):
        self.model = load_diffusion_model()

    # ✓ Discoverable: public, typed, Pydantic models
    async def generate(self, input: GenerateInput) -> ImageOutput:
        image = self.model.generate(input.prompt, steps=input.steps)
        return ImageOutput(uri=save(image), width=512, height=512)

    # ✓ Discoverable
    async def upscale(self, input: UpscaleInput) -> ImageOutput:
        image = load(input.image_uri)
        result = self.model.upscale(image, input.scale)
        return ImageOutput(uri=save(result), width=1024, height=1024)

    # ✗ Not discoverable: starts with underscore
    async def _helper(self, data):
        pass

    # ✗ Not discoverable: no type hints
    async def process(self, input):
        pass
```

```javascript Node.js
import { z } from "zod";

export const GenerateInput = z.object({
  prompt: z.string(),
  steps: z.number().default(30),
});

export const UpscaleInput = z.object({
  imageUri: z.string(),
  scale: z.number().default(2.0),
});

export const GenerateOutput = z.object({
  uri: z.string(),
  width: z.number(),
  height: z.number(),
});

// Output schemas can be shared via aliasing
export const UpscaleOutput = GenerateOutput;

export class App {
  async setup(config) {
    this.model = await loadDiffusionModel();
  }

  // ✓ Discoverable: GenerateInput + GenerateOutput exported
  async generate(input) {
    const image = await this.model.generate(input.prompt, input.steps);
    return { uri: save(image), width: 512, height: 512 };
  }

  // ✓ Discoverable: UpscaleInput + UpscaleOutput exported
  async upscale(input) {
    const image = await load(input.imageUri);
    const result = await this.model.upscale(image, input.scale);
    return { uri: save(result), width: 1024, height: 1024 };
  }

  // ✗ Not discoverable: starts with underscore
  _helper(data) {}

  // ✗ Not discoverable: no ProcessInput/ProcessOutput exports
  async process(input) {}
}
```

</div>

---

## Input and Output Schemas

Each function has independent typed input/output schemas:

```python
class ChatInput(BaseAppInput):
    messages: list[dict]
    max_tokens: int = 1000

class ChatOutput(BaseAppOutput):
    response: str
    tokens_used: int

class CompletionInput(BaseAppInput):
    prompt: str
    max_tokens: int = 1000

class CompletionOutput(BaseAppOutput):
    text: str
    tokens_used: int

class App(BaseApp):
    async def chat(self, input: ChatInput) -> ChatOutput:
        ...

    async def completion(self, input: CompletionInput) -> CompletionOutput:
        ...
```

### Schema Generation

When you deploy, the system generates `functions_schema.json`:

```json
{
  "generate": {
    "input_schema": { ... },
    "output_schema": { ... }
  },
  "upscale": {
    "input_schema": { ... },
    "output_schema": { ... }
  }
}
```

A function with [metadata](#function-metadata) also carries `credentials` and `annotations`. Each key is left out when the function doesn't declare it.

### Pydantic Compatibility

Both Pydantic V1 and V2 are supported:

```python
# Pydantic V1
from pydantic import BaseModel

# Pydantic V2
from pydantic import BaseModel

# Both work — the system detects the version automatically
```

---

## Function metadata

A function can declare the credentials it uses and hints about what it does. In Python, decorate the method with `@function(...)`. In Node.js, export `{PascalName}Meta = meta({...})` next to the function's Input and Output schemas. Both leave the method itself unchanged, so discovery and run vs stream detection work as before.

<div data-type="codegroup">

```python Python
from inferencesh import BaseApp, function

class App(BaseApp):
    @function(
        credentials=[{"slug": "google", "capabilities": ["google.calendar.events"]}],
        annotations={"readOnlyHint": True},
    )
    async def list_events(self, input_data: ListEventsInput) -> ListEventsOutput:
        ...
```

```javascript Node.js
import { meta } from "@inferencesh/app";

export const ListEventsInput = z.object({ /* ... */ });
export const ListEventsOutput = z.object({ /* ... */ });
export const ListEventsMeta = meta({
  credentials: [{ slug: "google", capabilities: ["google.calendar.events"] }],
  annotations: { readOnlyHint: true },
});

export class App {
  async listEvents(input) { /* ... */ }
}
```

</div>

- `credentials`: the subset of the app's `inf.yml` `credentials:` this function uses. See [Per-function credentials](/docs/extend/credentials#per-function-credentials) for the fallback rule and deploy checks.
- `annotations`: [MCP tool annotations](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#tool-annotations) for the function.

### Annotations

| Field | Type | Default | Meaning |
|-------|------|---------|---------|
| `title` | string | none | Human-readable name for the tool |
| `readOnlyHint` | boolean | `false` | The function doesn't change anything outside the app |
| `destructiveHint` | boolean | `true` | The function may delete or overwrite data. Only meaningful when `readOnlyHint` is `false`. |
| `idempotentHint` | boolean | `false` | Calling it again with the same input has no further effect. Only meaningful when `readOnlyHint` is `false`. |
| `openWorldHint` | boolean | `true` | The function reaches external systems |

Defaults are the MCP defaults and apply to any field you leave out. Set `destructiveHint: false` explicitly on a function that writes but never deletes.

Annotations are shown to MCP clients and agents that use the function as a tool. They are hints: the platform doesn't enforce them. Anything automatic, such as approving a tool call without asking, trusts them only for apps in verified namespaces.

---

## Calling Functions

Specify the `function` parameter when calling the app.

### CLI

```bash
# Call specific function
belt app run user/image-model --function upscale --input '{"image_uri": "inf://...", "scale": 4.0}'

# Call default function (run)
belt app run user/image-model --input '{"prompt": "a sunset"}'
```

### Python SDK

```python
from inferencesh import Inference

client = Inference(api_key="...")

# Call specific function
result = client.run({
    "app": "user/image-model@abc123",
    "function": "upscale",
    "input": {"image_uri": "inf://...", "scale": 4.0}
})

# Call default function
result = client.run({
    "app": "user/image-model@abc123",
    "input": {"prompt": "a sunset"}
})
```

### JavaScript SDK

```typescript
import { Inference } from '@anthropic/inference-sdk';

const client = new Inference({ apiKey: '...' });

// Call specific function
const result = await client.run({
  app: 'user/image-model@abc123',
  function: 'upscale',
  input: { image_uri: 'inf://...', scale: 4.0 }
});
```

### REST API

An unknown `function` name returns **422** with code `validation_error`. The `data` array includes a field error on `/function` listing the available function names for that app version.

```bash
curl -X POST https://api.inference.sh/apps/run \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "app": "user/image-model@abc123",
    "function": "upscale",
    "input": {"image_uri": "inf://...", "scale": 4.0}
  }'
```

---

## Default Function

If no function is specified, resolution order is:

1. Explicit `default_function` in `inf.yml` (if set)
2. A function named `run` (if present)
3. The sole function (when the app has exactly one entry point)
4. Otherwise, an error — use `-f` / `--function` to pick one

`belt app get` and `belt app sample` list other functions with descriptions and copy-pasteable `belt app run` / `belt app sample` commands when an app has multiple entry points.

```python
class App(BaseApp):
    # This becomes the default function
    async def run(self, input: GenerateInput) -> ImageOutput:
        return await self.generate(input)

    async def generate(self, input: GenerateInput) -> ImageOutput:
        ...
```

---

## Common Patterns

### Multiple Related Operations

```python
class App(BaseApp):
    async def setup(self, config):
        self.model = load_model()  # Shared across all functions

    async def generate(self, input: GenerateInput) -> ImageOutput:
        """Generate from text prompt."""
        ...

    async def img2img(self, input: Img2ImgInput) -> ImageOutput:
        """Transform existing image."""
        ...

    async def inpaint(self, input: InpaintInput) -> ImageOutput:
        """Fill masked regions."""
        ...

    async def upscale(self, input: UpscaleInput) -> ImageOutput:
        """Increase resolution."""
        ...
```

### Chat vs Completion Modes

```python
class App(BaseApp):
    async def chat(self, input: ChatInput) -> ChatOutput:
        """Conversation with message history."""
        messages = input.messages
        response = self.model.chat(messages)
        return ChatOutput(response=response)

    async def completion(self, input: CompletionInput) -> CompletionOutput:
        """Single-shot text completion."""
        text = self.model.complete(input.prompt)
        return CompletionOutput(text=text)
```

### Process / Validate / Export Pipeline

```python
class App(BaseApp):
    async def process(self, input: ProcessInput) -> ProcessOutput:
        """Transform raw data."""
        ...

    async def validate(self, input: ValidateInput) -> ValidateOutput:
        """Check data quality."""
        ...

    async def export(self, input: ExportInput) -> ExportOutput:
        """Format for delivery."""
        ...
```

---

## Validation and Debugging

### Check Discovery Locally

Run validation to see discovered functions:

```bash
belt app validate
```

Output:
```
✓ discovered 3 functions: [generate, upscale, inpaint]
```

### Why Methods Get Skipped

Run with verbose mode to see skip reasons:

```bash
belt app validate --verbose
```

Output:
```
Checking method: generate
  ✓ public method
  ✓ not reserved
  ✓ has return type hint
  ✓ has 1 parameter
  ✓ parameter has type hint
  ✓ input type is Pydantic model
  ✓ output type is Pydantic model
  → DISCOVERED

Checking method: _helper
  ✗ private method (starts with _)
  → SKIPPED

Checking method: process
  ✗ missing return type hint
  → SKIPPED
```

### Test Functions Locally

```bash
# Test specific function
belt app dev --function upscale --input '{"image_uri": "...", "scale": 2.0}'

# Test default function
belt app dev --input '{"prompt": "test"}'
```

---

## Next

- [Multi-Function App Example](/docs/examples/multi-function-app) — Quick overview
- [Sessions](/docs/extend/sessions) — Enable stateful multi-call interactions
