🔆 Airtable + Swarm

Pull The Swarm people and relationship data on Airtable

Relationship intelligence on Airtable

Airtable is the go-to for flexible databases and workflows, and now you can enrich it with Swarm relationship intelligence data. Map intro paths to your target companies and buyers based on your team and stakeholders' work history, education overlaps, social networks, and investor networks.

Whether you're building a custom CRM, prospect, partner, or customer records on Airtable, The Swarm will help you surface warm intro paths, without leaving your base!
The Swarm – Data API Example
"We've been using The Swarm API to pull relationship data from The Swarm on Airtable as part of one of our core workflows."
Andy Mowat
Founder at Whispered, former VP of RevOps at Carta

How to get started?

1. Watch the 2-min demo here.
2. Start an account at app.theswarm.com (14-day trial, plans start at $99/month) and retrieve your API key here.
3. Create an Airtable automation (Team plan required) with the script below and replace YOURKEY).

/**
 * Swarm — Network Mapper → Airtable (per official docs)
 * Table fields:
 *  - "Domain" (text)
 *  - "Relationship Count" (number)
 *  - "Top Connector" (text)
 *  - "Paths (JSON)" (long text)
 */

const TABLE_NAME = 'Profiles';
const DOMAIN_FIELD = 'Domain';
const REL_COUNT_FIELD = 'Relationship Count';
const TOP_CONNECTOR_FIELD = 'Top Connector';
const PATHS_JSON_FIELD = 'Paths (JSON)';

const SWARM_API_KEY = 'YOURKEY';
const SWARM_URL = 'https://bee.theswarm.com/v2/profiles/network-mapper';

// ---------- helpers ----------
function chunk(arr, n) { const out=[]; for (let i=0;i<arr.length;i+=n) out.push(arr.slice(i,i+n)); return out; }
function toItems(data) { return (data && Array.isArray(data.items)) ? data.items : []; }
function normDomain(raw) {
  if (!raw) return '';
  let d = String(raw).trim().toLowerCase();
  d = d.replace(/^https?:\/\//,'').replace(/^www\./,'').split('/')[0];
  return d;
}

// ---------- main ----------
const table = base.getTable(TABLE_NAME);
const query = await table.selectRecordsAsync();

const updates = [];
let processed = 0;

for (const record of query.records) {
  const domainRaw = record.getCellValue(DOMAIN_FIELD);
  if (!domainRaw) continue;
  const domain = normDomain(domainRaw);

  // Per docs: term on profile_info.current_company_website with { value: "<domain>" }
  const body = {
    query: {
      term: {
        'profile_info.current_company_website': { value: domain }
      }
    }
  };

  let json;
  try {
    const resp = await fetch(SWARM_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'x-api-key': SWARM_API_KEY },
      body: JSON.stringify(body)
    });
    if (!resp.ok) {
      const txt = await resp.text();
      console.log(`API failed for ${domain}: ${resp.status} ${resp.statusText} :: ${txt.slice(0,300)}`);
      // write zeros so the row is “done”
      updates.push({ id: record.id, fields: {
        [REL_COUNT_FIELD]: 0, [TOP_CONNECTOR_FIELD]: '', [PATHS_JSON_FIELD]: '[]'
      }});
      processed += 1;
      continue;
    }
    json = await resp.json();
  } catch (e) {
    console.log(`Request error for ${domain}: ${e.message}`);
    continue;
  }

  const items = toItems(json);

  // Flatten connections
  const connections = [];
  for (const item of items) {
    const profile = item?.profile || {};
    const conns = Array.isArray(item?.connections) ? item.connections : [];
    for (const c of conns) {
      connections.push({
        profileName: profile.full_name || '',
        profileTitle: profile.current_title || '',
        profileCompany: profile.current_company_name || '',
        profileWebsite: profile.current_company_website || '',
        teamUser: c.connector_name || c.connector_id || '',
        teamUserLinkedIn: c.connector_linkedin_url || '',
        strength: typeof c.connection_strength === 'number'
          ? c.connection_strength
          : (typeof c.connection_strength_normalized === 'number' ? c.connection_strength_normalized : 0),
        sources: (c.sources || []).map(s => s?.origin).filter(Boolean)
      });
    }
  }

  const relationshipCount = connections.length;
  let topConnector = '';
  if (relationshipCount > 0) {
    connections.sort((a,b)=>(b.strength||0)-(a.strength||0));
    const t = connections[0];
    topConnector = `${t.teamUser} -> ${t.profileName} (${(t.strength||0).toFixed(2)})`;
  }

  // Compact JSON for Airtable cell limits
  const compact = connections.map(c => ({
    profileName: c.profileName,
    profileTitle: c.profileTitle,
    teamUser: c.teamUser,
    strength: c.strength,
    sources: [...new Set(c.sources)]
  }));

  updates.push({
    id: record.id,
    fields: {
      [REL_COUNT_FIELD]: relationshipCount,
      [TOP_CONNECTOR_FIELD]: topConnector,
      [PATHS_JSON_FIELD]: JSON.stringify(compact, null, 2)
    }
  });

  processed += 1;
}

// Batch updates
for (const group of chunk(updates, 50)) {
  await table.updateRecordsAsync(group);
}

console.log(`Processed ${processed} record(s). Updated ${updates.length} record(s).`);