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.
Who can do what
Section titled “Who can do what”| 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.
Sources and formats
Section titled “Sources and formats”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.csvandcontacts.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.
The two flows
Section titled “The two flows”| 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.
Step 1 — Upload
Section titled “Step 1 — Upload”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.
Step 2 — Analysis
Section titled “Step 2 — Analysis”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.
Step 3 — Review
Section titled “Step 3 — Review”New-tables mode: review the schema
Section titled “New-tables mode: review the schema”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.
Existing-table mode: map columns
Section titled “Existing-table mode: map columns”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.
Step 4 — The job
Section titled “Step 4 — The job”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:
queued→running→succeeded,failed, orcancelled. - 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.
Invalid rows: skip or fail
Section titled “Invalid rows: skip or fail”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.
Cancelling — and why there is no undo
Section titled “Cancelling — and why there is no undo”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.
What an import does not trigger
Section titled “What an import does not trigger”- 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.
Limits
Section titled “Limits”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.
Troubleshooting
Section titled “Troubleshooting”“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.