Job searching on LinkedIn can be repetitive. You search for jobs, identify companies, research hiring managers, and send connection requestsβall manually. What if an AI agent could handle this entire workflow through natural language commands?
This article walks through a technical implementation of an autonomous job search agent built with Microsoft's AutoGen framework and ConnectSafely.ai's LinkedIn API. The agent handles everything from job discovery to sending personalized connection requests, all through conversational commands.
The Problem: Manual LinkedIn Job Search
The typical job search workflow involves several repetitive steps:
- Search for jobs matching specific criteria (location, keywords, role)
- Extract company information from job listings
- Find hiring managers or recruiters at those companies
- Check connection status to avoid duplicate requests
- Send personalized connection requests
- Track and manage responses
Each step requires multiple clicks, page navigations, and manual data entry. For someone applying to 50+ positions, this becomes time-consuming and error-prone.
The Solution: Autonomous Agent with API Access
Instead of manual clicks, we can use an AI agent that:
- Understands natural language commands
- Maintains context across commands
- Orchestrates multiple API calls automatically
- Makes intelligent decisions about which tools to use
The key enabler is ConnectSafely.ai, which provides programmatic access to LinkedIn data through a REST API. This eliminates the need for browser automation or unofficial scraping tools.
Architecture Overview
The system consists of three main components:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Streamlit Chat Interface β
β (Natural language commands from user) β
βββββββββββββββββββββ¬ββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β AutoGen Agent Framework β
β β
β ββββββββββββββββ βββββββββββββββββββββββ β
β β Assistant βββββββΊβ Memory Manager β β
β β Agent β β (Job Results) β β
β ββββββββ¬ββββββββ βββββββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β Tool Orchestration β β
β β (Selects & executes ConnectSafely API) β β
β ββββββββββββββββββββ¬βββββββββββββββββββββββ β
βββββββββββββββββββββ¬ββ΄βββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β ConnectSafely.ai REST API β
β β
β β’ Job Search β’ Company Details β
β β’ People Search β’ Profile Fetch β
β β’ Connection Status β’ Send Invitations β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Component Breakdown
1. Streamlit Interface (App.py)
A minimal chat interface that handles user input and displays results. The interface maintains session state to preserve conversation history and agent context.
import streamlit as st
from autogen_client import JobSearchClient
def main():
st.set_page_config(page_title="Job Search Agent", page_icon="πΌ")
# Initialize agent once per session
if "agent_client" not in st.session_state:
st.session_state.agent_client = JobSearchClient()
# Handle user commands
command = st.chat_input("Enter your command...")
if command:
result = st.session_state.agent_client.execute(command)
st.chat_message("assistant").write(result)
2. AutoGen Agent System
The agent system uses AutoGen's AssistantAgent to interpret commands and orchestrate tool execution. It maintains context across multiple commands and stores job results in memory.
3. ConnectSafely.ai API Tools
Seven specialized tools wrap ConnectSafely.ai's REST API endpoints. Each tool handles a specific LinkedIn operation.
ConnectSafely.ai API Integration
ConnectSafely.ai provides a secure, rate-limited REST API for LinkedIn operations. All endpoints use Bearer token authentication.
Authentication Setup
import os
import requests
api_token = os.getenv("CONNECTSAFELY_API_TOKEN")
headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
}
Tool 1: Geographic Location Search
Before searching jobs, we need to convert location names (like "India" or "San Francisco") to LinkedIn's internal location IDs.
def search_geo_location(keywords: str) -> dict:
"""Convert location name to geographic ID.
Args:
keywords: Location name (e.g., 'India', 'San Francisco')
Returns:
Dict with location_id and location details
"""
response = requests.post(
"https://api.connectsafely.ai/linkedin/search/geo",
headers=headers,
json={"keywords": keywords},
timeout=30
)
data = response.json()
# Returns location_id needed for job search
return {
"success": True,
"location_id": data["locations"][0]["id"],
"location_name": data["locations"][0]["name"]
}
Example Usage:
result = search_geo_location("Australia")
# Returns: {"location_id": "101452733", "location_name": "Australia"}
Tool 2: Job Search
With a location ID, we can search for jobs by keywords and filters.
def search_jobs(
location_id: str,
keywords: str = "software engineer",
count: int = 5,
date_posted: str = "past-week"
) -> dict:
"""Search for LinkedIn jobs.
Args:
location_id: Geographic location ID (from search_geo_location)
keywords: Job search keywords
count: Number of results (max 25)
date_posted: 'past-24-hours', 'past-week', 'past-month'
Returns:
Dict with jobs list containing: jobId, title, companyName,
companyId, location, etc.
"""
payload = {
"keywords": keywords,
"count": min(count, 25),
"filters": {
"datePosted": date_posted,
"locationId": location_id
}
}
response = requests.post(
"https://api.connectsafely.ai/linkedin/search/jobs",
headers=headers,
json=payload,
timeout=30
)
data = response.json()
return {
"success": True,
"jobs": data.get("jobs", []),
"total": data.get("total")
}
Example Response:
{
"success": true,
"jobs": [
{
"jobId": "4352695658",
"title": "Senior Software Engineer",
"companyName": "TechCorp",
"companyId": "1234567",
"location": "Sydney, New South Wales, Australia",
"postedAt": "2 days ago"
}
],
"total": 150
}
Tool 3: Finding Hiring Managers
Once we have companies, we can search for hiring managers, recruiters, or other relevant contacts.
def search_hiring_managers(
company_id: str,
job_title: str,
count: int = 3
) -> dict:
"""Find hiring managers at a company.
Args:
company_id: LinkedIn company ID
job_title: Job title to match relevant managers
count: Number of results
Returns:
List of profiles with name, headline, profileId (vanity name)
"""
# Auto-select appropriate search titles based on role
title_mapping = {
"engineer": "Engineering Manager, Tech Lead, Hiring Manager",
"product": "Product Manager, Head of Product",
"design": "Design Manager, Head of Design",
"data": "Data Science Manager, Analytics Manager"
}
search_title = next(
(v for k, v in title_mapping.items() if k in job_title.lower()),
"Hiring Manager, Recruiter"
)
payload = {
"keywords": search_title,
"filters": {
"currentCompany": company_id,
"connectionDegree": "SECOND" # 2nd-degree connections
},
"count": count
}
response = requests.post(
"https://api.connectsafely.ai/linkedin/search/people",
headers=headers,
json=payload,
timeout=30
)
data = response.json()
return {
"success": True,
"managers": data.get("profiles", [])
}
Key Feature: The tool automatically determines which manager titles to search for based on the job title. For a "Software Engineer" role, it searches for "Engineering Manager" and "Tech Lead" rather than generic "Hiring Manager".
Tool 4: Connection Status Check
Before sending connection requests, check if you're already connected to avoid duplicates.
def check_connection_status(profile_id: str) -> dict:
"""Check connection status with a profile.
Args:
profile_id: LinkedIn vanity name (e.g., 'john-doe-123')
Returns:
Dict with connection status: 'connected', 'invitationSent',
'invitationReceived', or 'not_connected'
"""
response = requests.get(
f"https://api.connectsafely.ai/linkedin/relationship/{profile_id}",
headers=headers,
timeout=30
)
data = response.json()
return {
"success": True,
"status": data.get("connectionDegree", "not_connected"),
"isConnected": data.get("isConnected", False)
}
Tool 5: Send Connection Request
Finally, send personalized connection requests with auto-generated messages.
def send_connection_request(
profile_id: str,
custom_message: str = None,
job_title: str = None,
company_name: str = None
) -> dict:
"""Send connection request with personalized message.
Args:
profile_id: LinkedIn vanity name
custom_message: Optional custom message (300 char limit)
job_title: Job title for auto-generated message
company_name: Company name for auto-generated message
Returns:
Success status and details
"""
# Auto-generate message if not provided
if not custom_message and job_title and company_name:
custom_message = (
f"Hi! I noticed the {job_title} role at {company_name} "
f"and would love to connect. Looking forward to learning more "
f"about the team and opportunity!"
)[:300] # LinkedIn's 300-character limit
payload = {
"profileId": profile_id,
"customMessage": custom_message
}
response = requests.post(
"https://api.connectsafely.ai/linkedin/connect",
headers=headers,
json=payload,
timeout=30
)
return {
"success": response.ok,
"message": custom_message,
"profile_url": f"https://linkedin.com/in/{profile_id}"
}
AutoGen Agent Configuration
The AutoGen agent orchestrates these tools and maintains context between commands.
Agent Factory
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.google import GoogleAIGeminiClient
def create_assistant_agent(tools: list) -> AssistantAgent:
"""Create configured assistant agent with tools."""
model_client = GoogleAIGeminiClient(
model="gemini-2.0-flash-exp",
api_key=os.getenv("GEMINI_API_KEY")
)
system_message = """You are a LinkedIn job search assistant.
Available tools:
- search_geo_location: Convert location names to IDs
- search_jobs: Find LinkedIn jobs
- search_hiring_managers: Find recruiters/managers at companies
- check_connection_status: Check if already connected
- send_connection_request: Send personalized requests
Context memory:
- You have access to previous job search results
- Always check connection status before sending requests
- Use job IDs and company IDs from previous searches
Execute commands step by step and provide clear feedback."""
return AssistantAgent(
name="job_search_assistant",
model_client=model_client,
tools=tools,
system_message=system_message
)
Memory Manager
The agent maintains a memory list of job search results that persists across commands.
class MemoryManager:
"""Manages job search results and context."""
def __init__(self):
self.job_results = []
def add_jobs(self, jobs: list):
"""Store job search results."""
self.job_results.extend(jobs)
def get_jobs_by_indices(self, indices: list) -> list:
"""Retrieve specific jobs by index."""
return [
self.job_results[i]
for i in indices
if i < len(self.job_results)
]
def get_job_by_id(self, job_id: str) -> dict:
"""Find job by ID."""
return next(
(j for j in self.job_results if j.get("jobId") == job_id),
None
)
def clear(self):
"""Reset memory."""
self.job_results = []
Natural Language Command Processing
The agent processes commands in natural language and determines which tools to use.
Example Workflow
Command 1: "Find 10 software engineering jobs in India"
The agent:
- Calls
search_geo_location("India")β gets location ID - Calls
search_jobs(location_id, "software engineering", count=10) - Stores results in memory
- Returns formatted job list
Command 2: "Find hiring managers for the first 3 jobs"
The agent:
- Retrieves first 3 jobs from memory
- Extracts company IDs
- For each company: calls
search_hiring_managers(company_id, job_title) - Returns managers with company context
Command 3: "Send connection requests to all managers found"
The agent:
- Retrieves managers from previous command
- For each manager:
- Calls
check_connection_status(profile_id) - If not connected: calls
send_connection_request(profile_id, ...)
- Calls
- Returns success/failure for each request
Code Execution Flow
class JobSearchClient:
"""Client wrapper for agent execution."""
def __init__(self):
self.agent = create_assistant_agent(tools)
self.memory = MemoryManager()
def execute(self, command: str) -> str:
"""Execute a natural language command.
Args:
command: User command (e.g., "Find 5 jobs in Canada")
Returns:
Formatted response string
"""
# Add memory context to command
context = self._build_context()
full_prompt = f"{context}\n\nCommand: {command}"
# Execute via AutoGen
response = self.agent.run(full_prompt)
# Update memory if jobs were found
if "jobs" in response.data:
self.memory.add_jobs(response.data["jobs"])
# Format and return response
return self._format_response(response)
def _build_context(self) -> str:
"""Build context from memory."""
if not self.memory.job_results:
return "No previous results in memory."
return f"Previous job search: {len(self.memory.job_results)} jobs found"
Installation and Setup
Prerequisites
- Python 3.10+
- ConnectSafely.ai API token (get yours here)
- Google Gemini API key (get here)
Quick Start
# Clone the repository
git clone https://github.com/ConnectSafelyAI/agentic-framework-examples
cd agentic-framework-examples/job-seekers-reach-out-to-hiring-managers/agentic/autogen
# Create .env file
cat > .env << EOF
CONNECTSAFELY_API_TOKEN=your_token_here
GEMINI_API_KEY=your_key_here
EOF
# Install dependencies (using uv)
uv sync
# Run the application
uv run streamlit run App.py
The app will launch at http://localhost:8501
Project Dependencies
[project]
dependencies = [
"autogen-agentchat>=0.7.0", # Core AutoGen framework
"autogen-ext[google]>=0.4.0", # Google Gemini integration
"streamlit>=1.39.0", # Web interface
"python-dotenv>=1.0.0", # Environment variables
"requests>=2.32.0" # HTTP client
]
Usage Examples
Example 1: Basic Job Search
User: Find 5 backend developer jobs in Canada
Agent Response:
β
Found 5 backend developer jobs in Canada
1. Senior Backend Engineer - TechCorp (Job ID: 4352695658)
π Toronto, Ontario, Canada
π Posted 1 day ago
2. Python Backend Developer - StartupXYZ (Job ID: 4352712948)
π Vancouver, British Columbia, Canada
π Posted 3 days ago
[... 3 more jobs ...]
Example 2: Multi-Step Workflow
User: Find 10 frontend developer jobs in Australia
Agent: β
Found 10 jobs
User: Find hiring managers for the first 3 jobs
Agent: β
Found hiring managers:
Job #1 (Senior Frontend Engineer at TechCorp):
β’ Jane Smith - Engineering Manager
β’ Bob Johnson - Tech Lead
Job #2 (React Developer at StartupXYZ):
β’ Alice Wong - Head of Engineering
[... etc ...]
User: Send connection requests to all managers
Agent:
β
Sent request to Jane Smith (jane-smith-123)
β
Sent request to Bob Johnson (bob-johnson-456)
βοΈ Already connected with Alice Wong
[... etc ...]
Example 3: Targeted Search
User: Find hiring manager for job ID 4352695658
Agent:
β
Found hiring manager for Senior Backend Engineer at TechCorp
Name: Michael Chen
Title: VP of Engineering
Profile: linkedin.com/in/michael-chen-789
Connection: 2nd degree
User: Send connection request to Michael Chen
Agent:
β
Connection request sent!
Profile: linkedin.com/in/michael-chen-789
Message: "Hi Michael! I noticed the Senior Backend Engineer role at
TechCorp and would love to connect. Looking forward to learning more
about the team and opportunity!"
Technical Considerations
Rate Limiting
ConnectSafely.ai implements rate limiting to comply with LinkedIn's terms of service. The agent handles rate limit errors gracefully:
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
if response.status_code == 429:
return {
"success": False,
"error": "Rate limit exceeded. Please wait a few minutes."
}
Error Handling
Each tool implements comprehensive error handling:
def search_jobs(...):
api_token = os.getenv("CONNECTSAFELY_API_TOKEN")
if not api_token:
return {
"success": False,
"error": "CONNECTSAFELY_API_TOKEN not set"
}
try:
response = requests.post(...)
if not response.ok:
return {
"success": False,
"error": f"API error: {response.status_code}"
}
return {"success": True, "data": response.json()}
except Exception as e:
return {"success": False, "error": str(e)}
Context Management
The agent maintains context with a 2000-character limit to prevent token overflow:
def trim_context(context: str, max_length: int = 2000) -> str:
"""Trim context to prevent token overflow."""
if len(context) <= max_length:
return context
# Keep most recent content
return "...[earlier context trimmed]\n" + context[-max_length:]
Profile ID Validation
LinkedIn uses "vanity names" for connection requests. The agent validates and extracts these automatically:
def extract_vanity_name(profile_url: str) -> str:
"""Extract vanity name from LinkedIn URL.
Args:
profile_url: Full URL like 'https://linkedin.com/in/john-doe-123'
Returns:
Vanity name: 'john-doe-123'
"""
if "linkedin.com/in/" in profile_url:
return profile_url.split("/in/")[-1].split("/")[0]
return profile_url
Benefits of This Approach
1. API-Based vs Browser Automation
Traditional Approach (Browser Automation):
- Prone to breakage when LinkedIn updates UI
- Slow (waits for page loads)
- Resource-intensive (runs browser instance)
- Risk of account restrictions
API Approach (ConnectSafely.ai):
- Stable interface (REST API doesn't change frequently)
- Fast (direct HTTP requests)
- Lightweight (no browser overhead)
- Rate-limited and compliant
2. Natural Language Interface
Users don't need to learn API syntax or write code. Commands like "Find 5 jobs in Canada" are much more intuitive than:
# Without agent
location = search_geo("Canada")
jobs = search_jobs(location["id"], count=5)
for job in jobs:
company_id = job["companyId"]
managers = search_people(company_id, ...)
# ... etc
3. Stateful Conversations
The agent remembers previous results, enabling follow-up commands:
"Find jobs" β stores results
"Find managers for first 3" β uses stored results
"Connect with them" β uses manager list from previous command
4. Intelligent Tool Selection
The agent autonomously decides which tools to use and in what order. For "Find jobs in Australia", it knows to:
- First call
search_geo_locationto get location ID - Then call
search_jobswith that ID
Limitations and Future Enhancements
Current Limitations
- Single Account: The agent operates with one LinkedIn account at a time
- Synchronous Execution: Processes commands sequentially, not in parallel
- Basic Personalization: Connection messages use templates, not deep personalization
- No Response Tracking: Doesn't track which connections accepted/responded
Potential Enhancements
- Response Tracking: Monitor connection acceptances and message replies
- Multi-Account Support: Manage campaigns across multiple LinkedIn accounts
- Advanced Personalization: Use profile data to craft highly personalized messages
- Parallel Processing: Execute multiple API calls concurrently
- Scheduling: Schedule connection requests over time to appear more human
- Analytics Dashboard: Track success rates, response times, and conversion metrics
Conclusion
This implementation demonstrates how AI agents can automate complex, multi-step workflows through natural language commands. By combining AutoGen's agent orchestration with ConnectSafely.ai's LinkedIn API, we've created a system that handles job search and outreach autonomously while remaining compliant and rate-limited.
The key insights:
- API-based automation is more reliable than browser automation
- Agent frameworks like AutoGen simplify complex tool orchestration
- Natural language interfaces make automation accessible to non-developers
- Stateful memory enables multi-step workflows with context preservation
For developers looking to build similar automation tools, the pattern is clear: choose a robust API provider (like ConnectSafely.ai), use an agent framework for orchestration (like AutoGen), and wrap it in a user-friendly interface.
Resources
- GitHub Repository: github.com/ConnectSafelyAI/agentic-framework-examples
- ConnectSafely.ai Documentation: connectsafely.ai/docs
- ConnectSafely.ai Dashboard: connectsafely.ai
- AutoGen Documentation: microsoft.github.io/autogen
Connect with ConnectSafely.ai
Stay updated with LinkedIn automation strategies and platform updates:
- LinkedIn: linkedin.com/company/connectsafelyai
- YouTube: youtube.com/@ConnectSafelyAI-v2x
- Instagram: instagram.com/connectsafely.ai
- Facebook: facebook.com/connectsafelyai
- X (Twitter): x.com/AiConnectsafely
Support
- Email: support@connectsafely.ai
- Documentation: connectsafely.ai/docs
- API Refrence: connectsafely.ai (for API key management)
Have you built automation tools using AI agents? Share your experiences in the comments below!


Top comments (0)