code icon Code

Analyze Attio Workspace

Analyze Attio People by sampling recently active records to discover attributes and data patterns

Source Code

import fs from "fs";

const [outputPath = "session/attio-analysis.json", focusField = ""] =
  process.argv.slice(2);

const ATTIO_API = "https://api.attio.com/v2";
const headers = {
  Authorization: "Bearer PLACEHOLDER_TOKEN",
  "Content-Type": "application/json",
};

/**
 * Infer attribute type from Attio value structure
 */
function inferType(value) {
  if (!value || !Array.isArray(value) || value.length === 0) return "unknown";

  const first = value[0];
  if (!first) return "unknown";

  if (first.email_address !== undefined) return "email";
  if (first.original_phone_number !== undefined) return "phone";
  if (first.full_name !== undefined) return "personal-name";
  if (first.target_object !== undefined)
    return `record-reference → ${first.target_object}`;
  if (first.option !== undefined) return "select";
  if (first.domain !== undefined) return "domain";
  if (first.line_1 !== undefined) return "location";
  if (first.currency_value !== undefined) return "currency";
  if (typeof first.value === "number") return "number";
  if (typeof first.value === "boolean") return "checkbox";
  if (typeof first.value === "string") {
    // Check if it looks like a date
    if (/^\d{4}-\d{2}-\d{2}/.test(first.value)) return "date";
    return "text";
  }
  if (first.interaction_type !== undefined) return "interaction";
  if (first.referenced_actor_id !== undefined) return "actor-reference";

  return "unknown";
}

/**
 * Extract display value from Attio value structure
 */
function extractDisplayValue(value) {
  if (!value || !Array.isArray(value) || value.length === 0) return null;

  const first = value[0];
  if (!first) return null;

  if (first.value !== undefined) return first.value;
  if (first.full_name !== undefined) return first.full_name;
  if (first.email_address !== undefined) return first.email_address;
  if (first.original_phone_number !== undefined)
    return first.original_phone_number;
  if (first.target_object !== undefined) return `[${first.target_object}]`;
  if (first.option !== undefined) return first.option;
  if (first.domain !== undefined) return first.domain;
  if (first.locality !== undefined) return first.locality;

  return null;
}

/**
 * Check if a record has a populated value for a given field
 */
function hasPopulatedField(record, fieldSlug) {
  const value = record.values?.[fieldSlug];
  return Array.isArray(value) && value.length > 0 && value[0] !== null;
}

console.log("Analyzing Attio workspace...");

try {
  // Build query - optionally filter by focus field
  const queryBody = {
    limit: 100,
    sorts: [{ attribute: "updated_at", direction: "desc" }],
  };

  // If focus field provided, add filter to only get records with that field populated
  if (focusField && focusField.trim()) {
    console.log(`Focusing on records with '${focusField}' populated...`);
    queryBody.filter = {
      [focusField]: { $not_empty: true },
    };
  }

  // Query records
  console.log(
    focusField
      ? `Sampling 100 records with '${focusField}' populated...`
      : "Sampling 100 recently active records..."
  );

  const queryRes = await fetch(`${ATTIO_API}/objects/people/records/query`, {
    method: "POST",
    headers,
    body: JSON.stringify(queryBody),
  });

  if (!queryRes.ok) {
    const errorText = await queryRes.text();
    console.error(`Query failed: ${queryRes.status}`);
    console.error(errorText);
    throw new Error(`Query failed: ${queryRes.status}`);
  }

  const queryData = await queryRes.json();
  const records = queryData.data || [];
  const sampleSize = records.length;

  console.log(`  Sampled ${sampleSize} records`);

  // Discover attributes from actual record values
  const discoveredAttributes = new Map();

  for (const record of records) {
    const values = record.values || {};

    for (const [attrSlug, attrValue] of Object.entries(values)) {
      // Initialize attribute tracking if first time seeing it
      if (!discoveredAttributes.has(attrSlug)) {
        discoveredAttributes.set(attrSlug, {
          slug: attrSlug,
          type: null,
          populatedCount: 0,
          sampleValues: [],
        });
      }

      const attr = discoveredAttributes.get(attrSlug);

      // Check if attribute has a value
      const hasValue =
        Array.isArray(attrValue) &&
        attrValue.length > 0 &&
        attrValue[0] !== null;

      if (hasValue) {
        attr.populatedCount++;

        // Infer type from first populated value
        if (!attr.type || attr.type === "unknown") {
          attr.type = inferType(attrValue);
        }

        // Collect sample values (up to 10 unique per attribute)
        if (attr.sampleValues.length < 10) {
          const displayValue = extractDisplayValue(attrValue);
          if (displayValue && !attr.sampleValues.includes(displayValue)) {
            attr.sampleValues.push(displayValue);
          }
        }
      }
    }
  }

  // Calculate population percentages and build stats array
  const attributeStats = [];
  const linkedObjects = [];

  for (const [slug, attr] of discoveredAttributes) {
    const percentage =
      sampleSize > 0 ? Math.round((attr.populatedCount / sampleSize) * 100) : 0;

    attributeStats.push({
      slug,
      type: attr.type || "unknown",
      population: percentage,
      populatedCount: attr.populatedCount,
      sampleValues: attr.sampleValues,
    });

    // Track record references (linked objects)
    if (attr.type && attr.type.startsWith("record-reference")) {
      const targetObject = attr.type.split(" → ")[1] || "unknown";
      linkedObjects.push({
        attribute: slug,
        targetObject,
        population: percentage,
      });
    }
  }

  // Sort by population (highest first)
  attributeStats.sort((a, b) => b.population - a.population);

  // Extract focus field stats if provided
  let focusFieldStats = null;
  if (focusField && focusField.trim()) {
    const focusAttr = attributeStats.find((a) => a.slug === focusField);
    if (focusAttr) {
      focusFieldStats = {
        slug: focusAttr.slug,
        type: focusAttr.type,
        population: focusAttr.population,
        uniqueValues: focusAttr.sampleValues,
      };
    }
  }

  // Extract common patterns for key fields
  const jobTitleCounts = {};
  const companyCounts = {};

  for (const record of records) {
    const values = record.values || {};

    // Job titles
    const jobTitle = values.job_title?.[0]?.value;
    if (jobTitle) {
      jobTitleCounts[jobTitle] = (jobTitleCounts[jobTitle] || 0) + 1;
    }

    // Companies (if linked)
    const company = values.company?.[0];
    if (company?.target_record_id) {
      companyCounts[company.target_record_id] =
        (companyCounts[company.target_record_id] || 0) + 1;
    }
  }

  const topJobTitles = Object.entries(jobTitleCounts)
    .sort((a, b) => b[1] - a[1])
    .slice(0, 10)
    .map(([title, count]) => ({
      title,
      count,
      percentage: Math.round((count / sampleSize) * 100),
    }));

  const uniqueCompanies = Object.keys(companyCounts).length;

  // Save analysis
  const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
  if (dir) {
    fs.mkdirSync(dir, { recursive: true });
  }

  const analysis = {
    analyzedAt: new Date().toISOString(),
    sampleSize,
    sampleType: focusField ? `filtered_by_${focusField}` : "recently_updated",
    focusField: focusFieldStats,
    attributes: attributeStats,
    linkedObjects,
    patterns: {
      topJobTitles,
      uniqueCompaniesInSample: uniqueCompanies,
    },
  };

  fs.writeFileSync(outputPath, JSON.stringify(analysis, null, 2));

  // Log summary
  console.log(`\n✓ Attio analysis complete`);
  if (focusFieldStats) {
    console.log(
      `  Focus field '${focusField}': ${focusFieldStats.uniqueValues.length} unique values`
    );
    console.log(
      `    Values: ${focusFieldStats.uniqueValues.slice(0, 5).join(", ")}`
    );
  }
  console.log(
    `  Discovered ${attributeStats.length} attributes from record values`
  );
  console.log(`  Linked objects: ${linkedObjects.length}`);
  console.log(`  Sample size: ${sampleSize} records`);

  const wellPopulated = attributeStats.filter((a) => a.population >= 50);
  console.log(`  Well-populated attributes (≥50%): ${wellPopulated.length}`);

  if (attributeStats.length > 0) {
    console.log(`\n  Top attributes by population:`);
    for (const attr of attributeStats.slice(0, 8)) {
      console.log(`    ${attr.slug}: ${attr.population}% (${attr.type})`);
    }
  }

  if (topJobTitles.length > 0) {
    console.log(`\n  Top job titles:`);
    for (const jt of topJobTitles.slice(0, 5)) {
      console.log(`    ${jt.title}: ${jt.count}`);
    }
  }

  console.log(`\n  Written to: ${outputPath}`);

  console.log(
    JSON.stringify({
      success: true,
      outputPath,
      attributeCount: attributeStats.length,
      linkedObjectCount: linkedObjects.length,
      sampleSize,
      focusField: focusField || null,
    })
  );
} catch (error) {
  console.error("Error analyzing Attio:", error.message);
  throw error;
}
                  import fs from "fs";

const [outputPath = "session/attio-analysis.json", focusField = ""] =
  process.argv.slice(2);

const ATTIO_API = "https://api.attio.com/v2";
const headers = {
  Authorization: "Bearer PLACEHOLDER_TOKEN",
  "Content-Type": "application/json",
};

/**
 * Infer attribute type from Attio value structure
 */
function inferType(value) {
  if (!value || !Array.isArray(value) || value.length === 0) return "unknown";

  const first = value[0];
  if (!first) return "unknown";

  if (first.email_address !== undefined) return "email";
  if (first.original_phone_number !== undefined) return "phone";
  if (first.full_name !== undefined) return "personal-name";
  if (first.target_object !== undefined)
    return `record-reference → ${first.target_object}`;
  if (first.option !== undefined) return "select";
  if (first.domain !== undefined) return "domain";
  if (first.line_1 !== undefined) return "location";
  if (first.currency_value !== undefined) return "currency";
  if (typeof first.value === "number") return "number";
  if (typeof first.value === "boolean") return "checkbox";
  if (typeof first.value === "string") {
    // Check if it looks like a date
    if (/^\d{4}-\d{2}-\d{2}/.test(first.value)) return "date";
    return "text";
  }
  if (first.interaction_type !== undefined) return "interaction";
  if (first.referenced_actor_id !== undefined) return "actor-reference";

  return "unknown";
}

/**
 * Extract display value from Attio value structure
 */
function extractDisplayValue(value) {
  if (!value || !Array.isArray(value) || value.length === 0) return null;

  const first = value[0];
  if (!first) return null;

  if (first.value !== undefined) return first.value;
  if (first.full_name !== undefined) return first.full_name;
  if (first.email_address !== undefined) return first.email_address;
  if (first.original_phone_number !== undefined)
    return first.original_phone_number;
  if (first.target_object !== undefined) return `[${first.target_object}]`;
  if (first.option !== undefined) return first.option;
  if (first.domain !== undefined) return first.domain;
  if (first.locality !== undefined) return first.locality;

  return null;
}

/**
 * Check if a record has a populated value for a given field
 */
function hasPopulatedField(record, fieldSlug) {
  const value = record.values?.[fieldSlug];
  return Array.isArray(value) && value.length > 0 && value[0] !== null;
}

console.log("Analyzing Attio workspace...");

try {
  // Build query - optionally filter by focus field
  const queryBody = {
    limit: 100,
    sorts: [{ attribute: "updated_at", direction: "desc" }],
  };

  // If focus field provided, add filter to only get records with that field populated
  if (focusField && focusField.trim()) {
    console.log(`Focusing on records with '${focusField}' populated...`);
    queryBody.filter = {
      [focusField]: { $not_empty: true },
    };
  }

  // Query records
  console.log(
    focusField
      ? `Sampling 100 records with '${focusField}' populated...`
      : "Sampling 100 recently active records..."
  );

  const queryRes = await fetch(`${ATTIO_API}/objects/people/records/query`, {
    method: "POST",
    headers,
    body: JSON.stringify(queryBody),
  });

  if (!queryRes.ok) {
    const errorText = await queryRes.text();
    console.error(`Query failed: ${queryRes.status}`);
    console.error(errorText);
    throw new Error(`Query failed: ${queryRes.status}`);
  }

  const queryData = await queryRes.json();
  const records = queryData.data || [];
  const sampleSize = records.length;

  console.log(`  Sampled ${sampleSize} records`);

  // Discover attributes from actual record values
  const discoveredAttributes = new Map();

  for (const record of records) {
    const values = record.values || {};

    for (const [attrSlug, attrValue] of Object.entries(values)) {
      // Initialize attribute tracking if first time seeing it
      if (!discoveredAttributes.has(attrSlug)) {
        discoveredAttributes.set(attrSlug, {
          slug: attrSlug,
          type: null,
          populatedCount: 0,
          sampleValues: [],
        });
      }

      const attr = discoveredAttributes.get(attrSlug);

      // Check if attribute has a value
      const hasValue =
        Array.isArray(attrValue) &&
        attrValue.length > 0 &&
        attrValue[0] !== null;

      if (hasValue) {
        attr.populatedCount++;

        // Infer type from first populated value
        if (!attr.type || attr.type === "unknown") {
          attr.type = inferType(attrValue);
        }

        // Collect sample values (up to 10 unique per attribute)
        if (attr.sampleValues.length < 10) {
          const displayValue = extractDisplayValue(attrValue);
          if (displayValue && !attr.sampleValues.includes(displayValue)) {
            attr.sampleValues.push(displayValue);
          }
        }
      }
    }
  }

  // Calculate population percentages and build stats array
  const attributeStats = [];
  const linkedObjects = [];

  for (const [slug, attr] of discoveredAttributes) {
    const percentage =
      sampleSize > 0 ? Math.round((attr.populatedCount / sampleSize) * 100) : 0;

    attributeStats.push({
      slug,
      type: attr.type || "unknown",
      population: percentage,
      populatedCount: attr.populatedCount,
      sampleValues: attr.sampleValues,
    });

    // Track record references (linked objects)
    if (attr.type && attr.type.startsWith("record-reference")) {
      const targetObject = attr.type.split(" → ")[1] || "unknown";
      linkedObjects.push({
        attribute: slug,
        targetObject,
        population: percentage,
      });
    }
  }

  // Sort by population (highest first)
  attributeStats.sort((a, b) => b.population - a.population);

  // Extract focus field stats if provided
  let focusFieldStats = null;
  if (focusField && focusField.trim()) {
    const focusAttr = attributeStats.find((a) => a.slug === focusField);
    if (focusAttr) {
      focusFieldStats = {
        slug: focusAttr.slug,
        type: focusAttr.type,
        population: focusAttr.population,
        uniqueValues: focusAttr.sampleValues,
      };
    }
  }

  // Extract common patterns for key fields
  const jobTitleCounts = {};
  const companyCounts = {};

  for (const record of records) {
    const values = record.values || {};

    // Job titles
    const jobTitle = values.job_title?.[0]?.value;
    if (jobTitle) {
      jobTitleCounts[jobTitle] = (jobTitleCounts[jobTitle] || 0) + 1;
    }

    // Companies (if linked)
    const company = values.company?.[0];
    if (company?.target_record_id) {
      companyCounts[company.target_record_id] =
        (companyCounts[company.target_record_id] || 0) + 1;
    }
  }

  const topJobTitles = Object.entries(jobTitleCounts)
    .sort((a, b) => b[1] - a[1])
    .slice(0, 10)
    .map(([title, count]) => ({
      title,
      count,
      percentage: Math.round((count / sampleSize) * 100),
    }));

  const uniqueCompanies = Object.keys(companyCounts).length;

  // Save analysis
  const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
  if (dir) {
    fs.mkdirSync(dir, { recursive: true });
  }

  const analysis = {
    analyzedAt: new Date().toISOString(),
    sampleSize,
    sampleType: focusField ? `filtered_by_${focusField}` : "recently_updated",
    focusField: focusFieldStats,
    attributes: attributeStats,
    linkedObjects,
    patterns: {
      topJobTitles,
      uniqueCompaniesInSample: uniqueCompanies,
    },
  };

  fs.writeFileSync(outputPath, JSON.stringify(analysis, null, 2));

  // Log summary
  console.log(`\n✓ Attio analysis complete`);
  if (focusFieldStats) {
    console.log(
      `  Focus field '${focusField}': ${focusFieldStats.uniqueValues.length} unique values`
    );
    console.log(
      `    Values: ${focusFieldStats.uniqueValues.slice(0, 5).join(", ")}`
    );
  }
  console.log(
    `  Discovered ${attributeStats.length} attributes from record values`
  );
  console.log(`  Linked objects: ${linkedObjects.length}`);
  console.log(`  Sample size: ${sampleSize} records`);

  const wellPopulated = attributeStats.filter((a) => a.population >= 50);
  console.log(`  Well-populated attributes (≥50%): ${wellPopulated.length}`);

  if (attributeStats.length > 0) {
    console.log(`\n  Top attributes by population:`);
    for (const attr of attributeStats.slice(0, 8)) {
      console.log(`    ${attr.slug}: ${attr.population}% (${attr.type})`);
    }
  }

  if (topJobTitles.length > 0) {
    console.log(`\n  Top job titles:`);
    for (const jt of topJobTitles.slice(0, 5)) {
      console.log(`    ${jt.title}: ${jt.count}`);
    }
  }

  console.log(`\n  Written to: ${outputPath}`);

  console.log(
    JSON.stringify({
      success: true,
      outputPath,
      attributeCount: attributeStats.length,
      linkedObjectCount: linkedObjects.length,
      sampleSize,
      focusField: focusField || null,
    })
  );
} catch (error) {
  console.error("Error analyzing Attio:", error.message);
  throw error;
}