Hotdata
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 and adds the Hotdata 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 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:
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-demoPass 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:
tenki sandbox exec --session hotdata-demo -- hotdata-smokehotdata-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:
{ "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:
tenki sandbox terminate hotdata-demoUse 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:
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 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:
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:
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:
hotdata databases tables load results_agent_1 --database "$db" --file results.csvSELECT * FROM default.public.results_agent_1
UNION ALL
SELECT * FROM default.public.results_agent_2Loads 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.
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 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
tenkiuser (uid 1000), which has passwordlesssudo. - 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:
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.