smithery/jeremylongshore

clay-data-handling

Implement GDPR/CCPA-compliant data handling for Clay enrichment pipelines. Use when handling PII from enrichments, implementing data retention policies, or ensuring regulatory compliance for Clay-enriched lead data. Trigger with phrases like "clay data", "clay PII", "clay GDPR", "clay data retention", "clay privacy", "clay CCPA", "clay compliance". '

Installation

$ npx skills add smithery/jeremylongshore --skill clay-data-handling

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/jeremylongshore · top by installs.

npx skills add smithery/jeremylongshore

Browse all from smithery/jeremylongshore

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.14.0
LicenseMIT
CompatibilityDesigned for Claude Code
Allowed toolsRead, Write, Edit, Grep
Declared agents claude-code

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 8,881 B
  • docs SUMMARY.md 391 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

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

// src/clay/data-classification.ts
enum DataSensitivity {
  PUBLIC = 'public',       // Company name, industry, employee count
  BUSINESS = 'business',   // Work email, job title, LinkedIn URL
  PERSONAL = 'personal',   // Phone number, personal email
  RESTRICTED = 'restricted' // Home address, personal phone
}

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

// src/clay/data-validation.ts
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: unknown; errors: string[] }[];
} {
  const valid: z.infer<typeof ClayInputSchema>[] = [];
  const invalid: { row: unknown; errors: string[] }[] = [];

  for (const row of rows) {
    const result = ClayInputSchema.safeParse(row);
    if (result.success) {
      valid.push(result.data);
    } else {
      invalid.push({
        row,
        errors: result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`),
      });
    }
  }

  return { valid, invalid };
}

Step 3: Deduplicate Before Enrichment

// src/clay/dedup.ts — prevent credit waste on duplicates
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

// src/clay/retention.ts
interface EnrichedRecordWithRetention {
  // Original enriched data
  [key: string]: unknown;
  // Retention metadata
  _enriched_at: string;       // ISO timestamp
  _retention_expires: string; // ISO timestamp
  _enrichment_source: string; // 'clay'
  _consent_basis: string;     // Legal basis for processing
  _data_subject_rights: string; // How to handle deletion requests
}

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.toISOString(),
    _retention_expires: expires.toISOString(),
    _enrichment_source: 'clay',
    _consent_basis: consentBasis,
    _data_subject_rights: 'Contact [email protected] for deletion/access requests',
  };
}

Step 5: GDPR-Compliant Export

// src/clay/export.ts
/** Strip PII for analytics/reporting exports */
function anonymizeForAnalytics(row: Record<string, unknown>): Record<string, unknown> {
  const anonymized = { ...row };
  // Hash identifiers instead of including plaintext
  if (anonymized.work_email) {
    anonymized.email_hash = crypto.createHash('sha256')
      .update(String(anonymized.work_email).toLowerCase())
      .digest('hex');
    delete anonymized.work_email;
  }
  // Remove all personal identifiers
  delete anonymized.first_name;
  delete anonymized.last_name;
  delete anonymized.phone_number;
  delete anonymized.linkedin_url;
  delete anonymized.personal_email;

  return anonymized;
}

/** Full export for CRM push (with consent tracking) */
function exportForCRM(row: Record<string, unknown>): Record<string, unknown> {
  return {
    ...row,
    processing_consent: row._consent_basis || 'legitimate_interest',
    enrichment_date: row._enriched_at,
    data_source: 'clay_enrichment',
  };
}

Step 6: Data Subject Rights Implementation

// src/clay/data-rights.ts — handle GDPR deletion/access requests
async function handleDeletionRequest(email: string): Promise<{
  tablesAffected: string[];
  recordsDeleted: number;
}> {
  // In Clay: manually delete rows containing this email
  // In your database: automated deletion
  console.log(`Processing deletion request for ${email}`);

  // 1. Find all records
  const records = await db.query('SELECT * FROM enriched_leads WHERE email = ?', [email]);

  // 2. Delete from database
  await db.query('DELETE FROM enriched_leads WHERE email = ?', [email]);

  // 3. Log for compliance audit
  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,
  ]);

  // 4. Note: Clay table rows must be deleted manually in Clay UI
  return {
    tablesAffected: ['enriched_leads'],
    recordsDeleted: records.length,
  };
}

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

Output

Maintain a data-handling record containing source and purpose, consent or other lawful basis, sensitivity classification, allowed tables/destinations, retention/deletion schedule, access owner, data-subject request status, and redacted audit reference. This guidance does not replace legal review for a specific jurisdiction or use case.

Examples

Before importing a prospect list, verify the allowed purpose and retention rule, then map each downstream CRM and warehouse copy for deletion coverage. For a deletion request, use the hashed audit record, remove the approved systems of record, confirm manual Clay-table cleanup, and report completion without exposing the individual’s data in the ticket.

Resources

Next Steps

For access control, see clay-enterprise-rbac.