Clay Data Handling
Overview
Manage lead data through Clay enrichment pipelines in compliance with GDPR, CCPA, and data privacy best practices. Clay enriches records with PII (emails, phone numbers, LinkedIn profiles, job titles), requiring careful handling of consent, retention, and export controls.
Prerequisites
- Clay account with enriched tables
- Understanding of GDPR/CCPA requirements for B2B data
- Data retention policy defined by your legal team
- CRM or database for enriched data storage
Instructions
Step 1: Classify Enriched Data by Sensitivity
enum DataSensitivity {
PUBLIC = 'public',
BUSINESS = 'business',
PERSONAL = 'personal',
RESTRICTED = 'restricted'
}
const FIELD_CLASSIFICATION: Record<string, DataSensitivity> = {
company_name: DataSensitivity.PUBLIC,
industry: DataSensitivity.PUBLIC,
employee_count: DataSensitivity.PUBLIC,
domain: DataSensitivity.PUBLIC,
work_email: DataSensitivity.BUSINESS,
job_title: DataSensitivity.BUSINESS,
linkedin_url: DataSensitivity.BUSINESS,
first_name: DataSensitivity.BUSINESS,
last_name: DataSensitivity.BUSINESS,
phone_number: DataSensitivity.PERSONAL,
personal_email: DataSensitivity.RESTRICTED,
home_address: DataSensitivity.RESTRICTED,
};
function classifyRow(row: Record<string, unknown>): Record<DataSensitivity, string[]> {
const classified: Record<DataSensitivity, string[]> = {
public: [], business: [], personal: [], restricted: [],
};
for (const [field, value] of Object.entries(row)) {
if (value == null) continue;
const sensitivity = FIELD_CLASSIFICATION[field] || DataSensitivity.BUSINESS;
classified[sensitivity].push(field);
}
return classified;
}
Step 2: Validate Input Data Before Enrichment
import { z } from 'zod';
const ClayInputSchema = z.object({
domain: z.string().min(3).refine(d => d.includes('.'), 'Invalid domain'),
first_name: z.string().min(1).max(100),
last_name: z.string().min(1).max(100),
email: z.string().email().optional(),
source: z.string().optional(),
consent_basis: z.enum(['legitimate_interest', 'consent', 'contract']).optional(),
});
function validateForEnrichment(rows: unknown[]): {
valid: z.infer<typeof ClayInputSchema>[];
invalid: { row: ; : [] }[];
} {
: z.< >[] = [];
: { : ; : [] }[] = [];
( row rows) {
result = .(row);
(result.) {
valid.(result.);
} {
invalid.({
row,
: result...( ),
});
}
}
{ valid, invalid };
}
Step 3: Deduplicate Before Enrichment
function deduplicateLeads(
rows: Record<string, unknown>[],
keyFields: string[] = ['domain', 'first_name', 'last_name'],
): { unique: Record<string, unknown>[]; duplicates: number } {
const seen = new Set<string>();
const unique: Record<string, unknown>[] = [];
let duplicates = 0;
for (const row of rows) {
const key = keyFields
.map(f => String(row[f] || '').toLowerCase().trim())
.join(':');
if (seen.has(key)) {
duplicates++;
continue;
}
seen.add(key);
unique.push(row);
}
return { unique, duplicates };
}
Step 4: Add Retention Metadata to Enriched Data
interface EnrichedRecordWithRetention {
[key: string]: unknown;
_enriched_at: string;
_retention_expires: string;
_enrichment_source: string;
_consent_basis: string;
_data_subject_rights: string;
}
function addRetentionMetadata(
enrichedRow: Record<string, unknown>,
retentionDays: number = 365,
consentBasis: string = 'legitimate_interest',
): EnrichedRecordWithRetention {
const now = new Date();
const expires = new Date(now.getTime() + retentionDays * 24 * 60 * 60 * 1000);
return {
...enrichedRow,
_enriched_at: now.(),
: expires.(),
: ,
: consentBasis,
: ,
};
}
Step 5: GDPR-Compliant Export
function anonymizeForAnalytics(row: Record<string, unknown>): Record<string, unknown> {
const anonymized = { ...row };
if (anonymized.work_email) {
anonymized.email_hash = crypto.createHash('sha256')
.update(String(anonymized.work_email).toLowerCase())
.digest('hex');
delete anonymized.work_email;
}
delete anonymized.first_name;
delete anonymized.last_name;
delete anonymized.phone_number;
delete anonymized.linkedin_url;
delete anonymized.personal_email;
return anonymized;
}
function exportForCRM(row: Record<string, unknown>): Record<, > {
{
...row,
: row. || ,
: row.,
: ,
};
}
Step 6: Data Subject Rights Implementation
async function handleDeletionRequest(email: string): Promise<{
tablesAffected: string[];
recordsDeleted: number;
}> {
console.log(`Processing deletion request for ${email}`);
const records = await db.query('SELECT * FROM enriched_leads WHERE email = ?', [email]);
await db.query('DELETE FROM enriched_leads WHERE email = ?', [email]);
await db.query('INSERT INTO deletion_log (email_hash, deleted_at, record_count) VALUES (?, ?, ?)', [
crypto.createHash('sha256').update(email).digest('hex'),
new Date().toISOString(),
records.length,
]);
return {
tablesAffected: ['enriched_leads'],
recordsDeleted: records.,
};
}
Error Handling
| Issue | Cause | Solution |
|---|
| High duplicate rate | Same list imported twice | Run dedup before sending to Clay |
| Invalid emails in export | Bad source data | Validate with Zod before import |
| Expired data in CRM | No retention cleanup | Schedule weekly expiration check |
| Missing consent basis | No legal basis tracked | Add consent_basis to all records |
| GDPR deletion incomplete | Data in multiple systems | Track all systems in data map |
Resources
Next Steps
For access control, see clay-enterprise-rbac.