Post-Event Contact Export
Run overnight contact extraction from LinkedIn, X, or other social platforms after networking events
Overview
After networking events, you need to export new connections from LinkedIn, X, or other platforms into your CRM. This automation handles it for you.
The workflow: Kick off the script after an event and let it run overnight. Wake up to a clean CSV ready for your CRM or email tool.
This example focuses on LinkedIn but works across platforms. It uses Cua Computer to interact with web interfaces and Agent Loops to iterate through connections with conversation history.
Why Cua is Perfect for This
Cua's VMs save your session data, bypassing bot detection entirely:
- Log in once manually through the VM browser
- Session persists - you appear as a regular user, not a bot
- No captchas - the platform treats automation like normal browsing
- No login code - script doesn't handle authentication
- Run overnight - kick off and forget
Traditional web scraping triggers anti-bot measures immediately. Cua's approach works across all platforms.
What You Get
The script generates two files with your extracted connections:
CSV Export (linkedin_connections_20250116_143022.csv):
first,last,role,company,met_at,linkedin
John,Smith,Software Engineer,Acme Corp,Google Devfest Toronto,https://www.linkedin.com/in/johnsmith
Sarah,Johnson,Product Manager,Tech Inc,Google Devfest Toronto,https://www.linkedin.com/in/sarahjohnsonMessaging Links (linkedin_messaging_links_20250116_143022.txt):
LinkedIn Messaging Compose Links
================================================================================
1. https://www.linkedin.com/messaging/compose/?recipient=johnsmith
2. https://www.linkedin.com/messaging/compose/?recipient=sarahjohnsonSet Up Your Environment
First, install the required dependencies:
Create a requirements.txt file:
cua-agent
cua-computer
python-dotenv>=1.0.0Install the dependencies:
pip install -r requirements.txtCreate a .env file with your API keys:
ANTHROPIC_API_KEY=your-anthropic-api-key
CUA_API_KEY=sk_cua-api01...
CUA_CONTAINER_NAME=m-linux-...Log Into LinkedIn Manually
Important: Before running the script, manually log into LinkedIn through your VM:
- Access your VM through the Cua dashboard
- Open a browser and navigate to LinkedIn
- Log in with your credentials (handle any captchas manually)
- Close the browser but leave the VM running
- Your session is now saved and ready for automation!
This one-time manual login bypasses all bot detection.
Configure and Create Your Script
Create a Python file (e.g., contact_export.py). You can customize:
# Where you met these connections (automatically added to CSV)
MET_AT_REASON = "Google Devfest Toronto"
# Number of contacts to extract (in the main loop)
for contact_num in range(1, 21): # Change 21 to extract more/fewer contactsSelect your environment:
import asyncio
import csv
import logging
import os
import signal
import traceback
from datetime import datetime
from agent import ComputerAgent
from computer import Computer, VMProviderType
from dotenv import load_dotenv
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Configuration: Define where you met these connections
MET_AT_REASON = "Google Devfest Toronto"
def handle_sigint(sig, frame):
print("\n\nExecution interrupted by user. Exiting gracefully...")
exit(0)
def extract_public_id_from_linkedin_url(linkedin_url):
"""Extract public ID from LinkedIn profile URL."""
if not linkedin_url:
return None
url = linkedin_url.split('?')[0].rstrip('/')
if '/in/' in url:
public_id = url.split('/in/')[-1]
return public_id
return None
def extract_contact_from_response(result_output):
"""
Extract contact information from agent's response.
Expects format:
FIRST: value
LAST: value
ROLE: value
COMPANY: value
LINKEDIN: value
"""
contact = {
'first': '',
'last': '',
'role': '',
'company': '',
'met_at': MET_AT_REASON,
'linkedin': ''
}
for item in result_output:
if item.get("type") == "message":
content = item.get("content", [])
for content_part in content:
text = content_part.get("text", "")
if text:
for line in text.split('\n'):
line = line.strip()
line_upper = line.upper()
if line_upper.startswith("FIRST:"):
value = line[6:].strip()
if value and value.upper() != "N/A":
contact['first'] = value
elif line_upper.startswith("LAST:"):
value = line[5:].strip()
if value and value.upper() != "N/A":
contact['last'] = value
elif line_upper.startswith("ROLE:"):
value = line[5:].strip()
if value and value.upper() != "N/A":
contact['role'] = value
elif line_upper.startswith("COMPANY:"):
value = line[8:].strip()
if value and value.upper() != "N/A":
contact['company'] = value
elif line_upper.startswith("LINKEDIN:"):
value = line[9:].strip()
if value and value.upper() != "N/A":
contact['linkedin'] = value
return contact
async def scrape_linkedin_connections():
"""Scrape LinkedIn connections and export to CSV."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
csv_filename = f"linkedin_connections_{timestamp}.csv"
csv_path = os.path.join(os.getcwd(), csv_filename)
# Initialize CSV file
with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=['first', 'last', 'role', 'company', 'met_at', 'linkedin'])
writer.writeheader()
print(f"\n🚀 Starting LinkedIn connections scraper")
print(f"📁 Output file: {csv_path}")
print(f"📍 Met at: {MET_AT_REASON}")
print("=" * 80)
try:
async with Computer(
os_type="linux",
provider_type=VMProviderType.CLOUD,
name=os.environ["CUA_CONTAINER_NAME"], # Your sandbox name
api_key=os.environ["CUA_API_KEY"],
verbosity=logging.INFO,
) as computer:
agent = ComputerAgent(
model="cua/anthropic/claude-sonnet-4.5",
tools=[computer],
only_n_most_recent_images=3,
verbosity=logging.INFO,
trajectory_dir="trajectories",
use_prompt_caching=True,
max_trajectory_budget=10.0,
)
history = []
# Task 1: Navigate to LinkedIn connections page
navigation_task = (
"STEP 1 - NAVIGATE TO LINKEDIN CONNECTIONS PAGE:\n"
"1. Open a web browser (Chrome or Firefox)\n"
"2. Navigate to https://www.linkedin.com/mynetwork/invite-connect/connections/\n"
"3. Wait for the page to fully load\n"
"4. Confirm you can see the list of connections\n"
"5. Ready to start extracting contacts"
)
print(f"\n[Task 1/21] Navigating to LinkedIn...")
history.append({"role": "user", "content": navigation_task})
async for result in agent.run(history, stream=False):
history += result.get("output", [])
print(f"✅ Navigation completed\n")
# Extract 20 contacts
contacts_extracted = 0
linkedin_urls = []
previous_contact_name = None
for contact_num in range(1, 21):
# Build extraction task
if contact_num == 1:
extraction_task = (
f"STEP {contact_num + 1} - EXTRACT CONTACT {contact_num} OF 20:\n"
f"1. Click on the first connection's profile\n"
f"2. Extract: FIRST, LAST, ROLE, COMPANY, LINKEDIN URL\n"
f"3. Return in exact format:\n"
f"FIRST: [value]\n"
f"LAST: [value]\n"
f"ROLE: [value]\n"
f"COMPANY: [value]\n"
f"LINKEDIN: [value]\n"
f"4. Navigate back to connections list"
)
else:
extraction_task = (
f"STEP {contact_num + 1} - EXTRACT CONTACT {contact_num} OF 20:\n"
f"1. Find '{previous_contact_name}' in the list\n"
f"2. Click on the contact BELOW them\n"
f"3. Extract: FIRST, LAST, ROLE, COMPANY, LINKEDIN URL\n"
f"4. Return in exact format:\n"
f"FIRST: [value]\n"
f"LAST: [value]\n"
f"ROLE: [value]\n"
f"COMPANY: [value]\n"
f"LINKEDIN: [value]\n"
f"5. Navigate back"
)
print(f"[Task {contact_num + 1}/21] Extracting contact {contact_num}/20...")
history.append({"role": "user", "content": extraction_task})
all_output = []
async for result in agent.run(history, stream=False):
output = result.get("output", [])
history += output
all_output.extend(output)
contact_data = extract_contact_from_response(all_output)
has_name = bool(contact_data['first'] and contact_data['last'])
has_linkedin = bool(contact_data['linkedin'] and 'linkedin.com' in contact_data['linkedin'])
if has_name or has_linkedin:
with open(csv_path, 'a', newline='', encoding='utf-8') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=['first', 'last', 'role', 'company', 'met_at', 'linkedin'])
writer.writerow(contact_data)
contacts_extracted += 1
if contact_data['linkedin']:
linkedin_urls.append(contact_data['linkedin'])
if has_name:
previous_contact_name = f"{contact_data['first']} {contact_data['last']}".strip()
name_str = f"{contact_data['first']} {contact_data['last']}" if has_name else "[No name]"
print(f"✅ Contact {contact_num}/20 saved: {name_str}")
else:
print(f"⚠️ Could not extract valid data for contact {contact_num}")
if contact_num % 5 == 0:
print(f"\n📈 Progress: {contacts_extracted}/{contact_num} contacts extracted\n")
# Create messaging links file
messaging_filename = f"linkedin_messaging_links_{timestamp}.txt"
messaging_path = os.path.join(os.getcwd(), messaging_filename)
with open(messaging_path, 'w', encoding='utf-8') as txtfile:
txtfile.write("LinkedIn Messaging Compose Links\n")
txtfile.write("=" * 80 + "\n\n")
for i, linkedin_url in enumerate(linkedin_urls, 1):
public_id = extract_public_id_from_linkedin_url(linkedin_url)
if public_id:
messaging_url = f"https://www.linkedin.com/messaging/compose/?recipient={public_id}"
txtfile.write(f"{i}. {messaging_url}\n")
print("\n" + "="*80)
print("🎉 All tasks completed!")
print(f"📁 CSV file saved to: {csv_path}")
print(f"📊 Total contacts extracted: {contacts_extracted}/20")
print(f"💬 Messaging links saved to: {messaging_path}")
print("="*80)
except Exception as e:
print(f"\n❌ Error: {e}")
traceback.print_exc()
raise
def main():
try:
load_dotenv()
if "ANTHROPIC_API_KEY" not in os.environ:
raise RuntimeError("Please set ANTHROPIC_API_KEY in .env")
if "CUA_API_KEY" not in os.environ:
raise RuntimeError("Please set CUA_API_KEY in .env")
if "CUA_CONTAINER_NAME" not in os.environ:
raise RuntimeError("Please set CUA_CONTAINER_NAME in .env")
signal.signal(signal.SIGINT, handle_sigint)
asyncio.run(scrape_linkedin_connections())
except Exception as e:
print(f"\n❌ Error: {e}")
traceback.print_exc()
if __name__ == "__main__":
main()# Same code as Cloud Sandbox, but change Computer initialization to:
async with Computer(
os_type="linux",
provider_type=VMProviderType.DOCKER,
image="trycua/cua-xfce:latest",
verbosity=logging.INFO,
) as computer:And remove the CUA_API_KEY and CUA_CONTAINER_NAME requirements from .env and the validation checks.
# Same code as Cloud Sandbox, but change Computer initialization to:
async with Computer(
os_type="macos",
provider_type=VMProviderType.LUME,
name="macos-sequoia-cua:latest",
verbosity=logging.INFO,
) as computer:And remove the CUA_API_KEY and CUA_CONTAINER_NAME requirements from .env and the validation checks.
# Same code as Cloud Sandbox, but change Computer initialization to:
async with Computer(
os_type="windows",
provider_type=VMProviderType.WINDOWS_SANDBOX,
verbosity=logging.INFO,
) as computer:And remove the CUA_API_KEY and CUA_CONTAINER_NAME requirements from .env and the validation checks.
Run Your Script
Execute your contact extraction automation:
python contact_export.pyThe agent will:
- Navigate to your LinkedIn connections page
- Extract data from 20 contacts (first name, last name, role, company, LinkedIn URL)
- Save contacts to a timestamped CSV file
- Generate messaging compose links for easy follow-up
Monitor the output to see the agent's progress. The script will show a progress update every 5 contacts.
How It Works
This script demonstrates a practical workflow for extracting LinkedIn connection data:
- Session Persistence - Manually log into LinkedIn through the VM once, and the VM saves your session
- Navigation - The script navigates to your connections page using your saved authenticated session
- Data Extraction - For each contact, the agent clicks their profile, extracts data, and navigates back
- Python Processing - Python parses responses, validates data, and writes to CSV incrementally
- Output Files - Generates a CSV with contact data and a text file with messaging URLs
Next Steps
- Learn more about Cua computers and computer commands
- Read about Agent loops, tools, and supported model providers
- Experiment with different Models and Providers
- Adapt this script for other platforms (Twitter/X, email extraction, etc.)
- Join our Discord community for help
Was this page helpful?