Szymon Kubisa
Work /

Data Onboarding

Project at a glance

What I built
A customer-list import for a CRM, from CSV/XLSX through mapping and validation to a clean export.
Key decision
AI proposes a mapping; a person approves it. Deterministic code handles the data, which stays in the browser by default.
Evidence
A 49-second walkthrough on 184 deliberately messy customer rows, 306 tests and five end-to-end flows.
Current status
Working prototype.

The question

How much of a messy spreadsheet should a model be allowed to touch?

My answer: the column names, nothing else. An AI suggests the mapping, a person approves it, and plain code does the rest.

The real app in demo mode, on 184 deliberately messy customer records. 49 seconds, no sound.
Role
Product, design and engineering
Platform
Web app · Next.js 16, LangGraph
Status
Working prototype
Year
2026 · Independent project

The problem

Customer lists never match the CRM.

They arrive as spreadsheets: E-mail in one, Kontakt in the next, countries spelled three ways, websites without https://, the same company twice, and now and then a cell that Excel would run as a formula.

Every one of these has a right answer, and most of them do not need a model to find it.

In the fileAfterWhat happened
  • Header “Kontakt”Email fieldMapped by the AI, approved by you
  • UK · GBRUnited KingdomApplied automatically
  • DeutschlandGermanyApplied automatically
  • N/A(empty)Applied automatically
  • acme.comhttps://acme.comSuggested, you accept
  • sales(at)northwind-packaging.co.uknot a valid email addressFlagged for you
  • GB123456789, twicepossible duplicate, never mergedFlagged for you

The flow

Four steps, and it works on a phone.

  1. Data Onboarding on a phone, Import step.
    01 · Import

    CSV or XLSX, parsed in the browser. Duplicate and empty headers are fixed and reported. Nothing is uploaded.

  2. Data Onboarding on a phone, Map step.
    02 · Map

    Each column gets a suggested field, a confidence bucket and a one-line reason. Accept, change or ignore.

  3. Data Onboarding on a phone, Validate step.
    03 · Validate

    Safe changes are applied and marked; meaning-changing ones are one-click fixes. Edit cells, exclude rows, review duplicates.

  4. Data Onboarding on a phone, Export step.
    04 · Export

    CSV in schema order, formula-injection safe, plus a JSON audit report of every decision and change.

AI suggests, a person approves, deterministic code executes. The model has exactly one job: propose how columns map to fields. Everything after the approval is plain, tested TypeScript.

What the model sees

A column profile, never the rows.

The file is parsed in the browser and stays there. The server gets each header plus statistics, such as how many values look like email addresses or countries. Up to three sample values per column are sent only if you switch sharing on.

The privacy claim is simple to check: open the network tab, and there is no file in it.

The mapping step: each source column with a suggested target field, a High confidence badge and a decision, the E-mail row expanded to show why it was suggested and how values will look, and the share-samples toggle switched off.
  1. 1Samples are opt-in: at most 3 values per column, 60 characters each.
  2. 2Confidence is shown as High, Medium or Low, labelled as a model estimate.
  3. 3Every suggestion comes with a one-line reason and a preview of the values.

Map · in demo mode the suggestions come from heuristics, and the screen says so.

Architecture

A real graph: retries, a fallback and a person in the loop.

Model output is parsed with Zod and repaired by domain rules. Garbage gets one retry with the validation errors; after that, or if the provider fails, heuristics stand in and the interface says so. The graph then stops and waits for a person, and an approved mapping that is still missing a required field goes back for another review.

Decision · no database

Serverless, and honest about it.

It runs on Vercel without a database. Graph threads live in a bounded in-memory saver, and when a review lands on a cold instance the route rebuilds the thread from what the browser echoes back, then re-validates it with the same rules. Production would use a shared Postgres checkpointer, with persistence and recovery verified across instances.

306

Tests

Unit, integration and component, from header parsing to the LangGraph retry paths.

5

End-to-end flows

Playwright on a production build, with axe accessibility checks on every step.

0

Bytes of the file uploaded

Parsing, validation and export all run in the browser.

Data quality rules

Every change has a from and a to.

The source file is never modified. Corrections and fixes are overlays you can revert, missing data is never invented, and the schema’s Zod contract has the final word on each record.

FieldApplied and shownSuggested, you acceptFlagged
All textTrim, collapse whitespace, N/A · null · - to empty––
Company name––Missing is an error
EmailLowercase domain, strip mailto:–Invalid is an error
Website–acme.com → https://acme.comNot http(s) or no domain is an error
CountryUK, GBR, Deutschland, U.S.A. to the canonical name–Unrecognised is a warning; NA is ambiguous
Tax ID–Remove spaces and punctuation, uppercase–
Phone––Letters or fewer than 6 digits is a warning
Duplicates––Same tax ID, email, or name and country. Never merged.
The validate step: 184 rows, 159 valid, 21 warnings, 4 errors, 12 duplicates, and a cell editor open on row 6 explaining that sales(at)northwind-packaging.co.uk is not a valid email address.
Validate · click a flagged cell to see why, apply a fix, or edit it in place.

Decision · formula-safe export

A CSV that spreadsheets can’t run.

A cell starting with =, @, a tab or a carriage return gets a leading quote. Cells starting with + or - are escaped only when they hold more than a number, so phone numbers and negatives survive.

  • -12.5stays
  • +44 20 7946 0000stays
  • +Plus Supplies Ltd'+Plus Supplies Ltd
  • -cmd|' /C calc'!A0'-cmd|' /C calc'!A0

CWE-1236 · next to the CSV comes a JSON report of every decision, change, open issue and excluded row.

The export step: 180 rows to export, 21 exported with warnings, 4 left out for errors, 0 excluded, with options for including rows with errors and the delimiter.
Export · nothing is left out silently: every left-out row is listed with its reason.

Built with

  • Next.js 16
  • React 19
  • TypeScript 6
  • LangGraph.js
  • OpenRouter
  • Zod 4
  • TanStack Table
  • Zustand
  • Tailwind CSS 4
  • shadcn/ui
  • Papa Parse
  • read-excel-file
  • Vitest
  • Playwright
  • axe-core

Without an API key it runs in demo mode on deterministic heuristics, which also makes it reviewable in CI.

More work

01 · Threadline

Can a CRM build itself from your inbox?

Threadline’s inbox on synthetic data: threads tagged Needs reply and Waiting on them, and an AI summary above the conversation.
Working MVP
Explore

02 · WildRoom Studio

What should a media studio look like in the AI era?

WildRoom Studio’s projects screen: Neon Miami, Impossible Worlds and other film projects, each one folder.
Working prototype
Explore
Szymon Kubisa © 2026 Szymon Kubisa · AI Product Engineer Built with Nuxt, Vue, Tailwind and AI tooling · Privacy