---
name: datatailr-deploy-excel-addin
description: Deploy an Excel add-in on the Datatailr platform using the Python SDK. Excel add-ins expose Python functions as Excel worksheet functions. Use when the user asks to deploy an Excel add-in, expose Python functions to Excel, or create Excel UDFs on Datatailr / dt.
---

# Deploy an Excel Add-in on Datatailr

## Overview

A Datatailr Excel add-in exposes Python functions as Excel worksheet functions (UDFs). Users call server-side computations directly from Excel spreadsheets. Functions can return single values or stream live data. The `datatailr` Python SDK handles packaging, image building, and scheduling. The SDK is already installed in the workspace -- do not look for it.

## Project layout

The deploy script must live **outside** the addin module directory. Do not use hyphens in directory names -- use underscores.

```
my_project/
├── my_addin/
│   ├── __init__.py
│   └── addin.py      # Add-in code
└── deploy.py         # Deploy script (outside my_addin/)
```

## Add-in code

Create an `Addin` instance and decorate functions with `@addin.expose()` to make them available in Excel. Include a `main(port, ws_port)` function -- the deploy script needs it as the entrypoint, and it is useful for local debugging. At runtime the platform launches the add-in directly via `dt-excel.sh` and does not call `main()`.

```python
# my_addin/addin.py
from datatailr.excel import Addin

addin = Addin("My Addin", "Description of the add-in")

@addin.expose(description="Adds two numbers", help="Provide a and b")
def add(a: float, b: float) -> float:
    return a + b

@addin.expose(description="Multiplies two numbers", help="Provide a and b")
def multiply(a: float, b: float) -> float:
    return a * b

def main(port=8080, ws_port=8000):
    addin.run(port, ws_port)

if __name__ == "__main__":
    main()
```

### `@addin.expose()` parameters

- `description`: short description shown in Excel.
- `help`: help text for the function.
- `streaming=True`: for functions that push live data continuously. Streaming functions receive a `Queue` as the first parameter.

### Streaming functions

```python
from datatailr.excel import Addin, Queue
import time

@addin.expose(description="Streams live prices", help="Provide initial price", streaming=True)
def stream_price(queue: Queue, initial_price: float) -> list:
    price = initial_price
    while True:
        price *= (1 + 0.001)
        queue.push([[price]])
        time.sleep(0.1)
```

### Multi-dimensional data (ranges)

**Input**: When a user selects a cell range in Excel (e.g., A1:C10), the function receives it as a `numpy.ndarray`. Type-hint the parameter with the element type (e.g., `float`) -- the SDK accepts both scalars and `numpy.ndarray` for any annotated type.

**Output**: Return a list of lists to fill multiple cells. Each inner list is one row. A `numpy.ndarray` is also accepted -- it is converted via `.tolist()` automatically.

```python
import numpy as np

@addin.expose(description="Cross-rate matrix for currency pairs", help="Provide base and quote currency lists")
def rate_table(bases: np.ndarray, quotes: np.ndarray) -> list:
    """Accepts a column of base currencies and a column of quote currencies.
    Returns an N x M matrix of cross rates."""
    rates = {"EUR": 1.0, "USD": 1.08, "GBP": 0.86, "JPY": 161.0}
    result = []
    for b in bases.flatten():
        row = []
        for q in quotes.flatten():
            row.append(rates.get(str(q), 1.0) / rates.get(str(b), 1.0))
        result.append(row)
    return result
```

**Streaming multi-dimensional data**: push a list of lists (or `.tolist()` on an array) to `queue.push()`.

```python
@addin.expose(description="Streams a matrix of random numbers", help="Provide rows and cols", streaming=True)
def random_matrix(queue: Queue, m: int, n: int) -> list:
    X = np.random.rand(int(m), int(n))
    while True:
        X += np.random.normal(0, 0.01, X.shape)
        queue.push(X.tolist())
        time.sleep(0.1)
```

### Key rules

- `main(port, ws_port)` is required as the deploy script entrypoint and for local debugging. The platform does **not** call `main()` at runtime -- it launches `dt-excel.sh` directly, which imports the `addin` object from the module.
- The `Addin` instance variable must be named `addin` in the module. The `Addin.__init__` method sets `__dt_addin__` on the module automatically -- you do not need to set it yourself, but the instance **must** be called `addin`.
- **Use only plain type hints**: `float`, `int`, `str`, `bool`, `numpy.ndarray`. Do **not** use generic types like `List[float]`, `list[list[str]]`, or `Optional[int]` -- they cause "Invalid argument" errors in the Excel bridge. The platform infers dimensionality from the actual value, not from the type hint.
- Always include `numpy` in `python_requirements` if any function uses `numpy.ndarray` parameters or does array operations.

## Deploy script

Import the `main` function and use the `ExcelAddin` class. No `framework` parameter.

```python
# deploy.py
from my_addin.addin import main
from datatailr import ExcelAddin, Resources

addin = ExcelAddin(
    name="My Excel Addin",
    entrypoint=main,
    resources=Resources(memory="4g", cpu=1),
    python_requirements=["numpy", "pandas"],
)
addin.run()
```

Key parameters:
- `name`: human-readable name on the platform.
- `entrypoint`: the `main` function (imported callable, not a string).
- `resources`: CPU and memory. Excel add-ins often need more memory (e.g., `"4g"`).
- `python_requirements`: list of pip packages the add-in needs.
- `addin.run()` saves the definition AND starts it. Use `addin.save()` to only save.
- `python_version`: Python version for the container image (default `"auto"` -- auto-detects from your environment).
- `build_script_pre` / `build_script_post`: shell commands for the Docker image build. If the user needs to install OS packages or system libraries, see [datatailr-build-scripts.md](../datatailr-build-scripts.md) for formatting details.
- `env_vars`: dict of environment variables passed to the running container.
- `run_as` / `acl`: leave unset to use default permissions. Only if the user explicitly requests access restrictions, see [datatailr-acl.md](../datatailr-acl.md). If their request is vague (e.g., "restrict access"), ask who should be able to see, edit, and operate the job before proceeding.

## Deploying

Run the deploy script:

```bash
python deploy.py
```

## Checklist

- [ ] `Addin` instance is named `addin` in the module
- [ ] Functions are decorated with `@addin.expose(description=..., help=...)`
- [ ] All function parameters have type hints
- [ ] Module has a `main(port, ws_port)` function calling `addin.run(port, ws_port)`
- [ ] Deploy script is outside the addin module directory
- [ ] Deploy script imports `main` (not a string path)
- [ ] `python_requirements` includes all dependencies
- [ ] No hyphens in directory names (use underscores)

## Platform environment variables

The platform sets these environment variables in every running add-in container:

- `DATATAILR_JOB_NAME`: name of the running add-in.
- `DATATAILR_USER`: username running the add-in.
- `DATATAILR_JOB_ENVIRONMENT`: `dev`, `pre`, or `prod`.
- `DATATAILR_JOB_TYPE`: always `excel` for Excel add-ins.

## Calling other services

If the add-in functions need to fetch data from a Datatailr service, see [service-communication.md](../service-communication.md) for how to construct the internal URL and authenticate.
