Back to all postsA brown paper cutout card index box holding two cream cards behind yellow and purple tabs lettered A and B, on cream paper

How to Remember CSV Import Mappings Between Uploads

A partner keeps sending the same employee export. Their header says Work where your field says Company, and their status column says LOA where your app says On leave. Someone on the customer side opens the importer, picks the right field from two dropdowns, and finishes. Later the same file arrives and the same two dropdowns are waiting for them.

A pair someone fixed once can come back the next time and land before anyone looks at the screen. Somebody has to keep those pairs from one import to the next.

What the matching engine does on its own

Updog scores every file header against every field in your schema. Both strings are normalized first, lowercased with spaces, underscores, hyphens and dots removed, so First Name, first_name and firstname are one string and score 100.

A pair listed in the synonym table scores 90. One string containing the other scores 80, provided the shorter one runs at least four characters. Half the words shared scores 70. A typo scores 65, where a typo means one edit for strings up to four characters and up to four edits past fifteen. Below 60 the header stays unmatched.

Columns match in two passes. Exact matches go first and reserve their field, then Updog sorts the remaining headers by their best score and lets each take what is left. A weaker guess never takes a field a confident header already claimed. Values run the same scoring against the options you allow in a select field.

Updog ships with a built-in table of common words. fname and given name reach First name, dept and division reach Department, hire date and joining date reach Start date. It carries value aliases too, M and F, Jr and Sr, FT and PT, yes and no.

A group scores 90 only when both strings fall into it. fname against Department stays at zero, and so does dept against First name.

That table stops where your industry invented a word. Work against Company scores zero on every tier. Neither string contains the other, they share no whole word, and their lengths differ by more than the typo allowance for strings that size. Somebody has to say what it means, once.

Where the memory can live

Nobody remembers. The importer scores each file from scratch and the person answers the same dropdown every time. Nothing to store, nothing to secure, and the work repeats for as long as that partner keeps sending files. Watch how many of your imports are the same file shape coming back.

The vendor remembers. The importer keeps confirmed pairs on its own servers and applies them next time. Your users get memory and you write no code for it. Those pairs are headers out of their spreadsheets and words out of their cells, and from that day they live with a third party. Check that vendor the way you check any processor, with a data processing agreement, a named storage region, and a retention period you can point at.

You remember. The importer hands those pairs back and accepts them as input on the next run. They go into your own database, beside the customer data you already hold, under the policy you already wrote. No new processor enters the picture, and the scope and the deletion rules stay yours. Decide where in your schema the table belongs before the first import ships.

Updog hands the pairs back and keeps none of them. As of August 2026 we run no server in the path of a file and hold no copy of anything a person imports, so a table on our side would contradict the whole design.

What Updog hands back after an import

Every onComplete result carries learnedSynonyms, split into columns and values. An entry is a source and a target. Source is the word the file used. Target is your column title, or the option value the person picked inside a select field.

{
columns: [
{ source: "Work", target: "Company" }
],
values: [
{ source: "LOA", target: "On leave" },
{ source: "New hire", target: "Onboarding" }
]
}

Updog keeps its own guess beside the finished mapping and hands back the difference. A pair the matcher filled in and the person left alone stays out, because next time the matcher fills it in again. A pair your onColumnMatch or onValueMatch returned stays out too, since that answer already lives in your code, in a rule you wrote or in your own model.

Updog also drops a pair the built-in table or your own synonyms prop already covers, and a pair whose two strings normalize to the same thing, so E-mail against Email never becomes a row. What comes back is the pair someone picked by hand.

Updog takes the difference when the rows land in the grid, and the list reaches you on submit. An import someone cancels halfway teaches nothing.

The two lists go back in as two tables. A word learned inside a dropdown scores only against dropdown options, and a header alias scores only against your columns.

The round trip in a React app

Take an employee schema with a Company field and a Status field with four options. The React import guide covers installing the package and wiring the first import.

const columns = [
{ id: "firstName", title: "First name" },
{ id: "lastName", title: "Last name" },
{ id: "email", title: "Email" },
{ id: "company", title: "Company" },
{
id: "status",
title: "Status",
editor: {
type: "select",
options: ["Active", "Onboarding", "On leave", "Terminated"],
},
},
];

A file lands with a Work header and the statuses Active, LOA and New hire. Active matches on its own. The other three score zero and wait for a person, who picks Company, On leave and Onboarding. Those three answers are what comes back in learnedSynonyms.

Post the object as it arrives and let your endpoint take it apart. It stores each pair with the table it came from, and it answers with the two tables keyed by target, which is the shape the synonyms prop takes.

import { useCallback, useEffect, useState } from "react";
import { DataEditor } from "@updog/data-editor";
export function ImportEmployees() {
const [open, setOpen] = useState(false);
const [synonyms, setSynonyms] = useState({ columns: {}, values: {} });
useEffect(() => {
fetch("/api/import-synonyms")
.then((response) => response.json())
.then(setSynonyms);
}, []);
const openImporter = useCallback(() => {
setOpen(true);
}, []);
const closeImporter = useCallback(() => {
setOpen(false);
}, []);
const onComplete = useCallback(async (result) => {
const response = await fetch("/api/import-synonyms", {
method: "POST",
body: JSON.stringify(result.learnedSynonyms),
});
setSynonyms(await response.json());
setOpen(false);
}, []);
return (
<>
<button onClick={openImporter}>Import employees</button>
<DataEditor
apiKey="your-license-key"
variant="uploader"
open={open}
onClose={closeImporter}
columns={columns}
primaryKey="email"
synonyms={synonyms}
onComplete={onComplete}
/>
</>
);
}

The next file of that shape reaches the Match columns step with Work already on Company, and the Match values step with both statuses already picked, all three at the synonym tier. Updog reads synonyms when the importer mounts, so the new tables have to be in place before the next open. Closing the modal and opening it again is enough.

What lands in your database

Those rows are user data. A source is a header someone typed into their spreadsheet, and on the value side it is a word out of their cells. Updog never sees the table, which leaves every decision about it with you.

Pick a scope. Per person keeps one user's answers to that one user. Per organization lets a team share what any of them fixed. Per partner feed fits a recurring file from the same sender.

Deduplicate on table, source and target as you write. A pair stops coming back once it reaches Updog through synonyms, and until then a second import of the same file teaches it again.

Decide what happens when someone maps one source to two different targets over time. Both pairs stay live, both score 90, and the field that wins is the one that comes first in your schema. Keeping the most recent answer per source is the simplest rule that stays predictable.

Value aliases carry no field of their own. The values table is keyed by the option value, so an alias learned inside one select field applies to every field offering the same option. Look at your schema for two fields that share option vocabulary and decide whether that suits you.

When you do not need this

A one-off migration has nothing to remember. A schema whose field names match the words the world already uses gets there on the built-in table alone, and adding storage buys nothing. A file that arrives once, from one person, is not worth a table either.

Conclusion

The partner will send Work again, and the matcher will put it on Company without asking. The engine handles the headers the world already shares, and a person answers the rest once. Those answers come back in learnedSynonyms on submit, go into your own table, and return through synonyms on the next open. The words your customers use stay yours, beside the customer data you already hold.