56 min readAutonomous agents

Implementation Guide: Source passive candidates on linkedin and send personalized outreach sequences

Step-by-step implementation guide for deploying AI to source passive candidates on linkedin and send personalized outreach sequences for HR & Staffing clients.

Hardware Procurement

Cloud Virtual Machine for n8n Orchestration

Amazon Web Services (AWS)t3.medium EC2 instance (2 vCPU, 4 GB RAM, 50 GB gp3 EBS)Qty: 1

$35–$55/month MSP cost / $75–$100/month suggested resale (bundled into managed service)

Hosts the self-hosted n8n workflow orchestration engine that coordinates the autonomous agent pipeline: triggering sourcing searches, calling OpenAI for message generation, pushing candidates to ATS, and managing outreach sequences. Self-hosting provides full data control and unlimited workflow executions.

Recruiter Workstations

Dell or LenovoDell Latitude 5550 or Lenovo ThinkPad E16 Gen 6Qty: 5

$900–$1,200 per unit MSP cost / $1,100–$1,500 suggested resale (only if client needs new hardware)

Standard business laptops for recruiters to oversee AI campaigns, review candidate shortlists, approve outreach sequences, and manage pipeline in the ATS. Any modern business laptop with 16 GB RAM, Intel Core i5/i7, and Chrome browser is sufficient.

Dedicated Residential Proxy IP Block

Bright Data or SmartproxyBright Data Residential Proxy - Static Residential IPs (5-pack)Qty: 1

$12–$15 per IP/month ($60–$75/month for 5 IPs) MSP cost / bundled into managed service

Provides dedicated static residential IP addresses that match the client's geographic location for LinkedIn automation safety. Each recruiter's Expandi instance routes through a unique residential IP to minimize detection risk and avoid LinkedIn account restrictions.

Software Procurement

hireEZ Professional

hireEZper-seat SaaS, billed annuallyQty: 5-seat deployment

$199/user/month (billed annually at $2,388/user/year); 5-seat deployment = $995/month / MSP resale at $249/user/month = $1,245/month

Primary AI sourcing engine with 800M+ candidate profiles, Boolean and AI-powered search, ATS Rediscovery (resurfaces past applicants from 45+ integrated ATS systems), built-in candidate engagement analytics, and native integrations with Bullhorn, Greenhouse, Lever, and other major ATS platforms. Generates ranked candidate shortlists from natural-language job descriptions.

Expandi

Expandiper-seat SaaS, monthly or annual billingQty: 5-seat deployment

$99/seat/month (or $79/seat/month billed annually); 5-seat deployment = $395–$495/month / MSP resale at $125/seat/month = $625/month

Cloud-based LinkedIn automation platform for executing personalized outreach sequences. Handles automated connection requests, follow-up messages, InMail sequences, and profile visits with built-in safety limits, dedicated country-based IP addresses, smart inbox for response management, and webhook triggers for n8n integration.

OpenAI API (GPT-5.4 mini)

OpenAIGPT-5.4 miniQty: usage-based

~$0.15 per 1M input tokens / $0.60 per 1M output tokens; estimated $30–$80/month for 5,000–15,000 personalized messages / MSP resale bundled into service fee

Powers the AI personalization engine that generates unique, context-aware outreach messages for each candidate based on their LinkedIn profile data, work history, skills, and the specific job opportunity. GPT-5.4 mini provides the optimal cost-to-quality ratio for high-volume message generation.

n8n Self-Hosted (Community Edition)

n8n GmbHOpen-source (Apache 2.0 with Commons Clause)

Free (self-hosted); infrastructure cost covered by AWS VM above / MSP resale bundled into managed service

Visual workflow automation platform that serves as the central orchestration layer for the autonomous agent. Connects hireEZ sourcing output → OpenAI message generation → Expandi campaign creation → ATS candidate record creation → response handling. Provides a visual canvas for building, monitoring, and debugging multi-step agent workflows.

Clay

ClaySaaS with credit-based enrichment

Pro plan at $349/month (includes enrichment credits at ~$1.20/lead for full enrichment) / MSP resale at $425/month

Waterfall data enrichment platform that finds verified personal email addresses, phone numbers, and company data for sourced candidates. Aggregates data from 75+ providers to maximize hit rates. Feeds enriched contact data into outreach sequences so candidates can be reached via both LinkedIn and email channels.

LinkedIn Recruiter Lite

LinkedIn (Microsoft)per-seat SaaS, billed annuallyQty: 5-seat deployment

$170/month per seat ($1,680/year); for 2–5 seats: $270/month per seat ($2,670/year); 5-seat deployment = $1,350/month / MSP resale at $310/seat/month = $1,550/month

Provides advanced candidate search with access to 1st, 2nd, and 3rd degree connections, 30 InMails per month per seat, saved searches, and project folders. Required as the foundational LinkedIn access layer that feeds candidate profiles into the sourcing pipeline. Also provides legitimate platform access that reduces compliance risk versus scraping.

Google Workspace (dedicated sending domain)

GoogleBusiness StarterQty: 5 mailboxes

$7.20/user/month; 5 mailboxes = $36/month / MSP resale at standard managed email rates

Dedicated email accounts on a sourcing-specific subdomain (e.g., talent.clientfirm.com) for outbound recruiting sequences. Isolated from the client's primary domain to protect main domain reputation. Google Workspace provides reliable deliverability and OAuth integration with outreach tools.

Bullhorn ATS/CRM (or existing ATS)

Bullhornper-seat SaaS (assumes client has existing subscription)

Client's existing subscription; no additional MSP procurement typically needed. If new: ~$99–$199/user/month

The client's system of record for all candidate data, job orders, submissions, and placements. All sourced candidates, enriched data, and outreach activity sync bidirectionally with Bullhorn via REST API or native hireEZ/Expandi integrations. If client uses Greenhouse, Lever, or JazzHR instead, the integration approach adapts accordingly.

Prerequisites

  • Client must have an active ATS subscription (Bullhorn, Greenhouse, Lever, or equivalent) with API access enabled and admin credentials available for integration configuration
  • Each recruiter must have a LinkedIn account that is at least 6 months old with 500+ connections — new or thin profiles will be flagged by LinkedIn's automation detection systems
  • Client must own a primary domain with DNS management access (e.g., GoDaddy, Cloudflare, Route 53) to configure a dedicated outreach subdomain and email authentication records
  • Client must provide detailed job descriptions or intake forms for at least 3–5 active roles to use as initial test campaigns during implementation
  • Client must designate a compliance stakeholder (internal counsel or HR leadership) to approve candidate notice language, data retention policies, and bias audit requirements based on jurisdictions where they recruit
  • Stable business internet connection (25+ Mbps) at all recruiter locations; VPN configurations must allow direct HTTPS access to cloud platforms without aggressive packet inspection
  • Client must provide a signed data processing agreement (DPA) template or approve the MSP-provided DPA for handling candidate PII across all integrated platforms
  • AWS account with billing configured (MSP-managed or client-owned) for hosting the n8n orchestration server
  • Client credit card or procurement approval for all SaaS subscriptions listed in software procurement — MSP will procure and manage on client's behalf where possible
  • Chrome browser (latest stable version) installed on all recruiter workstations; no conflicting LinkedIn browser extensions (e.g., older versions of LinkedIn Helper or Dux-Soup) installed

Installation Steps

Step 1: Provision Dedicated Outreach Email Infrastructure

Set up a dedicated subdomain and email accounts specifically for recruiting outreach. This isolates outreach reputation from the client's primary business email domain, protecting deliverability for regular business communications. We create a subdomain like talent.clientfirm.com, provision Google Workspace mailboxes for each recruiter, and configure all DNS authentication records.

1
Register subdomain in client's DNS provider (example: Cloudflare)
2
Add SPF Record — Type: TXT, Name: talent, Value: v=spf1 include:_spf.google.com ~all
3
Add DKIM Record (obtain from Google Workspace Admin > Apps > Gmail > Authenticate Email) — Type: TXT, Name: google._domainkey.talent, Value: [Google-provided DKIM key]
4
Add DMARC Record — Type: TXT, Name: _dmarc.talent, Value: v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@clientfirm.com; pct=100
5
Add MX Records for talent.clientfirm.com — Priority 1: ASPMX.L.GOOGLE.COM, Priority 5: ALT1.ASPMX.L.GOOGLE.COM, Priority 5: ALT2.ASPMX.L.GOOGLE.COM, Priority 10: ALT3.ASPMX.L.GOOGLE.COM, Priority 10: ALT4.ASPMX.L.GOOGLE.COM
6
Create Google Workspace accounts: recruiter1@talent.clientfirm.com, recruiter2@talent.clientfirm.com (one per recruiter, up to seat count)
Verify DNS propagation for talent.clientfirm.com
bash
dig TXT talent.clientfirm.com
dig TXT _dmarc.talent.clientfirm.com
dig MX talent.clientfirm.com
Note

DNS propagation can take 24–48 hours. Start this step first as the email warm-up period (Step 7) cannot begin until DNS is fully propagated and verified by Google. Use MXToolbox.com to validate all records. Do NOT use the client's primary domain for automated outreach — domain reputation damage is extremely difficult to reverse.

Step 2: Deploy n8n Orchestration Server on AWS

Provision and configure the self-hosted n8n instance that will serve as the central nervous system of the autonomous agent. This server orchestrates all workflows: triggering sourcing, calling the OpenAI API, managing candidate data flow between platforms, and handling webhook events from outreach tools.

1
Launch EC2 instance via AWS Console or CLI
2
SSH into the instance
3
Install Docker and Docker Compose
4
Create n8n directory structure
5
Create docker-compose.yml
6
Start n8n
7
Verify n8n is running
Step 1: Launch EC2 instance via AWS Console or CLI
bash
aws ec2 run-instances \
  --image-id ami-0c7217cdde317cfec \
  --instance-type t3.medium \
  --key-name msp-client-keypair \
  --security-group-ids sg-xxxxxxxxx \
  --subnet-id subnet-xxxxxxxxx \
  --block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":50,"VolumeType":"gp3"}}]' \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=n8n-recruiting-agent},{Key=Client,Value=clientfirm}]'
Step 2: SSH into the instance
bash
ssh -i msp-client-keypair.pem ubuntu@<EC2-PUBLIC-IP>
Step 3: Install Docker and Docker Compose
bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y docker.io docker-compose-v2
sudo systemctl enable docker
sudo usermod -aG docker ubuntu
newgrp docker
Step 4: Create n8n directory structure
bash
mkdir -p ~/n8n-recruiting && cd ~/n8n-recruiting
mkdir -p .n8n
Step 5: Create docker-compose.yml
yaml
version: '3.8'
services:
  n8n:
    image: n8nio/n8n:latest
    restart: always
    ports:
      - '5678:5678'
    environment:
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=mspadmin
      - N8N_BASIC_AUTH_PASSWORD=<GENERATE-STRONG-PASSWORD>
      - N8N_HOST=n8n.clientfirm.com
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://n8n.clientfirm.com/
      - N8N_ENCRYPTION_KEY=<GENERATE-32-CHAR-KEY>
      - GENERIC_TIMEZONE=America/New_York
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_USER=n8n
      - DB_POSTGRESDB_PASSWORD=<GENERATE-DB-PASSWORD>
    volumes:
      - ./.n8n:/home/node/.n8n
    depends_on:
      - postgres
  postgres:
    image: postgres:15-alpine
    restart: always
    environment:
      - POSTGRES_USER=n8n
      - POSTGRES_PASSWORD=<GENERATE-DB-PASSWORD>
      - POSTGRES_DB=n8n
    volumes:
      - postgres_data:/var/lib/postgresql/data
volumes:
  postgres_data:
Step 6: Start n8n
bash
docker compose up -d
Step 7: Verify n8n is running
bash
docker compose logs -f n8n
Note

Replace all placeholder passwords with strong generated values (use openssl rand -hex 16). Set up an Elastic IP and associate it with the EC2 instance for a stable public IP. Configure a DNS A record for n8n.clientfirm.com pointing to the Elastic IP. For production, place an Nginx reverse proxy or AWS ALB in front with Let's Encrypt SSL. Security group must allow inbound 443 (HTTPS) and restrict SSH (22) to MSP office IPs only. Set up AWS CloudWatch alarms for CPU and disk usage.

Step 3: Configure SSL and Reverse Proxy for n8n

Set up Nginx as a reverse proxy with automatic Let's Encrypt SSL certificates to secure the n8n web interface and webhook endpoints. This is required because Expandi webhooks and API callbacks require HTTPS endpoints.

1
Install Nginx and Certbot
2
Create Nginx config
3
Enable site and get SSL certificate
4
Set up auto-renewal
5
Verify HTTPS access
Install Nginx and Certbot
bash
sudo apt install -y nginx certbot python3-certbot-nginx
Create Nginx config
bash
sudo cat > /etc/nginx/sites-available/n8n << 'EOF'
server {
    listen 80;
    server_name n8n.clientfirm.com;

    location / {
        proxy_pass http://localhost:5678;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
        chunked_transfer_encoding off;
        proxy_buffering off;
    }
}
EOF
Enable site and obtain SSL certificate
bash
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d n8n.clientfirm.com --non-interactive --agree-tos -m msp-admin@mspfirm.com
Set up auto-renewal
bash
sudo systemctl enable certbot.timer
Verify HTTPS access
bash
curl -I https://n8n.clientfirm.com
Note

Ensure the DNS A record for n8n.clientfirm.com is propagated before running certbot. The SSL certificate auto-renews every 90 days via the certbot timer. If using AWS ALB instead of Nginx, use ACM certificates and skip this step.

Step 4: Provision and Configure hireEZ Professional Accounts

Set up the hireEZ AI sourcing platform with recruiter seats, configure the ATS integration (Bullhorn or client's ATS), and enable ATS Rediscovery to immediately surface existing candidates from the client's database who match new roles. This provides instant ROI from day one.

1
Navigate to https://app.hireez.com and sign up for Professional plan
2
Add recruiter user accounts (one per seat purchased)
3
Navigate to Settings > Integrations
4
Select the client's ATS (e.g., Bullhorn)
5
Enter ATS API credentials: - Bullhorn: Client ID, Client Secret, Username, Password - Greenhouse: API Key (with Harvest API permissions) - Lever: API Key (with full read/write access)
6
Enable bidirectional sync: - Push: Export sourced candidates to ATS - Pull: Import existing candidates for ATS Rediscovery
7
Configure ATS Rediscovery: - Navigate to ATS Rediscovery tab - Select all job categories to index - Run initial sync (may take 2–24 hours depending on DB size)
8
Set up team workspace and shared projects for collaboration
Note

hireEZ's ATS Rediscovery feature will scan the client's entire existing candidate database and re-rank candidates against current open roles. This often surfaces 50–200 immediately relevant candidates on day one — use this as an early win to demonstrate ROI. Request a dedicated Customer Success Manager from hireEZ during onboarding. The initial ATS sync for large databases (50K+ records) may take up to 24 hours.

Step 5: Set Up Expandi LinkedIn Automation Accounts

Configure Expandi cloud-based LinkedIn automation for each recruiter. This includes connecting LinkedIn accounts, assigning dedicated residential IPs, setting conservative daily limits for the warm-up period, and configuring webhook integration with n8n for autonomous agent orchestration.

1
Create Expandi account and log in at https://app.expandi.io
2
Connect LinkedIn account: Enter LinkedIn credentials, complete 2FA verification. Expandi will assign a dedicated IP matching the recruiter's country.
3
Configure Safety Settings (CRITICAL - warm-up period): Connection requests per day: START at 10 (increase by 5/week to max 30). Messages per day: START at 20 (increase by 10/week to max 60). Profile views per day: START at 40 (increase by 10/week to max 100). Active hours: Match recruiter's working hours (e.g., 8 AM - 6 PM ET). Enable random delays between actions (30–120 seconds).
4
Configure Webhook for n8n integration: Go to Settings > Integrations > Webhooks. Add webhook URL: https://n8n.clientfirm.com/webhook/expandi-events. Enable events: connection_accepted, message_replied, message_sent.
5
Set up Smart Inbox: Enable auto-categorization of replies (interested, not interested, out of office).
6
Create campaign templates (configured in Step 9).
Expandi webhook configuration values for n8n integration
text
Webhook URL: https://n8n.clientfirm.com/webhook/expandi-events
Enabled events: connection_accepted, message_replied, message_sent
Critical

THE MOST CRITICAL STEP: LinkedIn warm-up takes 2-3 weeks. Do NOT skip this or accelerate the ramp-up schedule. LinkedIn accounts that suddenly start sending high volumes of connection requests will be flagged and potentially restricted. Start each account at the minimum limits listed above and increase gradually. If any account receives a LinkedIn warning, immediately reduce limits by 50% and pause for 48 hours. Each recruiter should also manually engage (post, like, comment) on LinkedIn during the warm-up period to establish organic activity patterns alongside the automated activity.

Step 6: Configure Clay Data Enrichment Workspace

Set up Clay to enrich sourced candidate profiles with verified personal email addresses and phone numbers. This enables multi-channel outreach (LinkedIn + email) which dramatically increases response rates. Clay's waterfall enrichment queries 75+ data providers to maximize the hit rate for contact information.

1
Create Clay workspace for client
2
Navigate to Settings > API Keys
3
Generate API key for n8n integration — Copy the API key: clay_api_XXXXXXXXXXXXXXXX
4
Create an Enrichment Table: Name: 'Candidate Enrichment Pipeline' — Columns: full_name, linkedin_url, current_company, current_title, job_id
5
Add Enrichment Columns: 'Find Work Email' (uses waterfall: Hunter > Apollo > Snov.io > RocketReach), 'Find Personal Email' (uses waterfall: ContactOut > Lusha > SignalHire), 'Find Phone Number' (uses waterfall: Lusha > Apollo > ContactOut), 'Company Enrichment' (Clearbit/Apollo - gets company size, industry, funding)
6
Configure enrichment credits budget: Set monthly credit limit to prevent overspend — Full enrichment per candidate: ~75 credits on Pro plan (~$1.20/lead)
7
Enable Clay API access for n8n: Table API endpoint will be used in n8n workflows to push candidates and retrieve enriched data
Note

Clay Pro plan includes sufficient credits for ~290 full enrichments per month at $349/month. If the client needs higher volume, upgrade to the Team plan. Monitor credit consumption weekly during the first month to calibrate. Waterfall enrichment means Clay tries the cheapest provider first and cascades through more expensive ones only if no result is found, optimizing cost per successful enrichment.

Step 7: Warm Up Outreach Email Accounts

Begin the critical 2–4 week email warm-up process for the dedicated sending mailboxes. This gradually builds sender reputation with email providers so that outreach messages land in primary inboxes rather than spam folders. Use an automated warm-up service to simulate natural email conversations.

  • Option A: Use Expandi's built-in email warm-up — In Expandi > Settings > Email > Warm-up
  • Connect each recruiter's talent.clientfirm.com Google Workspace account via OAuth
  • Enable warm-up with these settings: Daily warm-up emails: Start at 5, increase to 40 over 2 weeks | Reply rate: 30–40% | Warm-up duration: 14–21 days minimum
1
Option B: Use dedicated warm-up service (Instantly.ai or Warmbox.ai) — if Expandi warm-up is insufficient
2
Sign up at https://instantly.ai ($30/month)
3
Connect Google Workspace accounts via OAuth
4
Configure warm-up: Increase per day: 3 emails | Daily limit: 40 emails | Reply rate: 35%
1
Verify deliverability after warm-up period: Go to https://mail-tester.com
2
Send a test email from each recruiter mailbox to the provided address
3
Target score: 9/10 or higher
1
Verify with Google Postmaster Tools: https://postmaster.google.com
2
Register talent.clientfirm.com domain
3
Monitor domain reputation (target: High)
Note

Email warm-up MUST run for a minimum of 14 days before sending any real outreach. During warm-up, do not send any manual cold emails from these accounts. After warm-up is complete, limit real outreach volume to 50 emails per mailbox per day maximum to maintain deliverability. If bounce rate exceeds 3% or spam complaint rate exceeds 0.1%, immediately pause campaigns and investigate list quality.

Step 8: Configure OpenAI API Access and Test Message Generation

Set up the OpenAI API account, configure API keys, set usage limits, and test the message personalization prompts that will power the autonomous outreach agent. The system uses GPT-5.4 mini for cost-effective high-volume message generation with high quality.

1
Create OpenAI API account at https://platform.openai.com
2
Navigate to API Keys > Create new secret key — Name: 'n8n-recruiting-agent-clientfirm' — Copy key: sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3
Set usage limits: Navigate to Settings > Limits — Set monthly budget: $100 (adjust based on volume) — Set notification threshold: $75
4
Test API access from n8n server:
SSH into n8n server
bash
ssh -i msp-client-keypair.pem ubuntu@<EC2-PUBLIC-IP>
Test OpenAI API
bash
# expected output is a personalized, natural-sounding connection request
# message. Verify the response includes candidate-specific details.

curl https://api.openai.com/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' \
  -d '{
    "model": "gpt-5.4-mini",
    "messages": [
      {
        "role": "system",
        "content": "You are a recruiting outreach specialist. Generate a personalized LinkedIn connection request message (under 300 characters) for a passive candidate."
      },
      {
        "role": "user",
        "content": "Candidate: Jane Smith, Senior Software Engineer at Google for 3 years. Previously at Meta. Stanford CS grad. Role: Staff Engineer at Series B fintech startup in NYC. Must mention: equity package and engineering culture."
      }
    ],
    "temperature": 0.8,
    "max_tokens": 200
  }'
Note

GPT-5.4 mini is recommended over GPT-5.4 for this use case because outreach messages are short (50–300 words) and the cost difference is ~15x while quality is comparable for this task. At ~$0.15/1M input tokens and ~$0.60/1M output tokens, generating 10,000 personalized messages costs approximately $5–10. Set temperature to 0.7–0.9 for natural variation across messages. Always include a system prompt that enforces compliance (no false claims, no discriminatory language, includes opt-out mention in email sequences).

Step 9: Build Core n8n Autonomous Agent Workflows

Import and configure the four core n8n workflows that comprise the autonomous recruiting agent: (1) Candidate Sourcing Trigger, (2) Profile Enrichment Pipeline, (3) Message Personalization Engine, and (4) Outreach Campaign Orchestrator. These workflows connect hireEZ, Clay, OpenAI, Expandi, and the ATS into a seamless autonomous pipeline.

1
Log into n8n at https://n8n.clientfirm.com
2
Navigate to Credentials and add the following: - OpenAI API: Type 'OpenAI', paste API key - Clay API: Type 'HTTP Header Auth', header name 'Authorization', value 'Bearer clay_api_XXX' - Expandi API: Type 'HTTP Header Auth', header name 'Authorization', value 'Bearer expandi_api_XXX' - Bullhorn API: Type 'OAuth2', configure with client_id, client_secret, and token URLs - Google Workspace: Type 'Gmail OAuth2', connect via OAuth flow
3
Import the four workflow JSON files (provided in custom_ai_components section)
4
Configure each workflow with client-specific parameters: - Company name, brand voice guidelines - Target job roles and sourcing criteria - Daily volume limits per recruiter - Business hours for outreach sending
5
Enable workflows one at a time, starting with the Sourcing Trigger
6
Test end-to-end flow with a single candidate before enabling full automation
Note

The workflows are designed with human-in-the-loop gates: the sourcing workflow outputs a candidate shortlist that a recruiter must approve in Bullhorn/ATS before the outreach orchestrator picks them up. This satisfies compliance requirements and ensures quality. All workflows should be tested individually before connecting them. Use n8n's execution log to debug any issues. Set up n8n error notifications via Slack or email webhook.

Step 10: Configure ATS Integration and Candidate Sync

Establish bidirectional data flow between the AI sourcing stack and the client's ATS (Bullhorn, Greenhouse, or Lever). Sourced candidates are automatically created as records in the ATS with source attribution, and recruiter actions in the ATS (e.g., marking a candidate as 'Approved for Outreach') trigger the outreach pipeline.

1
Obtain Bullhorn API credentials from client's Bullhorn admin: API Client ID, API Client Secret, Bullhorn username/password for OAuth, Bullhorn REST API base URL
2
Authenticate and get access token (test from n8n server)
3
Exchange auth code for access/refresh tokens
4
Get REST token
5
Create custom fields in Bullhorn for AI sourcing metadata: 'ai_source_platform', 'ai_personalization_notes', 'outreach_status' (values: pending_review, approved, in_sequence, responded, not_interested)
6
Configure hireEZ native Bullhorn integration: In hireEZ go to Settings > Integrations > Bullhorn, enter the same API credentials, map fields (hireEZ profile → Bullhorn Candidate record), and enable auto-export of shortlisted candidates
7
For Greenhouse: Use Harvest API key with candidate create/update permissions
8
For Lever: Use API key with full candidate read/write access
Bullhorn ATS — Authenticate and obtain access token
bash
curl -X POST 'https://auth.bullhornstaffing.com/oauth/authorize' \
  -d 'client_id=<CLIENT_ID>&response_type=code&username=<USERNAME>&password=<PASSWORD>&action=Login'
Bullhorn ATS — Exchange auth code for access/refresh tokens
bash
curl -X POST 'https://auth.bullhornstaffing.com/oauth/token' \
  -d 'grant_type=authorization_code&code=<AUTH_CODE>&client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>'
Bullhorn ATS — Get REST token
bash
curl -X GET 'https://rest.bullhornstaffing.com/rest-services/login?version=2.0&access_token=<ACCESS_TOKEN>'
Note

Bullhorn's REST API requires periodic token refresh (tokens expire after ~10 minutes of inactivity). The n8n workflow includes automatic token refresh logic. Custom fields in Bullhorn are critical for tracking which candidates came from AI sourcing and their outreach status. Work with the client's Bullhorn admin (or Bullhorn support) to add custom fields if needed. The hireEZ native integration handles most of the heavy lifting for candidate export; the n8n workflows handle the outreach status updates and response tracking.

Step 11: Build and Deploy Outreach Campaign Templates

Create the multi-step outreach sequence templates in Expandi that the autonomous agent will use. Each sequence includes a connection request, 2–3 follow-up messages, and an optional email fallback. Messages are personalized by the OpenAI engine but follow a proven structural framework.

  • In Expandi, create campaign templates:
  • Template 1: 'AI Sourced - Technical Roles'
  • Step 1 (Day 0): Connection Request with note (300 char limit) — Personalized by OpenAI, placeholder: {{ai_connection_message}}
  • Step 2 (Day 2): If connected, send Message 1 — Placeholder: {{ai_followup_1}} - detailed role pitch
  • Step 3 (Day 5): If no reply, send Message 2 — Placeholder: {{ai_followup_2}} - social proof / team info
  • Step 4 (Day 9): If no reply, send Message 3 (final) — Placeholder: {{ai_followup_3}} - soft close / alternative ask
  • Template 2: 'AI Sourced - Executive/Leadership Roles'
  • Step 1 (Day 0): Profile View
  • Step 2 (Day 1): Connection Request (more formal tone)
  • Step 3 (Day 3): Message 1 - executive-focused pitch
  • Step 4 (Day 7): Message 2 - market insights angle
  • Step 5 (Day 12): Message 3 - referral ask
  • Template 3: 'Email Fallback' (for candidates not accepting LinkedIn) — Triggered via n8n after 14 days with no LinkedIn connection
  • Step 1: Email via Google Workspace - role introduction
  • Step 2 (Day 3): Follow-up email - case study/social proof
  • Step 3 (Day 7): Final email - soft close
  • Configure Expandi campaign settings:
  • Send connection requests only during business hours
  • Random delay between actions: 45–180 seconds
  • Auto-withdraw pending connection requests after 21 days
  • Tag responses: interested, not_interested, referral, out_of_office
Note

LinkedIn connection request notes are limited to 300 characters — the OpenAI prompt must enforce this limit strictly. Follow-up messages can be longer (up to ~8,000 characters on LinkedIn) but keep them under 200 words for optimal response rates. The 3-touch sequence with 3–5 day gaps between messages is the industry standard — more aggressive cadences increase unsubscribe/report rates. Always include an opt-out line in the final message: 'If this isn't relevant, just let me know and I won't reach out again.'

Step 12: Implement Compliance and Human-in-the-Loop Controls

Configure the mandatory compliance layer including candidate notification of AI use, human review gates, data retention policies, opt-out handling, and bias audit infrastructure. This is non-negotiable for legal compliance with NYC LL 144, Illinois HB 3773, Colorado SB24-205, GDPR, and federal anti-discrimination law.

1
Create compliance notification template in ATS: Add to every first outreach message (LinkedIn or email): 'We use AI-assisted tools to identify candidates who may be a great fit for roles we're filling. You can learn more about how we use these tools at [client-website]/ai-recruiting-notice'
2
Create the AI Recruiting Notice webpage: Host at client's website: clientfirm.com/ai-recruiting-notice. Content must include: a statement that AI tools are used to source and screen candidates; names of AI tools used (hireEZ, etc.); what data is collected and how it's used; candidate rights (access, correction, deletion, opt-out); contact email for data requests: privacy@clientfirm.com
3
Configure n8n workflow for opt-out handling: When a candidate replies with opt-out language (detected by keyword matching), automatically stop all sequences, update ATS status to 'opted_out', add to global suppression list in Expandi and email tools, and trigger confirmation message: 'You've been removed from future outreach'
4
Set up data retention policy in ATS: Candidate data from sourcing — retain for 12 months max. GDPR candidates (EU sourcing) — contact within 30 days or delete. Create automated purge workflow in n8n (runs monthly)
5
Configure human review gate: In the n8n pipeline, after AI generates a shortlist, candidates are pushed to ATS with status 'pending_review'. Recruiter must change status to 'approved' before outreach triggers. No outreach fires without explicit human approval
Note

For clients recruiting in NYC: an annual bias audit by an independent auditor is REQUIRED under Local Law 144 if the AI tools are used as an Automated Employment Decision Tool (AEDT). Budget $5,000–$15,000 for a third-party bias audit. Recommended auditors: BLDS (Bayard), Holistic AI, or DCI Consulting. The audit examines selection rates across demographic groups. Even if the client doesn't recruit in NYC today, implementing audit-ready practices now is strongly recommended given the rapid expansion of state-level AI hiring laws. Document everything — audit trails are your best defense.

Step 13: End-to-End Integration Testing and Go-Live

Conduct thorough end-to-end testing of the complete autonomous agent pipeline with real candidate profiles and controlled test campaigns. Verify every integration point, data flow accuracy, message quality, compliance controls, and ATS synchronization before enabling production campaigns.

Testing Protocol: 6-Category End-to-End Test Suite

1
Test 1: Sourcing Pipeline - Create a test job in hireEZ for an active open role - Run AI search and verify candidate results quality - Export top 10 candidates to ATS - Verify candidate records appear correctly in Bullhorn/Greenhouse Test 2: Enrichment Pipeline - Take the 10 test candidates - Run through Clay enrichment via n8n workflow - Verify: email found rate > 60%, phone found rate > 40% - Verify enriched data syncs back to ATS candidate records Test 3: Message Personalization - Run OpenAI personalization for all 10 candidates - Review each message for: accuracy, tone, length compliance (300 char for connection requests) - Check for hallucinations (false claims about candidate's background) - Verify compliance notice is included Test 4: Outreach Execution (use recruiter's own LinkedIn as test target) - Create a test campaign in Expandi with 1-2 internal team members as targets - Verify connection request sends correctly with personalized note - Verify follow-up messages trigger on correct schedule - Verify webhook fires to n8n when message is sent/replied Test 5: Human-in-the-Loop Gate - Verify candidates in ATS with status 'pending_review' do NOT trigger outreach - Change status to 'approved' and verify outreach triggers within expected window - Test opt-out: reply with 'please remove me' and verify auto-suppression Test 6: Compliance Verification - Verify AI notice URL is live and accessible - Verify opt-out handling works end-to-end - Verify data retention workflow runs correctly (test with dummy records) - Document all test results in compliance log
Note

Run testing over 5–7 business days to verify time-delayed sequence steps fire correctly. Have a recruiter review every AI-generated message during testing — tune the OpenAI prompt based on feedback (too formal? too casual? missing key selling points?). Do not go live until all 6 test categories pass. Keep a testing log with screenshots for the compliance record.

Custom AI Components

Candidate Sourcing Trigger Workflow

Type: workflow

n8n workflow that triggers daily at a configured time, checks for new approved job requisitions in the ATS that need sourcing, calls the hireEZ API to initiate AI-powered candidate searches based on the job description and requirements, and processes the resulting candidate shortlists into the enrichment pipeline. Includes deduplication logic to avoid re-sourcing existing candidates.

Implementation

1
TRIGGER NODE: Schedule Trigger — Type: scheduleTrigger — Configuration: Run daily at 7:00 AM ET (Monday–Friday)
2
NODE: Fetch Open Requisitions from ATS — Type: httpRequest — Method: GET — Authentication: HTTP Header Auth (Bullhorn REST token) — On error: Continue on fail, log error
3
NODE: Filter Active Jobs — Type: filter — Condition: Only process jobs where dateAdded > (now - 30 days) AND isOpen = true
4
NODE: Loop Over Each Job — Type: splitInBatches — Batch Size: 1
5
NODE: Build hireEZ Search Query — Type: code (JavaScript) — See code block below
6
NODE: Call hireEZ API - Search Candidates — Type: httpRequest — Method: POST — URL: https://api.hireez.com/v1/search — Body: {{$json}} from previous node — Authentication: HTTP Header Auth (hireEZ API key) — Retry on fail: 3 times with 5s delay
7
NODE: Deduplicate Against Existing ATS Records — Type: code (JavaScript) — See code block below
8
NODE: Create Candidate Records in ATS (status: pending_review) — Type: httpRequest (in loop) — Method: PUT — URL: {{bullhorn_rest_url}}/entity/Candidate — Body: Map hireEZ fields to Bullhorn fields, set customText1='ai_sourced', status='pending_review', source='hireEZ'
9
NODE: Push to Enrichment Queue — Type: httpRequest — Method: POST — URL: https://n8n.clientfirm.com/webhook/enrichment-pipeline — Body: Array of candidate objects with linkedin_url, full_name, job_id
10
NODE: Send Summary Notification — Type: slack (or email) — Message: 'Sourcing complete for [job_title]: [X] new candidates found, [Y] deduplicated, [Z] pushed to review queue in [ATS]'

Node 2 — ATS Fetch URL

GET request URL for fetching open requisitions from Bullhorn ATS
text
{{bullhorn_rest_url}}/search/JobOrder?query=status:Open AND customText1:needs_sourcing&fields=id,title,description,skills,categories&count=20

Node 5 — Build hireEZ Search Query (JavaScript)

n8n Code node
javascript
// builds hireEZ API search payload from Bullhorn job record

const job = $input.first().json;
const searchPayload = {
  query: job.title,
  skills: job.skills ? job.skills.split(',').map(s => s.trim()) : [],
  location: job.address ? job.address.city + ', ' + job.address.state : '',
  experience_min: 3,
  experience_max: 15,
  limit: 50,
  exclude_current_company: job.clientCorporation ? job.clientCorporation.name : '',
  source: 'linkedin'
};
return [{ json: { ...searchPayload, job_id: job.id, job_title: job.title } }];

Node 7 — Deduplicate Against Existing ATS Records (JavaScript)

n8n Code node
javascript
// checks each hireEZ candidate against Bullhorn ATS by LinkedIn URL to
// prevent duplicate records

const candidates = $input.first().json.results || [];
const deduped = [];
for (const candidate of candidates) {
  // Check if LinkedIn URL already exists in ATS
  const checkUrl = `${$credentials.bullhorn_rest_url}/search/Candidate?query=customText5:"${encodeURIComponent(candidate.linkedin_url)}"&fields=id&count=1`;
  try {
    const response = await this.helpers.httpRequest({
      method: 'GET',
      url: checkUrl,
      headers: { 'BhRestToken': $credentials.bullhorn_token }
    });
    if (response.count === 0) {
      deduped.push(candidate);
    }
  } catch (e) {
    deduped.push(candidate); // Include if check fails
  }
}
return deduped.map(c => ({ json: c }));
Note

Error Handling: Each API call node has retry logic (3 attempts, exponential backoff). Failed jobs are logged and a Slack notification is sent to the MSP monitoring channel.

Profile Enrichment Pipeline Workflow

Type: workflow

n8n workflow triggered by webhook from the Sourcing Trigger. Takes candidate LinkedIn URLs and basic profile data, sends them to Clay for waterfall enrichment (personal email, work email, phone number, company data), waits for enrichment completion, and updates the candidate records in the ATS with enriched contact information.

Implementation

1
TRIGGER NODE: Webhook — Type: webhook | Path: /enrichment-pipeline | Method: POST | Accepts: Array of candidate objects
2
NODE: Split into Individual Candidates — Type: splitInBatches | Batch Size: 5 (Clay rate limit friendly)
3
NODE: Push to Clay Table via API — Type: httpRequest | Method: POST | URL: https://api.clay.com/v1/tables/{{clay_table_id}}/rows | Headers: Authorization: Bearer {{clay_api_key}}, Content-Type: application/json
4
NODE: Wait for Enrichment — Type: wait | Duration: 60 seconds (Clay enrichment typically completes in 15-45 seconds)
5
NODE: Fetch Enriched Data from Clay — Type: httpRequest | Method: GET | URL: https://api.clay.com/v1/tables/{{clay_table_id}}/rows?filter=enrichment_status:complete&sort=-created_at&limit=50 | Headers: Authorization: Bearer {{clay_api_key}}
6
NODE: Process Enrichment Results — Type: code (JavaScript)
7
NODE: Update ATS Candidate Records — Type: httpRequest (in loop) | Method: POST | URL: {{bullhorn_rest_url}}/entity/Candidate/{{candidate_id}} | Body: Update email, phone, custom fields with enrichment data
8
NODE: Route High-Quality Candidates to Personalization — Type: if | Condition: enrichment_score >= 1 (at least one contact method found) | True: Send to Message Personalization webhook | False: Log as 'enrichment_incomplete' — recruiter can manually source contact info
9
NODE: Trigger Personalization Pipeline — Type: httpRequest | Method: POST | URL: https://n8n.clientfirm.com/webhook/personalization-engine | Body: Enriched candidate data array
Node 3 — Push to Clay Table: Request body
json
{
  "rows": [
    {
      "full_name": "{{$json.full_name}}",
      "linkedin_url": "{{$json.linkedin_url}}",
      "current_company": "{{$json.current_company}}",
      "current_title": "{{$json.current_title}}",
      "job_id": "{{$json.job_id}}"
    }
  ]
}
Node 6 — Process Enrichment Results: JavaScript code
javascript
const rows = $input.first().json.rows || [];
const enriched = rows.map(row => ({
  linkedin_url: row.linkedin_url,
  personal_email: row.find_personal_email || null,
  work_email: row.find_work_email || null,
  phone: row.find_phone || null,
  company_size: row.company_enrichment?.employee_count || null,
  company_industry: row.company_enrichment?.industry || null,
  enrichment_score: [
    row.find_personal_email ? 1 : 0,
    row.find_work_email ? 1 : 0,
    row.find_phone ? 1 : 0
  ].reduce((a, b) => a + b, 0),
  job_id: row.job_id
}));
return enriched.map(e => ({ json: e }));
Note

Rate Limiting: The splitInBatches node with batch size 5 and 2-second delays between batches prevents Clay API rate limit errors. Monitor Clay credit consumption in the summary notification.

AI Message Personalization Engine

Type: workflow

n8n workflow that takes enriched candidate profiles, constructs detailed context prompts for GPT-5.4 mini, generates personalized outreach messages for each step of the sequence (connection request, 3 follow-ups, email versions), validates message quality and compliance, and stores the personalized content ready for the Outreach Orchestrator to deploy.

Implementation

1
TRIGGER NODE: Webhook — Type: webhook, Path: /personalization-engine, Method: POST
2
NODE: Fetch Job Details from ATS — Type: httpRequest, Method: GET
3
NODE: Build Personalization Context — Type: code (JavaScript)
4
NODE: Generate LinkedIn Connection Request (300 char max) — Type: openAi, Model: gpt-5.4-mini, Temperature: 0.85, Max Tokens: 100
5
NODE: Generate Follow-up Message 1 (Day 2 after connection) — Type: openAi, Model: gpt-5.4-mini, Temperature: 0.8, Max Tokens: 300
6
NODE: Generate Follow-up Message 2 (Day 5) — Type: openAi, Temperature: 0.8, Max Tokens: 250
7
NODE: Generate Follow-up Message 3 - Final (Day 9) — Type: openAi, Temperature: 0.75, Max Tokens: 200
8
NODE: Generate Email Versions (if email available) — Type: if, Condition: has_email == true. True branch: Generate 3 email versions (subject line + body) via OpenAI. System prompt adds: Include physical mailing address footer, CAN-SPAM unsubscribe line.
9
NODE: Quality Validation — Type: code (JavaScript)
10
NODE: Store Personalized Messages — Type: httpRequest. Store messages in n8n's internal data or ATS custom fields. Associate with candidate record and job_id. Flag candidates with quality issues for recruiter review.
11
NODE: Notify Recruiter of Ready Candidates — Type: email or Slack. Message: '[X] candidates for [Job Title] have personalized messages ready. Review and approve in [ATS] to start outreach.'
Node 2: Fetch Job Details from ATS — HTTP Request URL
http
GET {{bullhorn_rest_url}}/entity/JobOrder/{{$json.job_id}}?fields=title,description,salary,benefits,clientCorporation
Node 3: Build Personalization Context — JavaScript
javascript
const candidate = $input.first().json;
const job = $('Fetch Job Details').first().json;

const context = {
  candidate_name: candidate.full_name,
  candidate_first_name: candidate.full_name.split(' ')[0],
  current_title: candidate.current_title,
  current_company: candidate.current_company,
  linkedin_url: candidate.linkedin_url,
  job_title: job.title,
  company_name: job.clientCorporation?.name || 'our client',
  company_industry: candidate.company_industry,
  company_size: candidate.company_size,
  key_selling_points: job.description ? job.description.substring(0, 500) : '',
  salary_range: job.salary || 'competitive',
  candidate_email: candidate.personal_email || candidate.work_email,
  has_email: !!(candidate.personal_email || candidate.work_email)
};
return [{ json: context }];

Node 4 — Generate LinkedIn Connection Request: System prompt rules: (1) MUST be under 300 characters total including spaces. (2) Mention the candidate's current role or company specifically. (3) Be conversational and genuine, not salesy. (4) Do NOT mention the specific company hiring (say "an exciting opportunity" or similar). (5) End with a soft question or reason to connect. (6) Do NOT use emojis, exclamation marks, or ALL CAPS. (7) Do NOT make claims you cannot verify about the candidate. (8) Never use phrases like "I came across your profile" or "I was impressed by".

Node 4: LinkedIn Connection Request — User Prompt Template
text
Write a LinkedIn connection request note for {{candidate_first_name}}, who is currently a {{current_title}} at {{current_company}}. I'm recruiting for a {{job_title}} role. Keep it under 300 characters.

Node 5 — Generate Follow-up Message 1 (Day 2 after connection): System prompt rules: (1) Keep under 150 words. (2) Thank them for connecting (briefly). (3) Introduce the specific opportunity with 2–3 compelling details. (4) Mention the company name: {{company_name}}. (5) Include one specific detail relevant to their background at {{current_company}}. (6) End with a clear but low-pressure call-to-action (suggest a brief call). (7) Be warm and professional, not pushy. (8) Include: "We use AI-assisted tools to identify great candidates. Learn more: [client-website]/ai-recruiting-notice".

Node 6 — Generate Follow-up Message 2 (Day 5): System prompt angle: share a specific reason why this role could be a career-advancing move, reference market trends or team growth.

Node 7 — Generate Follow-up Message 3 - Final (Day 9): System prompt: Soft close. Acknowledge they may not be interested. Ask if they know someone who might be. Include opt-out: "If this isn't relevant, just let me know and I won't reach out again."

Node 9: Quality Validation — JavaScript
javascript
const messages = $input.first().json;
const connectionMsg = messages.connection_request;

// Validate connection request length
if (connectionMsg.length > 300) {
  // Truncate and regenerate flag
  messages.connection_request = connectionMsg.substring(0, 297) + '...';
  messages.quality_flag = 'connection_msg_truncated';
}

// Check for prohibited phrases
const prohibited = ['guarantee', 'best candidate', 'perfect fit', 'salary of'];
const allMessages = [messages.connection_request, messages.followup_1, messages.followup_2, messages.followup_3].join(' ').toLowerCase();
for (const phrase of prohibited) {
  if (allMessages.includes(phrase)) {
    messages.quality_flag = `contains_prohibited_phrase: ${phrase}`;
  }
}

// Check for hallucination indicators
if (allMessages.includes('your work on') || allMessages.includes('your project')) {
  messages.quality_flag = 'possible_hallucination_detected';
}

return [{ json: messages }];

Outreach Campaign Orchestrator Workflow

Type: workflow

n8n workflow that monitors the ATS for recruiter-approved candidates (human-in-the-loop gate), retrieves their personalized messages, creates and launches Expandi campaigns with the personalized content, monitors campaign progress via webhooks, handles responses, and updates the ATS with outreach status and candidate engagement data.

Implementation

1
TRIGGER NODE: Schedule (polling) + Webhook (events) — Schedule: Every 30 minutes during business hours (8 AM - 6 PM ET, Mon-Fri) — Webhook: /expandi-events (receives Expandi webhook callbacks)
2
NODE: Fetch Approved Candidates from ATS — Type: httpRequest, Method: GET — Finds candidates that recruiters have approved but not yet in an outreach sequence
3
NODE: Retrieve Personalized Messages — Type: code (JavaScript) — Fetch stored personalized messages for each candidate from n8n internal storage or ATS custom objects
4
NODE: Check Daily Volume Limits — Type: code (JavaScript) — Checks how many candidates have been added to campaigns today and enforces a daily limit of 15 per recruiter (conservative)
5
NODE: Create/Add to Expandi Campaign — Type: httpRequest, Method: POST — Adds approved candidates to Expandi campaign with personalized AI-generated message fields
6
NODE: Update ATS Status to 'in_sequence' — Type: httpRequest — Update Bullhorn candidate customText3 = 'in_sequence', add dateLastModified
7
WEBHOOK HANDLER: Expandi Event Processing — Type: switch (based on event type) — Handles: connection_accepted, message_replied, message_sent, connection_rejected
8
NODE: Response Classification via OpenAI — Type: openAi, Model: gpt-5.4-mini — Classifies replies into: INTERESTED, NOT_INTERESTED, QUESTION, OUT_OF_OFFICE, OPT_OUT, REFERRAL
9
NODE: Email Fallback Trigger — Condition: LinkedIn connection not accepted after 14 days AND candidate has email — Action: Send email sequence via Google Workspace (Gmail node) with CAN-SPAM footer
10
NODE: Daily Campaign Report — Type: scheduled (end of business day) — Aggregates sourced, enriched, in-sequence, responses received, and interested responses — Sends to recruiter team and MSP monitoring channel
Node 2: Fetch Approved Candidates from ATS — HTTP GET request to Bullhorn
http
GET {{bullhorn_rest_url}}/search/Candidate?query=customText1:ai_sourced AND status:approved AND customText3:NOT:in_sequence&fields=id,firstName,lastName,email,customText5,customObject1s&count=25
Node 4: Check Daily Volume Limits
javascript
// enforces 15 candidates/day per recruiter using n8n static data

// Check how many candidates have been added to campaigns today
const today = new Date().toISOString().split('T')[0];
const dailyCount = $workflow.staticData.dailyCounts?.[today] || 0;
const DAILY_LIMIT = 15; // per recruiter, conservative

const candidates = $input.all();
const toProcess = candidates.slice(0, Math.max(0, DAILY_LIMIT - dailyCount));

// Update counter
if (!$workflow.staticData.dailyCounts) $workflow.staticData.dailyCounts = {};
$workflow.staticData.dailyCounts[today] = dailyCount + toProcess.length;

return toProcess;
Node 5: Create/Add to Expandi Campaign
json
# POST body to
# https://api.expandi.io/api/v1/campaigns/{{campaign_id}}/prospects
# (Authorization: Bearer {{expandi_api_key}})

{
  "prospects": [
    {
      "linkedin_url": "{{candidate.linkedin_url}}",
      "first_name": "{{candidate.firstName}}",
      "last_name": "{{candidate.lastName}}",
      "custom_fields": {
        "ai_connection_message": "{{messages.connection_request}}",
        "ai_followup_1": "{{messages.followup_1}}",
        "ai_followup_2": "{{messages.followup_2}}",
        "ai_followup_3": "{{messages.followup_3}}"
      }
    }
  ]
}
  • connection_accepted: Update ATS, log event
  • message_replied: Parse reply content → classify with OpenAI → if INTERESTED: update ATS to 'responded_interested' and notify recruiter via Slack/email → if OPT_OUT: trigger suppression workflow, update ATS, stop sequence → if QUESTION: notify recruiter for manual response
  • message_sent: Log in ATS activity timeline
  • connection_rejected: After 14 days, trigger email fallback sequence if email is available

Node 8: OpenAI Response Classification — System prompt for gpt-5.4-mini (Temperature: 0.1, Max Tokens: 10)

Classify this recruiting outreach reply into exactly one category: - INTERESTED: Candidate expresses interest, wants to learn more, or agrees to a call - NOT_INTERESTED: Candidate declines, says they're happy in current role - QUESTION: Candidate asks a question about the role, compensation, or company - OUT_OF_OFFICE: Auto-reply or vacation notice - OPT_OUT: Candidate asks to stop being contacted or be removed - REFERRAL: Candidate suggests someone else for the role Respond with ONLY the category name, nothing else.
Sonnet 4.6
Note

Node 9 (Email Fallback): Emails sent via Google Workspace Gmail node must include a CAN-SPAM compliant footer with physical address and unsubscribe link. Triggered only when LinkedIn connection is not accepted after 14 days AND candidate email is available.

Candidate Opt-Out and Suppression Handler

Type: workflow

Dedicated n8n workflow that handles all opt-out requests across channels (LinkedIn replies, email unsubscribes, manual requests). Ensures immediate suppression from all active and future campaigns, updates the ATS suppression list, sends confirmation to the candidate, and logs the action for compliance audit trail.

Implementation

1
TRIGGER NODE: Webhook — Path: /opt-out-handler — Receives: candidate identifier (LinkedIn URL or email), source channel, raw message
2
NODE: Identify Candidate Across All Systems — Type: httpRequest — Search ATS by LinkedIn URL and/or email — Get all associated records and active campaign IDs
3
NODE: Stop All Active Expandi Campaigns — Type: httpRequest — Method: POST — URL: https://api.expandi.io/api/v1/campaigns/{{campaign_id}}/prospects/{{prospect_id}}/blacklist — Loop over all active campaigns for this candidate
4
NODE: Add to Global Suppression List — Type: code (JavaScript)
5
NODE: Update ATS Record — Type: httpRequest — Method: POST — Update Bullhorn candidate: status = 'opted_out', add note with timestamp and source — Set 'Do Not Contact' flag in ATS
6
NODE: Send Confirmation — If opt-out via LinkedIn: Send LinkedIn message via Expandi: 'Thanks for letting me know. I've removed you from our outreach list. Wishing you all the best.' — If opt-out via email: Send email confirmation with same message
7
NODE: Log for Compliance Audit — Type: code — Append to audit log (stored as JSON file on server or in database)
8
NODE: Notify MSP Monitoring — Type: slack or email — Message: 'Opt-out processed: [candidate name] ([linkedin_url]). All campaigns stopped. Confirmation sent.'
Node 4: Add to Global Suppression List
javascript
// Add to n8n static data suppression list
if (!$workflow.staticData.suppressionList) {
  $workflow.staticData.suppressionList = [];
}

const candidate = $input.first().json;
const entry = {
  linkedin_url: candidate.linkedin_url,
  email: candidate.email,
  opted_out_date: new Date().toISOString(),
  source: candidate.opt_out_source,
  reason: candidate.opt_out_message || 'no reason provided'
};

$workflow.staticData.suppressionList.push(entry);
return [{ json: { ...entry, status: 'suppressed' } }];
Node 7: Log for Compliance Audit
javascript
const fs = require('fs');
const logEntry = {
  timestamp: new Date().toISOString(),
  candidate_id: $input.first().json.candidate_id,
  action: 'OPT_OUT_PROCESSED',
  channels_suppressed: ['linkedin', 'email'],
  confirmation_sent: true,
  processed_by: 'automated_workflow'
};
fs.appendFileSync('/home/node/.n8n/compliance_audit_log.jsonl', JSON.stringify(logEntry) + '\n');
return [{ json: logEntry }];

Recruiter System Prompt Library

Type: prompt

A comprehensive library of tested and optimized system prompts for the OpenAI API calls throughout the pipeline. These prompts are tuned for the HR/Staffing vertical and enforce compliance, tone, length limits, and anti-hallucination guardrails. Each prompt includes the role context, output format requirements, and explicit constraints.

Implementation:

System Prompt Library for HR/Staffing Autonomous Agent

PROMPT 1: LinkedIn Connection Request Generator

Role: You are a senior recruiter at a boutique staffing firm writing LinkedIn connection request notes.

Task: Write a brief, personalized connection request note.

Rules

1
MAXIMUM 300 characters including all spaces and punctuation. Count carefully.
2
Reference the candidate's CURRENT role title and/or company name naturally.
3
Be conversational, warm, and genuine. Write like a real human, not a template.
4
Mention you have a relevant opportunity without giving too many details (create curiosity).
5
End with a soft reason to connect (e.g., "Would love to share details if you're open to it.").
6
NEVER use: exclamation marks, emojis, ALL CAPS, "I came across your profile", "I was impressed", "rockstar", "ninja", "guru", "perfect fit".
7
NEVER fabricate or assume details about the candidate's work, projects, or achievements that weren't provided in the input.
8
NEVER mention specific salary or compensation figures.
9
Output ONLY the message text, nothing else. No quotes, no labels.

Input format: Candidate name, current title, current company, target role title.

PROMPT 2: Follow-Up Message 1 (Post-Connection, Day 2)

Role: You are a senior recruiter following up after a LinkedIn connection was accepted.

Task: Write the first follow-up message introducing a specific job opportunity.

1
Maximum 150 words.
2
Briefly acknowledge the connection (one sentence max).
3
Introduce the opportunity: mention the hiring company name, role title, and 2-3 compelling aspects (team size, tech stack, growth stage, mission, remote policy).
4
Include one sentence that connects the candidate's current experience at [current_company] to why this role could be interesting for them.
5
End with a specific, low-pressure CTA: suggest a 15-minute call this week or next.
6
Include this compliance line naturally at the end: "PS: We use AI-assisted tools to help us find great candidates. More info: [compliance_url]"
7
Tone: Professional but warm. No corporate jargon. Write like a knowledgeable friend in the industry.
8
NEVER fabricate project names, achievement details, or specific work the candidate has done.
9
NEVER use pressure tactics or false urgency ("this role is closing soon", "only 2 spots left").

PROMPT 3: Follow-Up Message 2 (Day 5, No Reply)

Role: You are a recruiter sending a gentle second follow-up to a candidate who hasn't replied.

Task: Send a value-add follow-up that gives the candidate a new reason to engage.

1
Maximum 120 words.
2
Do NOT reference that they haven't replied. Do NOT say "just following up" or "bumping this".
3
Share one new piece of valuable information: a recent company milestone, team growth stat, industry trend that makes this role timely, or a benefit highlight.
4
Keep the tone lighter and more casual than Message 1.
5
End with an easy response prompt: "Even if the timing isn't right, I'd value your perspective on the [industry] market."
6
NEVER fabricate company news or statistics.

PROMPT 4: Follow-Up Message 3 (Day 9, Final)

Role: You are a recruiter sending a final, respectful outreach to a passive candidate.

Task: Write a graceful final message that respects their silence while keeping the door open.

Rules

1
Maximum 80 words.
2
Acknowledge you don't want to be a nuisance.
3
Make a single ask: either (a) if they're not interested, would they know someone who might be? or (b) would they prefer to revisit this conversation in a few months?
4
MUST include: "If this isn't relevant, just let me know and I won't reach out again."
5
Sign off warmly.
6
This is the LAST automated message. No further follow-ups.

PROMPT 5: Response Classifier

Role: You are an NLP classifier for recruiting outreach responses.

Task: Classify the candidate's reply into exactly one category.

Categories

  • INTERESTED: Wants to learn more, agrees to call, asks for details enthusiastically
  • NOT_INTERESTED: Declines, happy in current role, not looking, bad timing
  • QUESTION: Asks about salary, location, remote policy, company details, or role specifics
  • OUT_OF_OFFICE: Auto-reply, vacation notice, parental leave, sabbatical
  • OPT_OUT: Asks to stop being contacted, unsubscribe, remove from list, reports as spam
  • REFERRAL: Suggests another person for the role

Rules

1
Respond with ONLY the category name. One word. Nothing else.
2
If ambiguous between INTERESTED and QUESTION, choose QUESTION (safer to have human review).
3
If ambiguous between NOT_INTERESTED and OPT_OUT, choose OPT_OUT (safer for compliance).
4
Any mention of "stop", "remove", "unsubscribe", "don't contact" = OPT_OUT regardless of context.

PROMPT 6: Email Subject Line Generator

Role: You write email subject lines for recruiting outreach.

1
Maximum 60 characters.
2
Personalize with candidate's first name or current company.
3
Create curiosity without being clickbait.
4
No ALL CAPS, no excessive punctuation, no spam trigger words (free, guaranteed, act now).
5
Output 3 options, one per line, numbered 1-3.

Examples of good subject lines:

  • [FirstName], quick question about your next move
  • [CurrentCompany] → something interesting?
  • [RoleTitle] opportunity, [FirstName]

Testing & Validation

  • SOURCING TEST: Create a test search in hireEZ for 'Senior Software Engineer, Python, 5+ years, San Francisco Bay Area'. Verify that at least 30 candidate profiles are returned with LinkedIn URLs, current titles, and company names. Cross-reference 5 random profiles by manually checking their LinkedIn pages to confirm data accuracy.
  • ATS INTEGRATION TEST: Export 5 candidates from hireEZ to Bullhorn/ATS. Verify that all fields map correctly: first name, last name, current title, current company, LinkedIn URL (in customText5), source attribution (customText1 = 'ai_sourced'), and status (customText3 = 'pending_review'). Confirm no duplicate records were created.
  • ENRICHMENT TEST: Run 10 candidate LinkedIn URLs through the Clay enrichment pipeline via the n8n webhook. Verify that at least 6 out of 10 return a valid email address (personal or work). Spot-check 3 email addresses using an email verification service (e.g., NeverBounce or ZeroBounce) to confirm deliverability.
  • MESSAGE PERSONALIZATION TEST: Generate personalized outreach messages for 10 candidates using the OpenAI engine. Have two recruiters independently rate each connection request message on: accuracy (no fabricated details), tone (professional but warm), relevance (mentions something specific to the candidate), and length compliance (under 300 characters). Target: 8/10 messages pass all criteria without edits.
  • LINKEDIN AUTOMATION TEST: Create a test Expandi campaign targeting 2 internal team members (or recruiter's own secondary LinkedIn account). Verify: (a) connection request sends with personalized note correctly populated from custom fields, (b) after manual acceptance, follow-up message 1 fires on Day 2, (c) webhook fires to n8n upon message sent and reply received.
  • HUMAN-IN-THE-LOOP GATE TEST: Confirm that candidates with ATS status 'pending_review' are NOT picked up by the Outreach Orchestrator workflow. Change one candidate's status to 'approved' and verify that within 30 minutes, they are added to an Expandi campaign. Change another to 'rejected' and verify no outreach is triggered.
  • OPT-OUT HANDLING TEST: Simulate an opt-out by replying 'please remove me from your list' to a test campaign message. Verify within 5 minutes: (a) Expandi campaign stops for that prospect, (b) ATS record updated to 'opted_out' with Do Not Contact flag, (c) confirmation message sent to the candidate, (d) entry appears in compliance audit log, (e) candidate does not appear in any future sourcing exports.
  • EMAIL DELIVERABILITY TEST: After the 2-week warm-up period, send test emails from each recruiter's talent.clientfirm.com mailbox to mail-tester.com. Verify a score of 9/10 or higher. Also send to Gmail, Outlook, and Yahoo test accounts and verify messages land in Primary/Inbox (not Spam or Promotions).
  • END-TO-END PIPELINE TEST: Execute the full autonomous agent cycle for one real open job: (1) n8n triggers hireEZ search → (2) candidates created in ATS → (3) Clay enriches → (4) OpenAI personalizes → (5) recruiter reviews and approves 5 candidates → (6) Expandi sends connection requests → (7) monitor for 7 days. Verify all data flows correctly at each stage and ATS activity log shows complete audit trail.
  • COMPLIANCE VERIFICATION TEST: Verify the AI Recruiting Notice page is live at clientfirm.com/ai-recruiting-notice with all required disclosures. Verify all outreach messages include the compliance notice link. Verify data retention workflow correctly identifies and flags records older than the configured retention period. If recruiting in NYC: verify bias audit documentation is on file and candidate notice requirements are met per LL 144.
  • DAILY VOLUME LIMIT TEST: Attempt to approve more candidates than the daily limit (15 per recruiter). Verify the Outreach Orchestrator respects the limit and queues excess candidates for the following day rather than exceeding safe LinkedIn activity thresholds.
  • RESILIENCE TEST: Temporarily disconnect the Clay API key and trigger the enrichment pipeline. Verify the workflow handles the error gracefully (logs error, sends MSP notification, does not crash or lose candidate data). Restore the API key and verify queued candidates are processed on the next run.

Client Handoff

The client handoff session should be a structured 2-hour meeting with the recruiting team lead, all recruiters who will use the system, and the client's compliance stakeholder. Cover the following topics in order:

1
SYSTEM OVERVIEW (20 min): Walk through the complete pipeline architecture with a visual diagram showing how candidates flow from LinkedIn → hireEZ → ATS → enrichment → personalization → outreach → response handling. Emphasize the human-in-the-loop review gate and why it exists.
2
DAILY RECRUITER WORKFLOW (30 min): Demonstrate the day-to-day workflow: (a) Review AI-sourced candidates in the ATS each morning, (b) approve or reject candidates for outreach, (c) review personalized messages before they send (spot-check, not every one), (d) monitor Expandi Smart Inbox for responses, (e) handle interested candidates manually from this point. Provide a 1-page quick-reference card for daily tasks.
3
CAMPAIGN MANAGEMENT (20 min): Show how to create new job-based campaigns, adjust targeting criteria in hireEZ, modify outreach sequence templates, and pause/resume campaigns. Train on Expandi dashboard for monitoring campaign health metrics.
4
COMPLIANCE & SAFETY (20 min): Review LinkedIn account safety practices (never exceed daily limits, maintain manual engagement, what to do if a warning is received). Review the AI Recruiting Notice page and when it needs updating. Explain opt-out handling and the audit trail. For NYC-area recruiting: explain LL 144 requirements and the bias audit process.
5
TROUBLESHOOTING & ESCALATION (15 min): Common issues and first-response actions: LinkedIn account temporarily restricted (reduce limits, pause 48 hours, contact MSP), email bouncing (check DNS, contact MSP), n8n workflow failure (check n8n dashboard, restart Docker, contact MSP). Provide an escalation matrix with MSP contact information and SLA response times.
6
SUCCESS METRICS REVIEW (15 min): Establish baseline KPIs to track: candidates sourced per week, enrichment hit rate, outreach response rate (target: 25–35% for personalized outreach), interested response rate (target: 8–15%), time-to-fill improvement, and cost-per-hire reduction. Set a 30-day check-in meeting to review initial results.

Documentation to Leave Behind

Maintenance

ONGOING MSP MAINTENANCE RESPONSIBILITIES:

Weekly (1-2 hours/week)

  • Monitor n8n workflow execution logs for errors or failures; investigate and resolve any failed executions
  • Review LinkedIn account health for each recruiter in Expandi: check for any warnings, restrictions, or unusual activity flags
  • Monitor email deliverability metrics via Google Postmaster Tools: domain reputation should remain 'High', bounce rate under 3%, spam rate under 0.1%
  • Review Clay credit consumption and enrichment success rates; flag if email hit rate drops below 50%
  • Check Expandi campaign metrics: connection acceptance rate (target >30%), reply rate (target >15%), and flag campaigns significantly below benchmarks for prompt tuning

Monthly (3-5 hours/month)

  • Update n8n Docker image to latest stable version (test in staging first)
  • Review and optimize OpenAI prompts based on recruiter feedback and message performance data; A/B test variations
  • Run compliance audit: verify AI Recruiting Notice page is current, suppression list is enforced across all platforms, data retention purge workflow has run successfully
  • Generate monthly performance report for client: candidates sourced, enriched, contacted, responded, interviews scheduled, placements attributed to AI pipeline
  • Review hireEZ and Expandi platform release notes for new features or breaking changes; apply configuration updates as needed
  • Rotate or refresh dedicated residential proxy IPs if any are showing degraded LinkedIn performance
  • Verify SSL certificate auto-renewal is functioning (Let's Encrypt certificates every 90 days)
Update n8n Docker image to latest stable version
bash
docker compose pull && docker compose up -d

Quarterly (5-8 hours/quarter)

  • Conduct full security audit of n8n server: apply OS security patches, review access logs, rotate API keys and passwords
  • Review and update outreach sequence templates based on 90 days of performance data; retire underperforming sequences and deploy new variants
  • Conduct LinkedIn account maintenance: review each recruiter's connection acceptance patterns, adjust daily limits based on account maturity and performance
  • Review SaaS vendor pricing and contract renewals; evaluate whether current tier still fits usage patterns
  • If client recruits in jurisdictions with AI hiring laws: coordinate annual bias audit with third-party auditor (NYC LL 144 requires annual audit); update candidate notice language as regulations evolve
  • Backup n8n PostgreSQL database
Apply OS security patches to n8n server
bash
sudo apt update && sudo apt upgrade
Backup n8n PostgreSQL database
bash
docker exec n8n-postgres pg_dump -U n8n n8n > backup_$(date +%Y%m%d).sql

SLA Considerations

  • Critical (n8n server down, ATS integration broken): 4-hour response, 8-hour resolution target
  • High (LinkedIn account restricted, email deliverability degraded): 8-hour response, 24-hour resolution target
  • Medium (enrichment hit rate drop, prompt quality issues): 24-hour response, 72-hour resolution target
  • Low (feature requests, optimization suggestions): Addressed in next monthly review

Escalation Path

1
First response: MSP Level 1 technician checks n8n logs, Expandi status, basic troubleshooting
2
Second tier: MSP senior engineer for API integration issues, workflow debugging, infrastructure problems
3
Vendor support: hireEZ, Expandi, Clay, OpenAI support channels for platform-specific issues
4
Compliance escalation: Client's legal counsel + MSP compliance advisor for regulatory questions

Model/Prompt Retraining Triggers

  • Outreach response rate drops below 15% for 2 consecutive weeks → review and update personalization prompts
  • Connection acceptance rate drops below 20% → review connection request prompt and LinkedIn account health
  • Response classifier accuracy drops below 90% (measured by recruiter corrections) → retune classifier prompt with new examples
  • New state or federal AI hiring regulation enacted → update compliance notice, review all prompts for new requirements
  • Client adds new job categories or industries → create specialized prompt variants tuned for those verticals

Alternatives

All-in-One Platform Approach (SeekOut or Gem)

Instead of assembling a multi-tool stack (hireEZ + Expandi + Clay + OpenAI + n8n), deploy a single enterprise platform like SeekOut ($833+/seat/month) or Gem ($300+/seat/month) that handles sourcing, enrichment, personalization, and outreach sequences natively within one interface. These platforms offer built-in CRM, ATS integration, and compliance features without requiring custom workflow orchestration.

  • COST: Significantly higher per-seat cost ($833/seat/month for SeekOut vs. ~$400/seat/month for the assembled stack). For 5 seats, the annual difference is ~$26,000.
  • COMPLEXITY: Much lower — no n8n server to maintain, no custom workflows to debug, no multi-vendor API management.
  • CAPABILITY: SeekOut and Gem have less flexible personalization (pre-built templates rather than GPT-powered custom generation) and their LinkedIn automation is more conservative (typically limited to InMail rather than connection requests + messaging).
  • BEST FOR: Clients with budget for premium tools who prioritize simplicity and vendor support over maximum customization. Also better for clients with strict IT policies that prohibit self-hosted infrastructure.

Budget DIY Stack (Dripify + PhantomBuster + OpenAI)

For cost-sensitive small staffing firms (1-5 recruiters), assemble a minimal stack: LinkedIn Sales Navigator ($99/month) for manual searching, PhantomBuster ($69/month) for LinkedIn profile scraping and data extraction, OpenAI API (~$30/month) for message personalization, and Dripify ($59/seat/month) for automated LinkedIn outreach sequences. Use Zapier ($20/month) instead of self-hosted n8n for simpler workflow automation.

  • COST: Total ~$280-$350/month for a single recruiter vs. ~$600-$800/month for the primary approach. Saves ~$3,000-$5,000/year per recruiter.
  • COMPLEXITY: Lower initial setup but more manual work daily — no AI-powered candidate discovery (manual LinkedIn searching), less sophisticated enrichment, more hands-on campaign management.
  • CAPABILITY: No ATS Rediscovery feature, smaller candidate database (limited to LinkedIn search results), no autonomous sourcing (recruiter must define and execute searches manually). LinkedIn compliance risk is similar.
  • BEST FOR: Solo recruiters or micro-agencies with tight budgets who want to test AI-assisted outreach before committing to a full autonomous agent stack. Good starting point that can be upgraded to the primary approach as volume grows.

HeroHunt.ai Autonomous Agent (Turnkey SaaS)

Deploy HeroHunt.ai (~$107/month) as a fully autonomous AI recruiter that handles the entire pipeline — sourcing across LinkedIn, GitHub, and Stack Overflow, generating personalized outreach, and managing candidate pipelines — with minimal configuration. No custom workflows, no self-hosted infrastructure, no multi-vendor integration management.

  • COST: Lowest tool cost at ~$107/month, but less MSP recurring revenue opportunity since there's less to manage.
  • COMPLEXITY: Minimal — turnkey SaaS with guided setup. Can be deployed in days rather than weeks.
  • CAPABILITY: Less customizable than the primary approach — you're locked into HeroHunt's AI models and outreach logic rather than having full control over OpenAI prompts and workflow orchestration. Limited ATS integration options compared to hireEZ or Gem. Newer vendor with less market track record.
  • BEST FOR: MSP clients who need fast deployment (under 1 week), have simple ATS setups, and prioritize speed-to-value over customization. Also good as an interim solution while the full autonomous agent stack is being built out.

LinkedIn Recruiter + Hiring Assistant (Native LinkedIn)

Use LinkedIn's own enterprise recruiting tools: LinkedIn Recruiter Corporate ($10,800/seat/year) with the LinkedIn Hiring Assistant AI feature. This stays entirely within LinkedIn's ecosystem, eliminating third-party automation compliance risk. The Hiring Assistant helps draft InMails and provides AI-suggested candidates, though it doesn't provide fully autonomous sourcing or multi-channel outreach.

  • COST: Very expensive at $900/seat/month — more than double the primary approach for fewer capabilities.
  • COMPLEXITY: Lowest possible — no integrations, no servers, no third-party tools. Everything is within LinkedIn.
  • CAPABILITY: Most limited — no autonomous agent behavior (recruiters still manually search, evaluate, and initiate outreach), no email fallback channel, no custom AI personalization (LinkedIn's built-in AI is generic), no multi-platform sourcing (LinkedIn only). Zero risk of LinkedIn account restriction since you're using official tools within their TOS.
  • BEST FOR: Highly risk-averse clients (particularly those in regulated industries or with strict legal departments) who cannot tolerate any LinkedIn TOS compliance risk, and who are willing to pay a premium for the safety of staying within LinkedIn's official tools.

Custom Open-Source Agent Stack (LangChain + n8n)

Build a fully custom autonomous recruiting agent using LangChain (Python framework for AI agents), self-hosted open-source LLMs (e.g., Llama 3 or Mistral via Ollama), n8n for orchestration, and direct LinkedIn API/scraping for data collection. This gives maximum control, data privacy, and eliminates per-seat SaaS costs but requires significant development effort.

  • COST: Lowest recurring cost (primarily infrastructure: ~$100-$200/month for compute) but highest upfront development cost ($15,000-$30,000 for custom development, 4-8 weeks of senior developer time).
  • COMPLEXITY: Highest — requires Python development expertise, LLM fine-tuning, LinkedIn reverse-engineering, and ongoing maintenance of custom code.
  • CAPABILITY: Maximum flexibility and customization, full data ownership, no vendor lock-in. But open-source LLMs currently produce lower quality personalized messages than GPT-5.4 mini, and LinkedIn data access without official tools is the highest-risk approach for account restrictions.
  • BEST FOR: MSPs with strong in-house development teams who want to build a proprietary, white-labeled AI recruiting product to resell across many clients. The high upfront investment is amortized across the client base. Not recommended for single-client deployments.

Want early access to the full toolkit?