# Hotdata (https://tenki.cloud/docs/sandbox/hotdata)

> For the complete documentation index, see [llms.txt](https://tenki.cloud/llms.txt)

Start from Tenki's pre-built Hotdata image and give an agent its own database to load files into and query with SQL.

Tenki's public `tenki/hotdata:stable` image starts from the standard [`sandbox` base image](https://tenki.cloud/docs/sandbox/base-image.md) and adds the [Hotdata](https://www.hotdata.dev/) CLI (the `hotdata` command), the Hotdata Python SDK with Arrow support (`import hotdata` from the default `python3`), Hotdata's agent skill for coding agents, and the `hotdata-smoke` command for a one-shot connectivity check. It pins the CLI at 0.42.1 and the SDK at 0.12.0. No credential is baked into the image: you pass a Hotdata API key per session.

## Hotdata: a database per task

An instant database is a query scope you create in a single call. You load CSV, JSON, or Parquet files into it, query it with SQL, and delete it, or let it expire on its own. The query engine runs at Hotdata, not in the sandbox, so the VM only runs your agent and the client.

That shape fits agent work: each task can get its own database, sized to the data it needs, with an expiry that cleans up after a run that never finished. A database can also be forked or have another database's catalog attached read-only. See [Hotdata's core concepts](https://www.hotdata.dev/docs/core-concepts) for the full model.

## Start a session

Create an API key in the Hotdata dashboard, export it locally, and pass it only to the session. If the key can reach more than one Hotdata workspace, pass `HOTDATA_WORKSPACE` too; `hotdata workspaces list` shows the IDs. The Python SDK doesn't look the workspace up on its own, so pass it whenever you'll use the SDK; without it, an SDK call raises `MissingWorkspaceError` before sending anything:

```bash
export HOTDATA_API_KEY=<your-hotdata-api-key>
export HOTDATA_WORKSPACE=<your-workspace-id>

tenki sandbox create \
  --image tenki/hotdata:stable \
  --env "HOTDATA_API_KEY=$HOTDATA_API_KEY" \
  --env "HOTDATA_WORKSPACE=$HOTDATA_WORKSPACE" \
  --name hotdata-demo
```

Pass the key only through `--env` when you create the session. Never put it in a template, image, or source file, where it would reach every session built from them. Snapshots taken from this session also contain the key. Hotdata keys are read-only or read-write and can be limited to specific workspaces, so give a session the narrowest key that does its job.

Run the built-in connectivity check:

```bash
tenki sandbox exec --session hotdata-demo -- hotdata-smoke
```

`hotdata-smoke` creates a database that expires in an hour, loads a three-row CSV into it with the CLI, reads it back through the SDK (as JSON and as Arrow) and the CLI, and deletes it. A successful result looks like:

```json
{ "database_id": "dbid...", "rows": 3, "total": 60, "names": ["alpha", "beta", "gamma"] }
```

`hotdata-smoke` exits `2` when `HOTDATA_API_KEY` is unset and `1` when any step fails, so it also works as a readiness check at the start of a script.

Terminate the Tenki session when you finish:

```bash
tenki sandbox terminate hotdata-demo
```

## Use the CLI

Every database gets a `default` catalog, and the CLI puts tables in the `public` schema, so tables are addressed as `default.public.<table>`. Create a database that expires in two hours, load a file into it, and query it:

```bash
db=$(hotdata databases create --name agent-task --table orders --expires-at 2h -o json | jq -r .id)

hotdata databases tables load orders --database "$db" --file orders.csv
hotdata query "SELECT customer, sum(amount) AS total FROM default.public.orders GROUP BY customer" -d "$db"

hotdata databases remove "$db"
```

For a database that should survive retries of the same task, `hotdata databases create --name agent-task --if-not-exists` returns the existing database with that name instead of creating a second one. `hotdata databases use "$db"` makes it the current database, so later commands can drop `-d`. Loads replace a table by default; pass `--mode append`, or `--mode upsert`, `update`, or `delete` with `--key <column>`. Add `-o json` or `-o csv` to a query for output a script can parse. Run `hotdata --help` in the session or see [Hotdata's CLI reference](https://www.hotdata.dev/docs/cli-reference) for everything else.

## Use the Python SDK

The SDK is installed in the image's default Python environment, with `pyarrow`. `hotdata.Configuration()` reads `HOTDATA_API_KEY` and `HOTDATA_WORKSPACE` from the session environment, the same variables the CLI uses:

```python
import hotdata
from hotdata.arrow import ResultsApi

with hotdata.ApiClient(hotdata.Configuration()) as client:
    databases = hotdata.DatabasesApi(client)
    db = databases.create_database(
        hotdata.CreateDatabaseRequest(
            name="agent-task",
            expires_at="2h",
            schemas=[{"name": "public", "tables": [{"name": "orders", "key": ["id"]}]}],
        )
    )

    databases.load_database_table(
        db.id,
        "public",
        "orders",
        hotdata.LoadManagedTableRequest(mode="replace", data="id,customer,amount\n1,acme,120\n2,globex,80\n"),
    )
    databases.load_database_table(
        db.id,
        "public",
        "orders",
        hotdata.LoadManagedTableRequest(mode="upsert", data="id,customer,amount\n2,globex,95\n3,acme,45\n"),
    )

    result = hotdata.QueryApi(client).query(
        hotdata.QueryRequest(sql="SELECT customer, sum(amount) AS total FROM orders GROUP BY customer ORDER BY customer"),
        x_database_id=db.id,
    )
    print(result.rows)  # [['acme', 165], ['globex', 95]]

    table = ResultsApi(client).get_result_arrow(result.result_id, db.id)  # a pyarrow.Table

    databases.delete_database(db.id)
```

Inline `data` is for CSV up to 2 MiB. For anything larger, upload the file first. `upload_file` picks a single or multipart upload by size and sends the bytes straight to Hotdata's storage:

```python
upload = hotdata.UploadsApi(client).upload_file("events.parquet")
databases.load_database_table(
    db.id, "public", "events", hotdata.LoadManagedTableRequest(mode="replace", upload_id=upload.upload_id, format="parquet")
)
```

`QueryApi.query` returns every row of a result up to 1,000,000 rows or 64 MiB. For bigger results, pass `auto_follow=False` to get a preview and the `result_id`, then stream the full result in batches with `ResultsApi.stream_result_arrow`. For a result you only have the `result_id` of, such as one from an async query, `QueryApi.fetch_ready_result` waits until it is ready and returns its rows.

Data goes in through loads, in any of the five modes: `replace`, `append`, `upsert`, `update`, and `delete`. SQL is for reading it back.

## Parallel agents

Several agents, or several sandboxes, can work on the same database at once. Give each one its own table to load into, and query the combined result with SQL while they keep writing:

```bash
hotdata databases tables load results_agent_1 --database "$db" --file results.csv
```

```sql
SELECT * FROM default.public.results_agent_1
UNION ALL
SELECT * FROM default.public.results_agent_2
```

Loads into the same table run one at a time. The CLI and the SDK wait for the table, up to 60 seconds by default, instead of failing.

To retry a load safely after a dropped connection or a timeout, give it an `idempotency_key`: sending the same key again returns the original result instead of loading the rows twice.

```python
import uuid

request = hotdata.LoadManagedTableRequest(mode="append", data=csv_text, idempotency_key=str(uuid.uuid4()))
databases.load_database_table(db.id, "public", "results_agent_1", request)
```

## Coding agents

The image installs Hotdata's agent skill, matched to the pinned CLI, in `~/.agents/skills` and `~/.claude/skills`. Claude Code and other agents that read those directories pick it up, so an agent in the session can use `hotdata` from a prompt without extra setup. The coding agents on the [base image](https://tenki.cloud/docs/sandbox/base-image.md) reach Hotdata with the same `HOTDATA_API_KEY` you passed to the session.

## Isolation

The sandbox runs your agent, the CLI, and the SDK; your data is stored and queried at Hotdata. In a Tenki sandbox, the guest is the boundary for everything that runs locally, and it is enforced per session:

* **Inside the guest**: commands run as the unprivileged `tenki` user (uid 1000), which has passwordless `sudo`.
* **Outside the guest**: nothing in the session can reach the host kernel or filesystem, other tenants' sessions, or the Tenki control plane.
* **Network**: the session reaches Hotdata's API and storage through the sandbox's normal egress path.
* **Credentials**: the API key exists only as session environment, set at creation. The image contains no credential, and nothing is carried from one session to the next.

The `hotdata` command in the image is a wrapper that sets `HOTDATA_NO_UPDATE_CHECK=1` by default, so the CLI never prompts you to move off the pinned version. To move to a newer CLI, launch a newer build of the image rather than running `hotdata manage upgrade` in the session.

## Pin an exact build

`stable` moves to each newly published build. When a workload has to stay on the exact build it was tested against, resolve the tag once and launch the snapshot reference it returns:

```bash
tenki sandbox registry resolve tenki/hotdata:stable
# image_id    : <image-id>
# snapshot_id : <snapshot-id>

tenki sandbox create --image tenki/hotdata@<snapshot-id>
```

A snapshot reference never moves, so it keeps launching that build after `stable` advances.