MainSDK
Search⌘K
How-to

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.

Building for 0.4

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
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.

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.

1. Declare the import limits

main.yaml
imports:
  inline-max: 5MB
  max-rows: 1000000

2. Preview the file

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.

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet