← The Build Log / Building in Public
Building in Public

I built a pipeline that turns LinkedIn photos into personalised event invites

Rahul Tulsiani Rahul Tulsiani · Mar 14, 2025 · 8 min read

It started with a simple question: what if event invites felt like they were made for you?

Not "Hi {first_name}" made for you. Actually made for you. Your face, your name, your company branding, all composited into a personalised invite that looked like someone spent twenty minutes in Photoshop.

The answer, it turns out, involves Clay, Make.com, GPT-4o, Cloudinary, and a lot of trial and error.

The idea

I had been thinking about this for a while. Most event invites are forgettable. They land in your inbox looking exactly like the last fifty. The open rate is fine, the click rate is not, and the actual attendance rate tells the real story.

So I started building. The idea was straightforward: pull a prospect's LinkedIn photo, generate a personalised event banner with their face composited in, and send it as part of an invite sequence.

The stack

The stack ended up being simpler than I expected:

  • Clay — handles enrichment and LinkedIn photo extraction
  • Make.com — orchestrates the workflow between tools
  • GPT-4o — generates the personalised copy for each invite
  • Cloudinary — does the image compositing via URL-based transformations

Here's the Cloudinary transformation URL that does the heavy lifting:

https://res.cloudinary.com/demo/image/upload/
  w_1200,h_630,c_fill/
  l_fetch:BASE64_ENCODED_LINKEDIN_URL/
  w_200,h_200,c_fill,g_face,r_max/
  fl_layer_apply,g_west,x_80,y_0/
  event-invite-template.png

That single URL pulls the LinkedIn photo, crops it to a circle using face detection, and composites it onto the event invite template. No Photoshop. No manual work.

Wiring it together

The Make.com scenario runs like this:

1. Clay webhook fires with prospect data
2. Make receives: name, company, title, photo_url
3. GPT-4o generates personalised subject + body
4. Cloudinary URL is constructed with the photo
5. Email is sent via SendGrid with the composited image
6. Response is logged back to Clay

The Clay table feeds the whole thing. Each row is a prospect with their LinkedIn data already enriched. When I mark someone as "ready to invite," the webhook fires and the whole sequence runs in about 8 seconds.

Here's the GPT-4o prompt I used to generate the invite copy:

You are writing a personalised event invitation.

Context:
- Prospect name: {{name}}
- Company: {{company}}
- Title: {{title}}
- Event: B2B Marketing Automation Masterclass
- Date: March 28, 2025

Write a 3-sentence invite that:
1. References something specific about their role
2. Connects it to the event topic
3. Ends with a casual, low-pressure CTA

Tone: warm, specific, not salesy.
Keep it under 60 words.

Where it broke

The first version was terrible. The face compositing looked like a ransom note. The positioning was off, the scaling was wrong, and half the LinkedIn photos were too low-resolution to use.

Three things went wrong:

  1. Photo quality — LinkedIn serves different resolutions depending on the URL endpoint. The /shrink_200_200/ version is too small. You need the /original/ endpoint from Clay's enrichment.
  2. Face positioning — Without g_face in the Cloudinary transform, the crop centers on the image, not the face. Headshots with off-center compositions got cropped badly.
  3. Template sizing — My first template was 1200x628 (standard OG). But email clients render images differently. I switched to 600x314 for email and kept 1200x628 for landing pages.

The fix

Version two added three safeguards:

// Quality check before compositing
const photoCheck = await fetch(linkedinPhotoUrl, { method: 'HEAD' });
const contentLength = photoCheck.headers.get('content-length');

if (parseInt(contentLength) < 15000) {
  // Photo too small — fall back to text-only invite
  return generateTextOnlyInvite(prospect);
}

// Face detection check
const faceDetect = await cloudinary.api.resource(photoId, {
  faces: true
});

if (faceDetect.faces.length === 0) {
  // No face detected — fall back to initials avatar
  return generateInitialsInvite(prospect);
}

This catches the two most common failures: low-res photos and photos without detectable faces (logos, group photos, etc). About 15% of prospects fall into the fallback path.

The Clay formula for building the Cloudinary URL looks like this:

const cloudinaryBase = "https://res.cloudinary.com/your-cloud/image/upload";
const transforms = [
  "w_600,h_314,c_fill",
  `l_fetch:${btoa(prospect.photo_url)}`,
  "w_120,h_120,c_fill,g_face,r_max",
  "fl_layer_apply,g_west,x_40,y_0"
].join("/");

return `${cloudinaryBase}/${transforms}/event-template.png`;

Scaling it

Once the pipeline worked for one event, I templated it. Now I have three variants:

  • Event invite — face composited on event banner
  • Webinar reminder — face on a "saved your spot" card
  • Post-event follow-up — face on a "we met at" card

Each one uses the same Clay → Make → Cloudinary flow. The only thing that changes is the template ID and the GPT prompt.

The Make.com scenario has a router module that picks the right template:

Router conditions:
  - If campaign_type = "event_invite" → Template A
  - If campaign_type = "webinar_reminder" → Template B
  - If campaign_type = "post_event" → Template C

Each branch:
  1. Build Cloudinary URL with correct template
  2. Generate copy with campaign-specific prompt
  3. Send via SendGrid with correct sender identity

Results

After two weeks of testing across 200 prospects:

  • Open rate: 68% (vs 42% for standard invites)
  • Click rate: 24% (vs 8% standard)
  • Attendance rate: 31% of those who clicked actually showed up
  • Reply rate: 12% replied to say something about the invite itself

The replies were the most interesting part. People weren't just clicking — they were responding to say "how did you do this?" That's a conversation starter that no standard invite gives you.

The whole pipeline runs on tools I was already paying for. Total additional cost: about $0.003 per invite (GPT-4o tokens + Cloudinary transforms). For a 200-person event campaign, that's sixty cents.

What I'd do differently

If I were starting this today:

  1. Skip Make.com for the first version — Clay's built-in HTTP actions can handle simple flows. Add Make only when you need branching logic.
  2. Build the fallback first — the text-only invite should be your baseline, not an afterthought. Start there, then layer the photo compositing on top.
  3. Test with 10 people before building the full pipeline — I spent a week automating something before validating that the personalised invite actually outperformed the standard one. Do the manual version first.

The details matter. The system matters. But the thinking behind it matters more.

Keep reading

I write one email every two weeks about what I'm building.

No frameworks. No hot takes. Just what I tried, what happened, and what I'd change.

Joining a few hundred builders who like seeing how things actually work.