DEV Community

AMAAN SARFARAZ
AMAAN SARFARAZ

Posted on

Implementing a Job Outreach Agent with AutoGen and ConnectSafely.ai

autogen agent for jobSeekers

autogen agent for jobSeekers

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:

  1. Search for jobs matching specific criteria (location, keywords, role)
  2. Extract company information from job listings
  3. Find hiring managers or recruiters at those companies
  4. Check connection status to avoid duplicate requests
  5. Send personalized connection requests
  6. 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         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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"]
    }
Enter fullscreen mode Exit fullscreen mode

Example Usage:

result = search_geo_location("Australia")
# Returns: {"location_id": "101452733", "location_name": "Australia"}
Enter fullscreen mode Exit fullscreen mode

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")
    }
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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", [])
    }
Enter fullscreen mode Exit fullscreen mode

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)
    }
Enter fullscreen mode Exit fullscreen mode

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}"
    }
Enter fullscreen mode Exit fullscreen mode

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
    )
Enter fullscreen mode Exit fullscreen mode

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 = []
Enter fullscreen mode Exit fullscreen mode

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:

  1. Calls search_geo_location("India") β†’ gets location ID
  2. Calls search_jobs(location_id, "software engineering", count=10)
  3. Stores results in memory
  4. Returns formatted job list

Command 2: "Find hiring managers for the first 3 jobs"

The agent:

  1. Retrieves first 3 jobs from memory
  2. Extracts company IDs
  3. For each company: calls search_hiring_managers(company_id, job_title)
  4. Returns managers with company context

Command 3: "Send connection requests to all managers found"

The agent:

  1. Retrieves managers from previous command
  2. For each manager:
    • Calls check_connection_status(profile_id)
    • If not connected: calls send_connection_request(profile_id, ...)
  3. 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"
Enter fullscreen mode Exit fullscreen mode

Installation and Setup

Prerequisites

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
Enter fullscreen mode Exit fullscreen mode

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
]
Enter fullscreen mode Exit fullscreen mode

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 ...]
Enter fullscreen mode Exit fullscreen mode

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 ...]
Enter fullscreen mode Exit fullscreen mode

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!"
Enter fullscreen mode Exit fullscreen mode

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."
        }
Enter fullscreen mode Exit fullscreen mode

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)}
Enter fullscreen mode Exit fullscreen mode

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:]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

4. Intelligent Tool Selection

The agent autonomously decides which tools to use and in what order. For "Find jobs in Australia", it knows to:

  1. First call search_geo_location to get location ID
  2. Then call search_jobs with that ID

Limitations and Future Enhancements

Current Limitations

  1. Single Account: The agent operates with one LinkedIn account at a time
  2. Synchronous Execution: Processes commands sequentially, not in parallel
  3. Basic Personalization: Connection messages use templates, not deep personalization
  4. No Response Tracking: Doesn't track which connections accepted/responded

Potential Enhancements

  1. Response Tracking: Monitor connection acceptances and message replies
  2. Multi-Account Support: Manage campaigns across multiple LinkedIn accounts
  3. Advanced Personalization: Use profile data to craft highly personalized messages
  4. Parallel Processing: Execute multiple API calls concurrently
  5. Scheduling: Schedule connection requests over time to appear more human
  6. 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

Connect with ConnectSafely.ai

Stay updated with LinkedIn automation strategies and platform updates:

Support


Have you built automation tools using AI agents? Share your experiences in the comments below!

Top comments (0)