Skip to content

Importing data

Getting your existing data into Tessule is a guided pipeline: upload CSV files, let the analyzer propose a schema or a column mapping, adjust it, and run the import as a background job you can watch and cancel. One wizard covers both starting from scratch (files become new tables, with cross-table relationships detected automatically) and topping up an existing table.

If you only remember one thing:

Analysis proposes; the job disposes. Everything the wizard shows before you press Import — inferred types, suggested names, detected relationships — is a suggestion computed from a sample, and nothing is created until the job runs. Conversely, once the job is running it writes as it goes: rows already imported stay even if you cancel. When in doubt, use a dry run first — full validation, zero writes.

1 · Upload CSV file(s) 2 · Analyze types · links · issues 3 · Review schema or mapping 4 · Import job async · cancellable queued  →  applying schema  →  inserting rows  →  linking references  →  finalizing  →  succeeded / failed / cancelled dry run: same pipeline, full validation report, zero writes
The import pipeline. Steps 1–3 create nothing; step 4 is a background job whose progress, result and row errors you can watch.

Action Governs Held by default
records:create Uploading files, running analysis, importing into an existing table, cancelling a job Member, Admin
records:update Additionally required for update-mode deduplication (rows that overwrite existing records) Member, Admin
schema:apply Table-creating imports (POST /imports/jobs) — these apply a schema plan Admin
tables:read Listing jobs and reading job status/results Reader, Member, Admin

A job can be cancelled only by its creator or a workspace admin. The wizard itself lives in the Build area (Table Administration → Import CSV, or Import CSV on a table’s data page), which is visible only with the builder:access action — held by admins by default. A member without builder access can still import into an existing table through the API (POST /tables/{tableId}/import needs only the actions above); the table-creating flow is admin territory on both surfaces.


The import pipeline reads CSV files — one table per file. The delimiter is detected automatically, and headers become column names. There are currently no direct connectors to other CRMs: to migrate from one, export its objects as CSVs and upload them together.

The analyzer is deliberately good at CRM-shaped exports:

  • Uploading related files together (say accounts.csv and contacts.csv) detects cross-file relationships by checking whether one file’s column values are contained in another’s key column, and proposes proper reference fields instead of loose text IDs.
  • Salesforce ID columns are recognised, including the 15- and 18-character forms of the same ID — a child file using 18-character IDs links correctly to a parent exported with 15-character ones.

Full wizard (new tables) Per-table import
Start from Table Administration → Import CSV Import CSV on a table’s data page
Files Up to 25 CSVs at once One CSV
What it does Creates tables (and reference fields) from a schema plan, then loads all files Maps CSV columns onto the table’s existing fields
Deduplication — (tables are new) append / skip / update against existing records
Dry run API only Checkbox in the wizard
Endpoint POST /imports/jobs POST /tables/{tableId}/import
Needs schema:apply (admin) records:create (members may run it)

Both flows share the same upload and analysis steps and the same job lifecycle.


Files upload directly to storage via presigned URLs (POST /imports/uploads; large files upload in parts and complete via POST /imports/uploads/complete — the wizard handles this invisibly). Each upload returns a workspace-scoped file handle that the later calls take; you cannot analyze or import a file uploaded by another workspace.

Uploads are temporary staging, not attachments: imported files are kept for about a week after the job finishes and then deleted.


POST /imports/analyze reads a bounded sample of each file (up to a few MB / several thousand rows — the response flags when a file was sampled and estimates its total row count) and returns, per column:

  • a proposed field: name, display name, type, and required/unique suggestions, with confidence-ranked alternative type candidates;
  • consistency warnings — e.g. a column that is mostly numbers with a few stray strings, or malformed rows in the file;
  • skip suggestions for columns that don’t look worth importing;
  • for picklist-shaped columns, the detected option list.

With multiple files it also returns the detected relationships (with a match percentage and any alternative parent candidates) and a ready-to-edit schema-plan proposal. Analysis creates nothing — it exists to prime the review step.


Each file gets a card: rename the table, rename fields, change types, toggle required/unique, and untick columns to leave them out. Detected relationships are listed underneath — untick one to import the raw column instead of a reference, or repoint it at an alternative parent key. A live diagram previews the tables and references the import will create.

The schema plan is validated when you submit: structural problems come back immediately as teaching issues (nothing is enqueued), the same validation used elsewhere for schema plans. Field and table names are sanitised automatically as you type.

Each CSV column is mapped to one of the table’s fields, or skipped (unmapped columns are always skipped). Columns are auto-matched by name where possible. Values are coerced and validated against the field’s rules exactly as a manual edit would be.

Duplicates are handled by choice of three modes against a match field (one of the mapped, scalar-typed fields — JSON-shaped fields like address or multi-picklist cannot be matched on; a unique field is the best choice and the default):

Mode A CSV row matching an existing record…
Add anyway (append, default) …is inserted as a new record. No matching happens; a collision on a unique field is still a row error.
Skip …is counted and not written. Only new records import.
Update …overwrites the mapped fields of the matched record; unmapped fields keep their values. Non-matching rows are inserted.

Update mode has sharp edges, all reported per row rather than guessed at: a row matching more than one record, matching a record you may not update, duplicating an earlier row’s match value, or with an empty match value is a row error. Update mode also requires the records:update action and honours your row and field permissions — an import can never update a record or field you couldn’t edit by hand.


Starting the import enqueues an asynchronous job and returns immediately. The wizard polls it for you; via the API, GET /imports/jobs/{jobId} returns status, progress and result, and GET /imports/jobs lists the workspace’s jobs newest-first (tables:read).

  • Status: queuedrunningsucceeded, failed, or cancelled.
  • Progress: a phase (pending, applying_schema, inserting, linking, finalizing), rows processed, the file being worked on, and a total (estimated until the file has been read through once).
  • Result: rows inserted and updated per table, duplicates skipped, total error count, warnings, and the first batch of row errors — each with its 1-based row number, file, and messages. The wizard can download the errors as a CSV; fix those rows and re-import just them.

Skip invalid rows and report them (the default, skipInvalid) imports everything valid and reports the rest per row. Untick it and the first invalid row fails the whole job with that row’s error. The API also offers an atomic option — all-or-nothing in one transaction — accepted only for jobs small enough to finish in one slice; for anything larger, dry run first instead.

POST /imports/jobs/{jobId}/cancel (or the wizard’s Cancel button): a queued job cancels immediately; a running job cancels cooperatively at the next batch boundary. Tables and rows already created stay — the result reports exactly what landed. There is no rollback of a completed or cancelled import; the safeguards are ordered accordingly: dry run → skip-invalid run → (small jobs only) atomic run. If an import does land wrongly, the imported rows are ordinary records — filter and delete them, or re-run in update mode to correct them.

A job interrupted by a platform fault (rather than cancelled) is retried automatically from its last checkpoint a bounded number of times before being marked failed.


  • Per-row record events do not fire. An import announces itself as a single table-level change per touched table, not thousands of individual record events — deliberate, so a 100,000-row import cannot fan out into 100,000 downstream reactions. If something must happen per record, run it after the import over the new rows.
  • Approval rules do not hold import rows. Approvals intercept the interactive record-write path; import rows are written by the job directly. Combined with the permission checks above, the import needs to be trusted the way its runner is trusted — which is why table-creating imports are admin-only.
  • Open Sheets and grids on a touched table are told to refresh when the job finishes, rather than receiving each row live.

What is enforced per row, identically to a manual edit: field validation, required/unique constraints, picklist option rules (you cannot import a row into a retired option value), and stage transition rules — in update mode, an unchanged stage is not a transition, so re-importing an export of a table with guarded stages does not fail every row.


Fixed platform guardrails:

  • File size: 1 GB per CSV (checked at upload and analysis).
  • Files per import: 25.
  • Row size: ~1 MB per CSV record.
  • Rows per job: 1,000,000 — larger datasets should be split and imported in parts.

From the organization limits framework (defaults, adjustable per plan or per-organization):

Limit Default
Concurrent queued/running import jobs per workspace 5
Tables per workspace (checked before a table-creating import) 200
Rows per table (checked before an import that would exceed it) 5,000,000

Finished jobs and their reports are retained for about 30 days; uploaded files for about 7.


“Import exceeds the row limit” — the job refuses to start (or stops) when a file exceeds the per-job row cap. Split the file and import in parts; in existing-table mode, later parts can use skip/update deduplication so overlaps between parts are harmless.

Half my rows imported and the rest are “row errors”. That’s skipInvalid doing its job. Download the error CSV, fix those rows in your source file, and re-import just the fixed rows — with duplicate handling set to skip or update so the rows that already landed aren’t doubled.

A relationship column imported as plain text. Either the relationship wasn’t detected (check the analysis step’s relationship list next time) or it was unticked. You can add a reference field to the table afterwards and re-import in update mode to populate it.

The update-mode preview shows fewer updates than I expected. Duplicate matching compares the match field’s text form against the records the job’s creator can see — the import mirrors the same row scoping every record endpoint applies. A row whose only match is a record outside your visibility behaves as unmatched (it inserts — or becomes a row error if that collides with a unique field). An import can never silently update a record you couldn’t see or edit by hand.

Open the app