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
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
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();
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);
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();
}
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();
}
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();
}
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();
}
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();
}
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();
}
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
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[];
}
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";
}
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
- LangGraph is ideal for multi-step, stateful agents
- Agents should orchestrate, not improvise
- LinkedIn automation belongs behind an API boundary
- Deterministic tools beat prompt-only logic
- 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
Running the Agent
# Interactive mode
bun run start
# Direct execution
bun run agent --job "Product Manager" --location "Remote"
Resources & Documentation
Official Documentation
Support
- Email: support@connectsafely.ai
- Documentation: connectsafely.ai/docs
- Api Refrence: connectsafely.ai (API key management)
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)