Building a Referral Outreach Agentic Workflow in LangGraph
This guide presents a comprehensive overview of building an automated, agentic referral outreach workflow from scratch using Python, LangChain, and LangGraph. Grounded in the architectural principles demonstrated by Google Web Solutions Engineer Saraf Pratik, this note explores how to transform a tedious, manual candidate outreach process into a deterministic, graph-based AI pipeline that ingests raw profile data, extracts structured parameters, and drafts concise, high-converting referral pitches.
1. The Operational Problem: Manual LinkedIn Referral Outreach
Job candidates seeking internal referrals on platforms like LinkedIn face an inefficient, repetitive workflow:
- Target Identification: Locate the target contact (receiver) at a desired organization.
- Context Extraction: Manually inspect their profile to identify their exact name, current role, domain focus, and company.
- Candidate Positioning: Select relevant experiences and skills from one's own resume/profile that align with the open position.
- Drafting Under Constraints: Synthesize these disparate inputs into a polite, professional outreach message that remains short enough (< 100 words) for high read/response rates.
Executing this process manually across dozens of prospective referrers creates cognitive overhead and inconsistent outreach quality.
The Solution: Agentic Workflow Automation
An agentic workflow automates profile parsing, structured entity extraction, context matching, and message generation. Instead of relying on a single monolithic prompt—which often fails or hallucinates when handling complex multi-step logic—we decompose the problem into modular, single-responsibility execution nodes coordinated by an orchestration graph.
2. Core Mental Model & High-Level Architecture
The referral outreach agent operates by processing two primary data streams—the Candidate Profile and the Receiver Profile—to synthesize a tailored referral pitch.
Real-Life Analogy: The Contextual Relay
Imagine a multi-stage office assembly line. The first worker fetches raw personnel folders from file cabinets. The second worker highlights only the critical attributes (names, job titles, technical skills). The final worker takes these highlighted note cards and writes a custom, polished cover letter. Each worker performs a singular specialized job, passing the updated ledger down the line.
3. Sequential Workflow vs. Parallel Execution
Agentic workflows can be structured either sequentially or in parallel depending on data dependencies between tasks.
Execution Complexity Analysis
In a strictly sequential architecture with nodes, the total execution latency is the direct summation of the individual node response times:
For our initial baseline build, a sequential layout keeps debugging straightforward and deterministic. However, because Candidate extraction and Receiver extraction are independent operations, advanced implementations can process them in parallel branches, reducing critical path latency to:
4. The Shared State Pattern in LangGraph
In LangGraph, individual nodes do not pass variables directly to one another via standard function arguments. Instead, all nodes read from and write to a single Shared State object (a state schema typically backed by a Python TypedDict).
Mental Model: The Ledger
Think of the Shared State as a central ledger bound to the workflow execution instance. When a node finishes executing, it returns a dictionary containing only the keys it updated. LangGraph merges these updates back into the primary state ledger, which is then made available to subsequent nodes downstream.
Python Implementation of State Schema
from typing import TypedDict, Optional, Dict, Any
class ReferralAgentState(TypedDict):
# Input profile pointers / links
candidate_profile_path: str
receiver_profile_path: str
# Raw extracted text content
candidate_profile_content: Optional[str]
receiver_profile_content: Optional[str]
# Structured representations extracted by LLM
candidate_profile_info: Optional[Dict[str, Any]]
receiver_profile_info: Optional[Dict[str, Any]]
# Final generated outreach deliverable
referral_pitch: Optional[str]
5. Granular Node Engineering: Step-by-Step Python Implementation
Each node in LangGraph is represented as a plain Python function that accepts the state dictionary, performs a discrete operation, and returns updated key-value pairs.
Setup and Environment Initialization
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
# Load environment variables (OPENAI_API_KEY)
load_dotenv()
# Initialize LLM backend
llm = ChatOpenAI(model="gpt-4o", temperature=0.2)
Node 1 & 2: Ingesting Profile Content
These nodes load raw profile contents from local text dumps, document stores, or web scrape URLs into the shared state.
def get_candidate_profile_content(state: ReferralAgentState) -> Dict[str, Any]:
"""Reads raw candidate profile content from source path."""
path = state.get("candidate_profile_path")
with open(path, "r", encoding="utf-8") as f:
content = f.read()
print("[LOG] Ingested candidate profile raw content.")
return {"candidate_profile_content": content}
def get_receiver_profile_content(state: ReferralAgentState) -> Dict[str, Any]:
"""Reads raw receiver profile content from source path."""
path = state.get("receiver_profile_path")
with open(path, "r", encoding="utf-8") as f:
content = f.read()
print("[LOG] Ingested receiver profile raw content.")
return {"receiver_profile_content": content}
Node 3 & 4: Extracting Structured Attributes
Raw profile text contains noise (formatting artifacts, extended career histories, administrative metadata). Structured extraction nodes use targeted LLM prompts to isolate only the required entities.
def extract_receiver_profile_info(state: ReferralAgentState) -> Dict[str, Any]:
"""Extracts target details: Name, Organization, Role, and Location."""
raw_content = state.get("receiver_profile_content")
system_prompt = (
"You are a helpful assistant that extracts concise structured profile details "
"from raw text content."
)
user_prompt = f"""
Extract the following details from the profile content below:
- Target Person Name
- Current Organization
- Current Role
- Location
Profile Content:
{raw_content}
"""
messages = [
SystemMessage(content=system_prompt),
HumanMessage(content=user_prompt)
]
response = llm.invoke(messages)
print("[LOG] Extracted receiver profile details.")
return {"receiver_profile_info": {"extracted_text": response.content}}
def extract_candidate_profile_info(state: ReferralAgentState) -> Dict[str, Any]:
"""Extracts candidate details: Name, Current Role, Organization, Key Skills, and Experience Summary."""
raw_content = state.get("candidate_profile_content")
system_prompt = (
"You are an expert recruiter assistant extracting key qualifications from candidate profiles."
)
user_prompt = f"""
Extract the following candidate details from the provided text:
- Candidate Name
- Current Role & Organization
- Core Technical Skills
- Years / Key Milestones of Experience
Profile Content:
{raw_content}
"""
messages = [
SystemMessage(content=system_prompt),
HumanMessage(content=user_prompt)
]
response = llm.invoke(messages)
print("[LOG] Extracted candidate profile details.")
return {"candidate_profile_info": {"extracted_text": response.content}}
Node 5: Synthesis Node (Writing the Referral Pitch)
This node synthesizes the extracted target context and candidate attributes into a focused, highly personalized referral request under strict length constraints.
def write_referral_pitch(state: ReferralAgentState) -> Dict[str, Any]:
"""Synthesizes receiver context and candidate qualifications into a referral pitch."""
receiver_info = state.get("receiver_profile_info", {}).get("extracted_text", "")
candidate_info = state.get("candidate_profile_info", {}).get("extracted_text", "")
system_prompt = (
"You are a professional outreach strategist. Your goal is to draft highly effective, "
"concise, and polite job referral request messages."
)
user_prompt = f"""
Write a professional referral pitch applying for an open position at the receiver's organization.
CRITICAL CONSTRAINTS:
1. Keep the pitch concise, professional, and strictly UNDER 100 WORDS.
2. Explicitly highlight how the candidate's skills align with the receiver's company.
3. Respectful tone; easy to read in under 30 seconds.
RECEIVER INFORMATION:
{receiver_info}
CANDIDATE INFORMATION:
{candidate_info}
"""
messages = [
SystemMessage(content=system_prompt),
HumanMessage(content=user_prompt)
]
response = llm.invoke(messages)
print("[LOG] Generated final referral outreach pitch.")
return {"referral_pitch": response.content}
6. Graph Orchestration: Connecting Nodes & Edges in LangGraph
With the shared state schema defined and all processing nodes created, we assemble the workflow using LangGraph's StateGraph.
from langgraph.graph import StateGraph, END
def build_referral_agent_graph():
# Initialize state graph with schema
workflow = StateGraph(ReferralAgentState)
# Register processing nodes
workflow.add_node("get_candidate_profile_content", get_candidate_profile_content)
workflow.add_node("get_receiver_profile_content", get_receiver_profile_content)
workflow.add_node("extract_candidate_profile_info", extract_candidate_profile_info)
workflow.add_node("extract_receiver_profile_info", extract_receiver_profile_info)
workflow.add_node("write_referral_pitch", write_referral_pitch)
# Set execution entry point
workflow.set_entry_point("get_candidate_profile_content")
# Define directed edges forming the linear graph pipeline
workflow.add_edge("get_candidate_profile_content", "get_receiver_profile_content")
workflow.add_edge("get_receiver_profile_content", "extract_candidate_profile_info")
workflow.add_edge("extract_candidate_profile_info", "extract_receiver_profile_info")
workflow.add_edge("extract_receiver_profile_info", "write_referral_pitch")
workflow.add_edge("write_referral_pitch", END)
# Compile graph into executable runnable
app = workflow.compile()
return app
7. Execution Walkthrough & Real-World Outreach Output
To run the graph, pass an initial state dictionary containing the profile file paths into app.invoke().
if __name__ == "__main__":
app = build_referral_agent_graph()
# Initial state configuration
initial_state = {
"candidate_profile_path": "data/candidate_profile.txt",
"receiver_profile_path": "data/receiver_profile.txt"
}
# Execute graph synchronously across nodes
final_output = app.invoke(initial_state)
print("\n================ FINAL REFERRAL OUTREACH PITCH ================\n")
print(final_output.get("referral_pitch"))
Generated Sample Output
Subject: Referral Opportunity at Mandera
Hi Zach,
I hope this message finds you well. I am reaching out to express my strong interest in joining Mandera. With over a decade of experience in software engineering—including designing scalable microservices at UBS and developing advanced core algorithms—I believe my expertise in Java, C++, and AI directly aligns with your team's engineering roadmap.
My experience leading cross-functional teams has prepared me to make immediate contributions under your leadership. I would be thrilled to discuss potential opportunities at Mandera.
Best regards,
John Doe
8. Tabular Synthesis: Workflow Nodes, State Schema, & Complexity
Table 1: Node Responsibilities and Shared State Dependencies
Table 2: Sequential vs. Parallel Architectural Patterns
9. Future Enhancements & Production Roadmap
- Parallelization via Fan-Out/Fan-In: Re-architect ingestion and extraction nodes into parallel graph branches to halve initial extraction latency.
- Custom State Reducers:
Incorporate
Annotated[list, operator.add]state reducers to support aggregating profile data from multiple social channels simultaneously. - Structured Outputs (Pydantic Integration):
Enforce JSON Schema compliance on extraction nodes using
ChatOpenAI.with_structured_output()to guarantee field presence (name,role,company,skills). - Automated Platform Integration: Connect output nodes directly to browser automation libraries (e.g., Playwright or LinkedIn API integrations) to auto-fill outreach text boxes.
Summary and Key Takeaways
- Modular Agentic Architecture: Breaking down complex tasks into single-responsibility nodes in LangGraph yields significantly higher reliability than relying on single, long prompts.
- Shared State Pattern: Nodes communicate exclusively through a shared ledger (
TypedDict), returning dict updates that LangGraph automatically merges into the global workflow state. - Entity Extraction Layer: Separating raw data ingestion from entity extraction removes noise from LLM contexts, keeping downstream synthesis prompts clean and hyper-focused.
- Strict Output Guardrails: Setting explicit constraints in the synthesis node prompt (such as a strict 100-word limit) prevents verbose outreach, keeping messages crisp and engaging.
- Direct Graph Compilation: Connecting nodes with explicit directed edges (
add_edge) builds clear execution paths bounded byset_entry_point()andEND. - Latency Optimization: While linear sequential flows are easier to debug during initial builds, independent tasks (such as parsing candidate and target profiles) should be parallelized in production to minimize latency.