Automation · Revenue operations
How to build lead import and validation
A list comes back from an event as a spreadsheet with 1,400 rows, three of which will break the import and none of which anybody will chase. What follows is the model behind an import that survives that, the prompts that build it, and what changes in production.
Built with Tray Headless
- System Google Drive
- Step Map columns
- Step Validate all rows
- Step Dedupe vs CRM
- System Salesforce
Validation runs across the whole file before a single record is written, so a bad file is rejected instead of half-imported.
The short answer
What is lead import and validation?
Lead import is four parts: validation that runs before anything is written, a quarantine for the rows that fail instead of a silent drop, deduplication against what is already in the CRM, and a preserved link back to the source file. The mistake that costs most is the quarantine. An import that reports 1,397 of 1,400 succeeded and says nothing useful about the three has thrown away exactly the rows somebody needs to fix.
What matters here
- Validate before writing anything. A partial import is far harder to clean up than a rejected one.
- Quarantine bad rows with the reason and the original values. A count of failures is not something anybody can act on.
- Dedupe against the CRM, not just within the file. The duplicate that matters is the one already sitting in Salesforce.
- Keep the source file and the row number on every lead. Six weeks later somebody will ask where a record came from.
- Normalise on the way in: casing, country codes, phone formats. Every downstream rule depends on fields being shaped consistently.
Who this is for
You run marketing operations or revenue operations. Lists arrive from events, webinars, agencies and partners, in whatever shape the sender felt like, and each one becomes somebody's afternoon. What you want is an import that validates, explains its rejections, and does not create duplicates.
How it works in practice
Everything that sits between a spreadsheet landing and leads appearing in the CRM.
- 1
The file lands and the columns are mapped
Against a saved mapping per source, because the same agency sends the same shape every time and nobody should re-map it.
- 2
Every row is validated before anything is written
Required fields, email syntax, country codes and phone shapes. Nothing reaches the CRM until the whole file has been checked.
- 3
Failures are quarantined with a reason
The row, the reason and the original values, in a place somebody can correct and resubmit.
- 4
Valid rows are deduped against the CRM
Not just within the file. The duplicate that costs you is the one already in Salesforce with an owner.
- 5
Fields are normalised on the way in
Casing, country to ISO codes, phone to E.164, so downstream routing and matching have something consistent to work with.
- 6
Every lead keeps its source
File name, row number and import run, so the origin of a record is answerable months later.
What an import is made of
An import is not a file upload. It is four parts, and the second one is what makes it fixable.
Validation before any write
Required fields, syntax, and value ranges checked across the whole file first. A partial import is worse than a rejected one.
A quarantine, not a drop
Failed rows kept with the reason and the original values, correctable and resubmittable without re-running the whole file.
Deduplication against the CRM
Email exact first, then email plus company, then a fuzzy pass. Existing records get updated, not duplicated.
A preserved source
File, row number and run id on every record, so provenance survives long after the spreadsheet is gone.
The Tray Headless prompts
Paste these into Claude Code or Codex with the Tray Headless plugin installed. Each stage runs on its own. The systems named in them are the worked example rather than a requirement, and every prompt says so.
Once per project, run
/tray-workflows:set-workspace
to pick the workspace these build in. Point it at a sandbox first.
- 1
Set up and confirm the target schema
Import failures are usually schema surprises, so start there.
Headless skills
build-workflowUse build-workflow. The systems in play are Salesforce, or whatever we run in those seats. Give me the real Lead fields with their types, which are required, which are picklists and what values those picklists accept. Picklists are where imports fail silently, so I want the accepted values before we map anything.
- 2
Map columns per source, and save the mapping
Because the same sender sends the same shape every time.
Headless skills
build-workflowUse build-workflow. The trigger is a file arriving in a watched folder. Build column mapping that is saved per source, so an agency that sends the same layout monthly is mapped once. Match on header name, tolerate case and whitespace differences, and flag any column in the file that is not in the mapping instead of ignoring it silently. If a saved mapping does not exist for a source, stop and ask me to map it. Do not guess at column meaning.
- 3
Validate the whole file before writing anything
A partial import is much harder to clean up than a rejected one.
Validate every row before a single record is created: Required fields present: email, last name, company Email syntax valid, and reject obvious role addresses if I ask for it Country resolvable to an ISO 3166 code Phone parseable to E.164, or left empty instead of half-formatted Picklist values matching what the CRM actually accepts Run this across the whole file first and report the summary before writing. If more than a threshold I set fails, stop entirely and tell me, because that usually means the mapping is wrong, not the data.
The threshold matters. A file where 40% of rows fail is not a data quality problem, it is a column mapped to the wrong field.
- 4
Quarantine what fails, do not drop it
The difference between a report and something somebody can act on.
Failed rows go to a quarantine, never to a log line. Keep the original row exactly as it arrived, the row number, every reason it failed, and the source file. Make it correctable in place and resubmittable without re-running the whole file. Then report quarantine reasons grouped by frequency. One row with a bad phone number is a typo. Two hundred rows with bad phone numbers is a column in the wrong format, and that is a mapping fix rather than two hundred corrections.
- 5
Dedupe against the CRM, then normalise and write
The duplicate that costs you is the one already in the CRM.
Before writing, dedupe in this order: 1. Within the file itself, on lower(email) 2. Against existing Leads on lower(email) 3. Against existing Contacts on lower(email) 4. A softer pass on email plus company for near matches An existing record is updated instead of duplicated, and never has its owner overwritten by an import. Stamp Source_File__c, Source_Row__c and Import_Run__c on everything written. Normalise as you write: title case names, ISO country codes, E.164 phones. Every downstream routing and matching rule depends on these being shaped consistently.
- 6
Check it end to end, then hand it to the team that runs imports
So a new list format is not an engineering request.
Run the per-step schema checks and the whole-workflow audit before this touches production. Then open the same workflow in Tray Build so marketing operations can add a source mapping or change a validation rule in the visual canvas. New list formats arrive constantly, and each one should not be a ticket.
What it connects to
Import reaches into wherever files arrive and writes into the system of record.
Salesforce
Read existing leads and contacts for deduplication, then write validated records with their source stamped on.
Reads and writes
Google Drive
Watch the folder lists actually land in, and keep the original file alongside the import run.
Reads and writes
Marketo
Push imported leads into the right programme with the campaign attribution the file came with.
Writes
Slack
Report the run to whoever owns the list: written, quarantined, deduped, and the top rejection reasons.
Writes
Clearbit
Fill the firmographics the file did not include, so routing has something to band on.
Reads
Snowflake
Land every import run and quarantine reason, so recurring data quality problems are visible by source.
Writes
Same build, other stacks
The design does not change if you run something else in one of these seats. The same prompts build it against Microsoft Dynamics 365, Google BigQuery, Microsoft Teams, SharePoint, HubSpot or Databricks.
Named systems are the ones most teams run, not the only ones that work. Each is an authentication in your Tray workspace, referenced by name, so the workflow never holds a credential. Where we have a connector page, the name links to it.
Running it in production
Imports create records that everything downstream depends on. A bad run is felt for months.
The platform runs it, not a laptop under a desk
File watching, validation and write-back execute on the same engine, with retries and a run history, instead of a script on somebody's machine.
Every record keeps its provenance
Source file, row number and run id, with the platform audit trail behind it. Where did this lead come from has an answer.
Credentials are managed, never in code
The import writes to the CRM at volume, so its credential is the one worth watching. Held in your workspace, rotatable without republishing the workflow.
Ops owns the mappings and the rules
Source mappings and validation rules open in Tray Build, so a new list format is handled by the team that receives it.
Quarantine is watched, not just written
Alert when it grows or when one reason spikes. A quarantine nobody opens is a delete with extra steps.
Questions people ask
Why validate the whole file before writing any of it?
Because a partial import leaves the CRM in a state nobody can describe. Half the leads are in, some are duplicates of the half that failed, and unwinding it is worse than the original problem.
What belongs in the quarantine?
The original row exactly as it arrived, its row number, every reason it failed and the source file, in a place somebody can correct and resubmit. A count of failures is not actionable.
Why dedupe against the CRM, not the file?
Because the expensive duplicate is the one already in Salesforce with an owner and a history. Deduping within the file alone still creates a second record for somebody a rep is already working.
Should an import ever overwrite an owner?
No. Update the record, enrich the fields, but never reassign it because a list arrived. That is how a rep loses an account they have been working for a month.
Can marketing operations add a new list format themselves?
Yes. The workflow is built from a coding assistant and opens in Tray Build, so a source mapping or a validation rule is an edit by the team receiving the lists.
Vibe-coding app guides
Vibe-code an entire app with Helix
This moves the data between systems. It does not give anybody a screen to work in. Build that app in Claude Code, Codex or Cursor, then deploy and run it governed on Tray Helix. Same kind of guide, same kind of prompts.
How to build a lead routing app (opens helix.tray.ai in a new tab)
Further reading
Background on the same subject, for the case rather than the build.
Related guides
Revenue operations
How to build lead-to-account matching
Domain first, then fuzzy name, then hierarchy, with a confidence score and a review queue for the ones that are close. The Headless prompts that build it.
Revenue operations
How to build lead routing that assigns in seconds
Match the lead to an account first, evaluate rules in a fixed order, catch what they miss, and time it from arrival. The Headless prompts that build it.
Last reviewed September 2026.