---
title: "Import a customer's spreadsheet"
description: "Preview a CSV or Excel file, confirm the column mapping, run a dry run, then write the rows, with a report of every row that failed."
section: "guides"
status: "Building for 0.4"
spec: "imports"
canonical: "https://mainsdk.dev/docs/guides/import-data"
updated: "2026-10-28"
---

# Import a customer's spreadsheet

> Preview a CSV or Excel file, confirm the column mapping, run a dry run, then write the rows, with a report of every row that failed.

Status: Building for 0.4 · Canonical: https://mainsdk.dev/docs/guides/import-data · Updated: 2026-10-28

A customer uploads a spreadsheet of contacts. You preview the first rows, confirm which column is which, check a dry run, and write the rows. Nothing is written until the mapping is confirmed. It takes about 25 minutes.

> **Note:** `imports` is in 0.4 (spec `imports`) and runs from the repository; it is not published to npm. Google Sheets and Airtable sources need a `connections` token, which is not built yet; files and `rest` sources work now.

## You need

- A MainSDK app with a `db` model for the rows you import (for example `contacts`).
- A schema for the row, with a unique key such as `email`.
- An upload, from [`files`](https://mainsdk.dev/docs/interfaces/files).

## 1. Declare the import limits

```yaml title="main.yaml"
imports:
  inline-max: 5MB
  max-rows: 1000000
```

## 2. Preview the file

```ts title="app/admin/import/actions.ts"
"use server";
import { imports } from "mainsdk";
import type { FileRef } from "mainsdk/files";
import { s } from "mainsdk/schema";

const Contact = s.object({ email: s.string(), name: s.string() });

export async function previewContacts(file: FileRef) {
  return imports.preview(file, { schema: Contact, key: "email" });
}
```

The preview returns the columns, the first 20 rows, a suggested mapping and `missing`, the required fields no column maps to. Show these to the person.

## 3. Dry run with the confirmed mapping

```ts
import { imports } from "mainsdk";
import type { FileRef } from "mainsdk/files";
import { s } from "mainsdk/schema";

const Contact = s.object({ email: s.string(), name: s.string() });
declare const file: FileRef;   // the upload

const run = await imports.file(file, {
  schema: Contact, key: "email", into: "contacts",
  mapping: { "E-mail": "email", "Full name": "name", "Company": null },   // null ignores a column
  dryRun: true,
});
console.log(run.counts, run.report);
```

`counts` shows what would be inserted, updated, unchanged or failed. `report` is a CSV of the failed rows.

## 4. Write the rows

Run the same call without `dryRun`. The `mapping` you pass is the confirmation. Set `maxErrors` if a few bad rows should not stop the run, for example `maxErrors: "2%"`.

## Check it worked

- `run.state` is `completed`, and `counts.inserted + counts.updated + counts.unchanged` equals the rows you expect.
- Running step 4 a second time writes nothing new. The default idempotency key is the file, target, key and mapping together.
- Open `run.report` and find the rows that failed.

## If it fails

- `MAIN-IMPORT-0003`: a required field has no column. Map it, or fix the header in the file.
- `MAIN-IMPORT-0005`: failed rows passed `maxErrors`. Fix the rows in the report and run again.
- `MAIN-IMPORT-0001`: the file is not CSV, XLSX, JSON or NDJSON in UTF-8. Save it again.

## Prompt for your coding agent

> Add a spreadsheet import for contacts using MainSDK's `imports` interface. Preview the upload, show the suggested mapping, require the person to confirm it, run a dry run, then write the rows with a report of failed rows.
