> ## Documentation Index
> Fetch the complete documentation index at: https://developers.zite.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Import spreadsheets

> Read and analyze .xlsx/.csv/.tsv files with pandas, then load the rows into your database with bulkCreate.

Two steps: **inspect and clean** the file with pandas in the build sandbox (`pandas` + `openpyxl` pre-installed), then **insert the rows** with the [`zite` client](/framework/database-client). Attach the `.xlsx` / `.csv` / `.tsv` file so the agent can read it while building.

## 1. Inspect the structure first

Check sheet names, shapes, and headers before mapping — data often lives on a secondary sheet:

```python theme={null}
import pandas as pd

xls = pd.ExcelFile('customers.xlsx')
print("Sheets:", xls.sheet_names)

for name in xls.sheet_names:
    df = pd.read_excel(xls, sheet_name=name)
    print(f"\n--- {name} ({df.shape[0]} rows x {df.shape[1]} cols) ---")
    print("Columns:", list(df.columns))
    print(df.head(10).to_string())
```

Reading variants — first sheet, all sheets, a named sheet, or delimited text:

```python theme={null}
df = pd.read_excel('file.xlsx')                          # first sheet
all_sheets = pd.read_excel('file.xlsx', sheet_name=None) # dict of DataFrames
sales = pd.read_excel('file.xlsx', sheet_name='Sales')   # one sheet
csv = pd.read_csv('file.csv')
tsv = pd.read_csv('file.tsv', sep='\t')
```

Uploaded files download to `$ZITE_UPLOAD_DIR` — `cd "$ZITE_UPLOAD_DIR"` first, then bare filenames work.

## 2. Clean, then load into a table

Normalize with pandas, map each row to your table's **SDK field names**, and insert with `bulkCreate`. Pass `matchOn` so re-imports upsert on a stable key instead of duplicating:

```typescript theme={null}
// apps/<app>/src/api/importCustomers.ts
import { z } from 'zod';
import { createEndpoint } from 'zitejs/backend';
import { zite } from 'zitejs/db';

export default createEndpoint({
  description: 'Import customer rows from a parsed spreadsheet',
  inputSchema: z.object({
    rows: z.array(z.object({ email: z.string().email(), name: z.string(), plan: z.string() })),
  }),
  outputSchema: z.object({ imported: z.number() }),
  execute: async ({ input }) => {
    const result = await zite.Customers.bulkCreate({
      records: input.rows.map(r => ({ email: r.email, fullName: r.name, plan: r.plan })),
      matchOn: ['email'], // upsert on email so re-imports don't duplicate
    });
    return { imported: result.records.length };
  },
});
```

## Gotchas

* **Check every sheet** — key data is often on a secondary tab.
* **Pin dtypes** to avoid inference surprises: `pd.read_excel('f.xlsx', dtype={'id': str})`. Use `header=None` when there's no header row, `usecols=[...]` on wide files.
* **Handle missing values** — `df.isna().sum()` finds them; drop or default before loading.
* **Keep script output under \~20 KB** — summarize with `.head()` / `.describe()` rather than dumping frames.
* **Map to SDK names.** `bulkCreate` records are keyed by field SDK name from `.zite/db.ts`, not spreadsheet labels. Up to 2,000 rows per call — chunk larger files.
