DEV Community

Cover image for Building a LinkedIn Outreach Agent with LangGraph and ConnectSafely.ai
AMAAN SARFARAZ
AMAAN SARFARAZ

Posted on

Building a LinkedIn Outreach Agent with LangGraph and ConnectSafely.ai

A production-style agentic workflow for LinkedIn outreach using LangGraph state machines and ConnectSafely.ai API

This article walks through building a production-grade agentic workflow using LangGraph and ConnectSafely.ai for LinkedIn outreach automation.

The goal: help job seekers identify relevant jobs, discover hiring managers, and perform controlled LinkedIn outreach—without browser automation, scraping, or UI hacking.

Core Design Principles

✅ Explicit state transitions

✅ Deterministic tools

✅ Strict separation between reasoning and execution

✅ No prompt hacks or brittle automation


System Architecture Overview

This isn't a free-running autonomous loop. It's a graph-based agent where LangGraph controls execution flow and ConnectSafely.ai enforces safety boundaries.

LangGraph State Machine
├─ Search jobs
├─ Resolve company details
├─ Find hiring managers
├─ Fetch LinkedIn profile data
├─ Check connection status
└─ Send connection request
    
ConnectSafely.ai API
    
LinkedIn Platform
Enter fullscreen mode Exit fullscreen mode

LangGraph controls what happens next.

ConnectSafely.ai controls what is allowed to happen.


Why LangGraph Over Simple Agent Loops?

LangGraph is used because this workflow is stateful and conditional.

Each step:

  • Consumes structured state
  • Produces structured output
  • Decides the next transition deterministically

This approach:

  • ✅ Avoids prompt-driven branching
  • ✅ Makes execution traceable
  • ✅ Enables debugging and replay
  • ✅ Prevents infinite loops

Project Structure

├── index.ts                    # Entry point
├── agents/
│   └── job-search-outreach-agent.ts  # LangGraph state machine
├── tools/                      # LinkedIn API wrappers
│   ├── searchJobs.ts
│   ├── getCompanyDetails.ts
│   ├── searchHiringManagers.ts
│   ├── fetchProfileDetails.ts
│   ├── checkConnectionStatus.ts
│   └── sendConnectionRequest.ts
├── cli/                        # Interactive CLI
└── .env                        # API keys
Enter fullscreen mode Exit fullscreen mode

Runtime Stack

  • TypeScript for type safety
  • Bun for fast startup and dependency management
  • LangGraph for workflow orchestration
  • ConnectSafely.ai for LinkedIn API access

Entry Point

// index.ts
import { runCli } from "./cli";

runCli();
Enter fullscreen mode Exit fullscreen mode

Simple, clean, and framework-agnostic.


The Agent Definition

The core logic lives in agents/job-search-outreach-agent.ts:

import { StateGraph } from "@langchain/langgraph";

const graph = new StateGraph<AgentState>()
  .addNode("searchJobs", searchJobs)
  .addNode("getCompanyDetails", getCompanyDetails)
  .addNode("searchHiringManagers", searchHiringManagers)
  .addNode("fetchProfileDetails", fetchProfileDetails)
  .addNode("checkConnectionStatus", checkConnectionStatus)
  .addNode("sendConnectionRequest", sendConnectionRequest)
  .addEdge("searchJobs", "getCompanyDetails")
  .addEdge("getCompanyDetails", "searchHiringManagers")
  .addEdge("searchHiringManagers", "fetchProfileDetails")
  .addEdge("fetchProfileDetails", "checkConnectionStatus")
  .addConditionalEdges("checkConnectionStatus", routingLogic);
Enter fullscreen mode Exit fullscreen mode

Each node is a pure function over state. No side effects, no hidden behavior.


Tool Design: Deterministic by Construction

All LinkedIn-facing logic is implemented as tools. Each tool:

  • ✅ Has a single responsibility
  • ✅ Performs one API call
  • ✅ Returns structured data
  • ✅ Never makes decisions

This keeps the agent layer clean and testable.


1️⃣ Searching Jobs

export async function searchJobs(input: {
  keyword: string;
  location: string;
}) {
  const response = await fetch(`${API_URL}/linkedin/search/jobs`, {
    headers: {
      Authorization: `Bearer ${process.env.CONNECTSAFELY_API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(input)
  });

  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

The agent decides when to search.

The tool only executes.


2️⃣ Fetching Company Details

export async function getCompanyDetails(input: {
  companyId: string;
}) {
  const response = await fetch(
    `${API_URL}/linkedin/search/companies/details`,
    {
      headers: {
        Authorization: `Bearer ${process.env.CONNECTSAFELY_API_KEY}`
      }
    }
  );

  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

No scraping. No HTML parsing. Only structured responses.


3️⃣ Finding Hiring Managers

export async function searchHiringManagers(input: {
  companyId: string;
}) {
  const response = await fetch(
    `${API_URL}/linkedin/search/hiring-managers`,
    {
      headers: {
        Authorization: `Bearer ${process.env.CONNECTSAFELY_API_KEY}`
      }
    }
  );

  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

The agent receives candidates directly—no guessing or scraping required.


4️⃣ Fetching Profile Details

export async function fetchProfileDetails(input: {
  profileId: string;
}) {
  const response = await fetch(
    `${API_URL}/linkedin/profile/details`,
    {
      headers: {
        Authorization: `Bearer ${process.env.CONNECTSAFELY_API_KEY}`
      }
    }
  );

  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

This data is used for:

  • ✅ Validation
  • ✅ Personalization
  • ✅ Rule-based decisions

5️⃣ Connection Safety Checks

Before any outreach, the system verifies connection state:

export async function checkConnectionStatus(input: {
  profileId: string;
}) {
  const response = await fetch(
    `${API_URL}/linkedin/connections/status`,
    {
      headers: {
        Authorization: `Bearer ${process.env.CONNECTSAFELY_API_KEY}`
      }
    }
  );

  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

This prevents:

  • ❌ Duplicate requests
  • ❌ Invalid actions
  • ❌ Noisy automation

LangGraph uses this output to decide the next edge.


6️⃣ Sending Connection Requests

Only after passing all checks does the workflow reach execution:

export async function sendConnectionRequest(input: {
  profileId: string;
  note?: string;
}) {
  const response = await fetch(
    `${API_URL}/linkedin/connections/send`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.CONNECTSAFELY_API_KEY}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify(input)
    }
  );

  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

The agent cannot bypass platform rules—those are enforced by ConnectSafely.ai.


ConnectSafely.ai as the Execution Boundary

This system does not:

❌ Run headless browsers

❌ Scrape LinkedIn pages

❌ Manage cookies or sessions

❌ Simulate human behavior

Instead:

✅ All LinkedIn actions are API calls

✅ Authentication is key-based

✅ Rate limits and safety rules are enforced externally

✅ Platform complexity is isolated

The agent stays clean and portable.


CLI Interface

The cli/ directory provides:

# Interactive execution
$ bun run start

# Command routing
$ bun run agent --job "Software Engineer" --location "San Francisco"

# State visualization
$ bun run agent --debug
Enter fullscreen mode Exit fullscreen mode

This makes the workflow usable without modifying code and helps with debugging graph transitions.


State Management Example

interface AgentState {
  // User inputs
  jobKeyword: string;
  location: string;

  // Job search results
  jobs: Job[];
  selectedJob?: Job;

  // Company context
  company?: Company;

  // Hiring managers
  hiringManagers: Profile[];
  selectedManager?: Profile;

  // Connection state
  connectionStatus?: ConnectionStatus;

  // Execution tracking
  currentStep: string;
  errors: string[];
}
Enter fullscreen mode Exit fullscreen mode

State flows through the graph explicitly—no hidden context.


Conditional Routing Logic

function routingLogic(state: AgentState): string {
  const { connectionStatus } = state;

  if (connectionStatus === "ALREADY_CONNECTED") {
    return "skipConnectionRequest";
  }

  if (connectionStatus === "PENDING") {
    return "skipConnectionRequest";
  }

  if (connectionStatus === "CAN_CONNECT") {
    return "sendConnectionRequest";
  }

  return "end";
}
Enter fullscreen mode Exit fullscreen mode

Decision logic is explicit, testable, and debuggable.


Why This Architecture Works

This pattern scales because:

Reasoning is explicit — no hidden prompt logic

Execution is deterministic — same input = same path

State is visible — full observability

Platform risk is externalized — LinkedIn complexity isolated


Ideal Use Cases

  • 🎯 Recruiting workflows
  • 🔍 Job discovery automation
  • 📊 CRM enrichment
  • ⚖️ Compliance-sensitive LinkedIn tooling
  • 🤝 B2B outreach campaigns

Key Takeaways

  1. LangGraph is ideal for multi-step, stateful agents
  2. Agents should orchestrate, not improvise
  3. LinkedIn automation belongs behind an API boundary
  4. Deterministic tools beat prompt-only logic
  5. Safety checks must be first-class nodes

Get Started

Prerequisites

# Install Bun
curl -fsSL https://bun.sh/install | bash

# Clone the repository
git clone https://github.com/ConnectSafelyAI/agentic-framework-examples/tree/main/job-seekers-reach-out-to-hiring-managers/agentic/langGraph

# Install dependencies
bun install

# Set up environment variables
cp .env.example .env
# Add your ConnectSafely.ai API key
Enter fullscreen mode Exit fullscreen mode

Running the Agent

# Interactive mode
bun run start

# Direct execution
bun run agent --job "Product Manager" --location "Remote"
Enter fullscreen mode Exit fullscreen mode

Resources & Documentation

Official Documentation

Support

Connect With Us


What's Next?

This architecture can be extended to:

  • Multi-channel outreach (email + LinkedIn)
  • CRM integration (sync to Salesforce, HubSpot)
  • Advanced personalization with LLMs
  • Analytics and campaign tracking
  • Team collaboration features

Want to build your own agentic workflows? Start with LangGraph and ConnectSafely.ai.


Building reliable AI automation requires architecture, not just prompts. This is what production-grade agents look like.


Have questions or want to share your implementation? Drop a comment below! 👇

Top comments (0)