Jasmine Frantz · 2026.8.6 · 6 min read
Some failures should fix themselves. Others are the system telling us it doesn't know enough to safely make the decision.
After writing about how we're rebuilding our CRM importer, someone asked a good question. Once an import partially fails, there's still work left for the user. They have to understand what went wrong, fix the affected data, and retry. So what happens next, and how much of it can we automate?
Quite a bit, actually. But not all failures are the same, and treating them as if they are creates a different problem: a system that gets very good at quietly making decisions about your data.
The distinction we're working around is simple. If the system knows what went wrong and can safely recover, it should. If it doesn't know what the data means, it shouldn't pretend that it does. That's where data integrity becomes the boundary.
"Data integrity" can sound like a compliance term. For an importer, we mean something more practical: after an import touches a record, that record should still reflect reality, and nothing about it should have changed unless the incoming data or a person said it should.
Nothing was invented. A phone number that wasn't in the file didn't appear out of nowhere. Nothing was lost silently. A title that was already correct didn't get blanked or overwritten behind the user's back. Nothing was merged silently. Two different people who happen to share a name didn't collapse into one. Replacing a value isn't inherently wrong when someone chooses to do it. Losing or changing it by accident is.
An import can make thousands of these small decisions in a single file. The challenge isn't automating more of them. It's knowing which decisions are safe to automate.
Suppose an import fails because a worker times out. That's not a judgment call. Retry it. If an AI service is temporarily unavailable but deterministic mapping can handle the file, fall back to it. If processing fails halfway through for a recoverable reason, resume rather than asking the user to upload everything again. Those are system failures, and there's no reason to turn them into user work.
The same applies to many data issues. If 184 rows use the same recognizable date format, normalize them. If a phone number contains formatting characters we know how to standardize, standardize it. If "Work e-mail" clearly maps to an email field, don't ask 184 times. The system already has enough information to make those decisions safely.
The interesting failures are the ones where it doesn't.
Take an email address like jane@acme.con. It looks like a typo, and changing it to jane@acme.com would probably be right. But "probably" is doing a lot of work there. The domain could be legitimate. The source file could be wrong. The existing CRM value could be wrong. Or the user might know something the importer doesn't.
The same problem appears with duplicates. Two records named John Smith aren't necessarily the same person. Two similarly named companies aren't necessarily one account. And two deals involving the same company aren't necessarily duplicate opportunities.
At that point, retrying doesn't help, and neither does throwing more automation at the problem. The system isn't failing to process the data. It lacks information. That's a fundamentally different kind of failure.
That distinction gives us a practical rule. If a decision is mechanical and reversible (parsing, normalization, exact-key matching, retries, fallbacks, known mappings, recoverable infrastructure failures), automate it. If it could invent, destroy, overwrite, or merge something true, require enough confidence or ask.
The rule we keep drawing looks roughly like this:
type Failure =
| { kind: "system"; recoverable: true } // worker timeout, transient AI outage
| { kind: "deterministic"; fix: SafeFix } // known date format, standardizable phone
| { kind: "ambiguous"; question: Question } // typo? duplicate? needs a human
function next(failure: Failure) {
switch (failure.kind) {
case "system": return retry(failure)
case "deterministic": return applyFix(failure.fix)
case "ambiguous": return ask(failure.question) // never guess
}
}That principle shows up throughout the importer. Blank incoming cells don't silently erase populated fields. Existing values aren't overwritten by default. People match on email and companies on domain, but deals don't automatically merge. If an incoming record matches an actual member of the organization rather than a contact, their profile gets stronger protection from spreadsheet updates. And when confidence in a mapping or match drops below the level where we're comfortable applying it automatically, we surface the decision rather than quietly guessing.
The simplest of those rules is, in effect, a single field merge:
// Fill gaps, never clobber.
function resolveField<T>(existing: T | null, incoming: T | null): T | null {
if (incoming == null || incoming === "") return existing // blank never clears
if (existing != null) return existing // populated never overwritten (default)
return incoming // fill the gap
}None of those rules are sophisticated. They're guardrails against turning uncertainty into bad data.
This is where the original question gets interesting. Today, the familiar pattern after a partial import is something like: import completed, 812 succeeded, 188 failed. Download the error file, open it, read through 188 rows, figure out which problems are actually the same problem, fix them, upload again. That's technically human-in-the-loop. It's also terrible UX.
Keeping a person involved in ambiguous decisions doesn't mean they should have to do all the work surrounding those decisions. If those 188 failures really represent three problems, the system should be able to tell you that:
184 rows use a date format we recognize. We can normalize these automatically.
3 rows may match existing contacts. Review the suggested matches.
1 value can't be mapped confidently. Tell us where it belongs.
Now the user isn't reviewing 188 failures. They're making four decisions, and maybe only one actually requires thought. That's the direction that interests us far more than generating a better error CSV.
This is also where our experience with AI evaluation shows up. Automation can do an enormous amount before a person needs to get involved: classify the failure, group similar issues, decide whether the problem is recoverable, suggest a resolution, apply safe fixes in bulk, retry when the failure is mechanical, and explain what's left in plain language. What it shouldn't do is turn uncertainty into certainty just because removing the last human interaction makes the workflow look more automated. The goal isn't zero human involvement. It's zero unnecessary human involvement. That's a very different target.
There's one more requirement if the system is going to make more decisions on its own: we need to know what it did. Today, every record written during an import is stamped with the import it came from, so we can answer the basic version of that question. Which import created or last touched this record? The harder version is the one that matters more as automation grows. Where did a given value come from? Was it in the original file, normalized on the way in, inferred, or approved by the user? We can't answer that at the field level yet. Records are last-writer-wins, with no per-field history. That trail is exactly what makes more automation safe to add, so building it is the direction we're working toward, not a box we've already checked.
Ideally, much less than today. System failures recover without bothering the user, deterministic data problems get fixed automatically, repeated failures are grouped rather than dumped into a spreadsheet, and genuine ambiguity is reduced to the smallest possible number of questions.
But there will still be a line. If resolving a failure requires deciding what someone's data means, and getting that decision wrong could silently change something true, we would rather ask than guess. The manual step the reader noticed isn't a gap we forgot to close. The gap is all the unnecessary work surrounding that decision. That's what we should automate away.
Try Introzy: Import your contacts, companies, and deals without the system guessing at your data. Get started free →
Introzy is free to start. No card required.