---
name: personalize-outbound-linkedin
description: "Personalize Outbound at Scale Using LinkedIn Data. Use when generic cold emails get few replies, and reps cannot manually research every prospect on a high-volume list."
---

# Personalize Outbound at Scale Using LinkedIn Data

Generic cold emails get few replies, and reps cannot manually research every prospect on a high-volume list.

## When to use this skill

- The user wants to set up or run this workflow: "Personalize Outbound at Scale Using LinkedIn Data".
- The user describes this problem: Generic cold emails get few replies, and reps cannot manually research every prospect on a high-volume list.

## At a glance

- Category: Outbound
- Difficulty: Intermediate
- Time saved: 10 hours per week. This figure is an estimate, not a measured result.

## Tools

Options separated by "or" are alternatives; use whichever the user already has.

- Clay
- Claygent
- Instantly

## Steps

1. Import prospect list to Clay (name, company, LinkedIn URL)
2. Use Claygent to scrape recent LinkedIn posts and extract topics
3. Generate personalized first line referencing their post topic
4. Use Clay AI to write custom icebreaker based on post content
5. Push to Instantly campaign with dynamic merge tags

## Template outline

Field names, thresholds, and message text are starting points to adapt.

~~~text
Clay Table:
- Input: CSV with LinkedIn URLs
- Column 1: Claygent scrapes last 3 LinkedIn posts
- Column 2: Extract topics (AI prompt: "Summarize topics in 3 words")
- Column 3: Generate opener (AI prompt: "Write 1 sentence referencing {{topic}}")
- Column 4: Instantly HTTP API to add contact with custom field {{opener}}
~~~

## Optional: build it with the Clay CLI

This builds the playbook as a Clay workflow. A CSV trigger sends each prospect through three steps: research their recent LinkedIn posts, write a first line, and add the lead to an Instantly campaign. Each CSV row becomes one workflow run. The Clay CLI cannot create tables, add columns, or import rows into a table, so this build uses a workflow in place of the table in the template outline.

Written against Clay CLI v1.3.1. Run these commands only in the user's own Clay workspace, logged in with their own account, after they confirm. Items marked VERIFY are config shapes the CLI help does not document; check them in the workspace before relying on them.

### Prerequisites

- Install the Clay CLI: Use version 1.3.1 or later, installed from Clay's own instructions. This page does not include an install command because the CLI reference it was written from does not list one. You also need jq, which the script uses to read the JSON the CLI prints.
- Log in to your workspace: Browser login is the default. Use the device flow on a headless machine. A static key in the CLAY_API_KEY environment variable also works. Workflow commands need the terracotta:cli scope, so an auth error that names it means your login cannot manage workflows.
  - Log in with a browser: `clay login`
  - Log in on a headless machine: `clay login --device`
  - Confirm who you are logged in as: `clay whoami`
- Pick the workspace to build in: The workflow is created in your active workspace. Switch first if you belong to more than one.
  - List workspaces: `clay workspaces list`
  - Show the active workspace: `clay workspaces current`
  - Switch workspace: `clay workspaces switch <workspaceId>`
- Find your action keys: Action package ids and keys are specific to your workspace. Save the catalog, search it for the Claygent research, AI text, and Instantly actions, then paste their ids into the top of the script. Skip "clay workflows actions test": it spends credits.
  - Save the action catalog: `clay workflows actions list > catalog.json`
  - Search the catalog: `grep -Eio '"[^"]*(claygent|instantly|use ai|generate)[^"]*"' catalog.json | sort -u`
  - See an action's inputs and outputs: `clay workflows actions schema <packageId> <actionKey>`
- Connect Instantly and make a test campaign: Connect your Instantly account inside Clay and create an empty Instantly campaign for testing, so early runs do not land in a live sequence.

### Workflow: Personalize outbound with LinkedIn posts

Trigger (csv_upload): You upload a CSV of prospects to the trigger. Each header becomes a string input, and each row becomes one run once the workflow is published. VERIFY: The trigger type string "csv_upload" is inferred. The CLI help does not show how to create a CSV trigger.

Inputs (CSV headers):

- first_name: Prospect first name (example: Jane)
- last_name: Prospect last name (example: Doe)
- company: Company name (example: Example Co)
- linkedin_url: Full LinkedIn profile URL (example: https://www.linkedin.com/in/...)
- email: Work email for Instantly (example: jane@example.com)

1. Research recent LinkedIn posts (action): A Claygent web research action reads the prospect's latest posts and pulls out what they talk about.
   - Inputs: linkedin_url, company
   - Outputs: topics, recent_post_summary
   - Prompt:

~~~text
Open {{linkedin_url}} and read this person's 3 most recent LinkedIn posts or reposts.
Return JSON with two fields:
- topics: up to 3 short phrases, 3 words or fewer each, describing what the posts are about
- recent_post_summary: one sentence summarizing the most recent post
If there are no posts from the last 90 days, return an empty topics list and an empty summary.
~~~

   - VERIFY: Created with "nodes create". Passing an action package id and key on create is a guess. If you saved this research as a Claygent, the documented alternative is an "agent" step with its claygentId.
2. Write the first line (action): An AI text action turns the topics into a single opening sentence for the email.
   - Inputs: first_name, company, topics
   - Outputs: opener
   - Prompt:

~~~text
Write one sentence, under 25 words, that opens a cold email to {{first_name}} at {{company}}.
Reference one of these topics from their recent LinkedIn posts: {{topics}}.
No flattery, no questions, no exclamation marks, no em dashes.
If topics is empty, return an empty string.
~~~

   - VERIFY: Inserted after step 1. The field layout for an action step inside "nodes insert" is inferred.
3. Add the lead to an Instantly campaign (action): Adds the prospect to your Instantly campaign with the opener as a custom variable for the first email.
   - Inputs: email, first_name, last_name, company, opener, campaign
   - Outputs: Instantly response
   - VERIFY: Check that an Instantly action appears in your catalog. If it does not, an HTTP request action against Instantly's API is the fallback. Point it at a test campaign first.

### Script: build-linkedin-outbound.sh

Creates the workflow, trigger, and three steps, then pauses so you can map step inputs. It validates the graph, offers one test run, and asks before publishing. Every CLI response is saved to a local folder so you can check ids.

~~~bash
#!/usr/bin/env bash
# Builds the "Personalize outbound with LinkedIn posts" Clay workflow
# in YOUR Clay workspace, using YOUR Clay CLI login.
# Written against Clay CLI v1.3.1. Not run or tested by GTM Playbooks.
# Lines tagged VERIFY use a shape the CLI help does not document.
set -euo pipefail

# Copy these from catalog.json (see "Find your action keys").
RESEARCH_PACKAGE_ID=""    # AI web research action (Claygent)
RESEARCH_ACTION_KEY=""
WRITER_PACKAGE_ID=""      # AI text generation action
WRITER_ACTION_KEY=""
INSTANTLY_PACKAGE_ID=""   # Instantly: add lead to campaign
INSTANTLY_ACTION_KEY=""

# One row to test before running a whole list. Use yourself or a teammate.
TEST_ROW='{"first_name":"","last_name":"","company":"","linkedin_url":"","email":""}'

for v in RESEARCH_PACKAGE_ID RESEARCH_ACTION_KEY WRITER_PACKAGE_ID WRITER_ACTION_KEY INSTANTLY_PACKAGE_ID INSTANTLY_ACTION_KEY; do
  [[ -n "${!v}" ]] || { echo "Set $v at the top of this script." >&2; exit 1; }
done
command -v clay > /dev/null || { echo "Clay CLI not found on PATH." >&2; exit 1; }
command -v jq > /dev/null || { echo "jq is required to read CLI output." >&2; exit 1; }
clay whoami > /dev/null || { echo "Not logged in. Run: clay login" >&2; exit 1; }

# VERIFY: only "workflows create" documents an "id" field in its output.
read_id() {
  local id
  id=$(jq -r '.id // empty' "$1")
  [[ -n "$id" ]] || { echo "No id found in $1. Open it and adjust read_id." >&2; exit 1; }
  echo "$id"
}

mkdir -p clay-linkedin-outbound
cd clay-linkedin-outbound

echo "1/7 Create the workflow"
clay workflows create --name "Personalize outbound with LinkedIn posts" > workflow.json
WF_ID=$(read_id workflow.json)
WF_URL=$(jq -r '.url // empty' workflow.json)

echo "2/7 Add the CSV upload trigger"
# VERIFY: triggerType "csv_upload" is inferred; the help does not show it.
clay workflows triggers create "$WF_ID" --input '{"triggerType":"csv_upload"}' > trigger.json
TRG_ID=$(read_id trigger.json)

echo "3/7 Step 1: research recent LinkedIn posts"
# VERIFY: help shows nodeType and name only; actionPackageId and actionKey are a guess.
STEP1=$(jq -nc --arg p "$RESEARCH_PACKAGE_ID" --arg k "$RESEARCH_ACTION_KEY" '{nodeType: "tool", name: "Research recent LinkedIn posts", actionPackageId: $p, actionKey: $k}')
clay workflows nodes create "$WF_ID" --input "$STEP1" > step1.json
STEP1_ID=$(read_id step1.json)

echo "4/7 Step 2: write the first line"
# VERIFY: action fields inside "step" are inferred from the documented utility example.
STEP2=$(jq -nc --arg n "$STEP1_ID" --arg p "$WRITER_PACKAGE_ID" --arg k "$WRITER_ACTION_KEY" '{target: {type: "after-node", nodeId: $n}, step: {kind: "action", actionPackageId: $p, actionKey: $k}}')
clay workflows nodes insert "$WF_ID" --input "$STEP2" > step2.json
STEP2_ID=$(read_id step2.json)

echo "5/7 Step 3: add the lead to an Instantly campaign"
# VERIFY: same inferred insert shape as step 2.
STEP3=$(jq -nc --arg n "$STEP2_ID" --arg p "$INSTANTLY_PACKAGE_ID" --arg k "$INSTANTLY_ACTION_KEY" '{target: {type: "after-node", nodeId: $n}, step: {kind: "action", actionPackageId: $p, actionKey: $k}}')
clay workflows nodes insert "$WF_ID" --input "$STEP3" > step3.json
clay workflows graph get "$WF_ID" --mode full > graph.json

echo "6/7 Map step inputs"
cat <<EOF

Step inputs are specific to your workspace. Map them before validating:
  Step 1 research:   linkedin_url, company, and the research prompt
  Step 2 first line: first_name, company, step 1 topics, and the writer prompt
  Step 3 Instantly:  email, first_name, last_name, company, the step 2 opener
                     as a custom variable, and a TEST campaign
Map them in the Clay UI${WF_URL:+ at $WF_URL}, or per node with:
  clay workflows nodes get $WF_ID <nodeId>
  clay workflows nodes update $WF_ID <nodeId> --input node.json
graph.json in this folder shows the current nodes and edges.

EOF
read -r -p "Press Enter when inputs are mapped. "

echo "7/7 Validate, test one row, publish"
clay workflows graph validate "$WF_ID"
clay workflows diagram "$WF_ID"

if jq -e 'all(.[]; . != "")' <<< "$TEST_ROW" > /dev/null; then
  read -r -p "Run TEST_ROW once? It can spend Clay credits and add a lead in Instantly. [y/N] " answer
  if [[ "$answer" == [yY] ]]; then
    clay workflows runs test "$WF_ID" --inputs "$TEST_ROW" > test-run.json
    echo "Saved test-run.json. Check each step with: clay workflows runs steps $WF_ID <runId>"
  fi
else
  echo "TEST_ROW is empty, skipping the test run."
fi

read -r -p "Publish the workflow now? [y/N] " answer
if [[ "$answer" == [yY] ]]; then
  clay workflows publish "$WF_ID" --name v1
fi

echo "Workflow $WF_ID, CSV trigger $TRG_ID. All responses are saved in $(pwd)."
~~~

### Prospect CSV: prospects.csv

~~~csv
first_name,last_name,company,linkedin_url,email
Jane,Doe,Example Co,https://www.linkedin.com/in/your-test-profile,jane@example.com
~~~

### Run it on your list

After publishing, upload the CSV to the trigger and start with a few rows. Each row is one run.

- Load the saved ids (run inside clay-linkedin-outbound/): `WF_ID=$(jq -r '.id' workflow.json); TRG_ID=$(jq -r '.id' trigger.json)`
- Upload the CSV (this does not start runs): `clay workflows triggers csv upload "$TRG_ID" --file prospects.csv`
- Run the first 5 rows (needs a published workflow, one run per row): `clay workflows triggers csv run "$TRG_ID" --limit 5`
- List runs: `clay workflows runs list "$WF_ID"`
- Inspect one run: `clay workflows runs get "$WF_ID" <runId>`
- See each step's output for that run: `clay workflows runs steps "$WF_ID" <runId>`

### Verify in the workspace

- The CSV trigger type "csv_upload" is inferred. If "triggers create" exits with code 2 (validation), check which trigger types your workspace accepts.
- The help documents "nodes create" with nodeType and name only. If it rejects actionPackageId and actionKey, create the node with a name only and pick the action in the Clay UI.
- The reference says action steps take actionPackageId and actionKey but shows only a utility example for "nodes insert", so the placement of those fields inside "step" is inferred.
- Only "workflows create" documents an id field in its response. The script reads .id from trigger and node responses too and stops if it is missing.
- Package ids and action keys for the Claygent research, AI text, and Instantly actions differ by workspace. Copy them from your catalog; this page does not guess them.
- Step input mapping is dynamic per workspace. The CLI path is "nodes get" then "nodes update", where incomingEdges is a full replacement. The argument order for "nodes get" is inferred from "nodes update".
- The help does not say whether step 1 connects to the trigger automatically. "clay workflows graph validate" reports a disconnected graph.
- "runs test" without --live is assumed to run the draft. The reference lists --live without describing it.
- Workflow runs can spend Clay credits, and retrying a CSV run spends them again. Exit code 3 means re-run clay login, and exit code 4 means you are rate limited (wait for details.retryAfter).

## Guardrails

- Work only in the user's own accounts and workspaces, with their credentials.
- Confirm with the user before any step that writes to a CRM, sends email or messages, spends credits, or moves money.
- Treat every hours, cost, and threshold figure as a starting assumption and replace it with the user's numbers.
- Do not invent data, prices, or results. If a vendor price matters, check the vendor's current pricing page.

## Source

https://gtmplaybooks.io/playbooks/personalize-outbound-linkedin/
