Skip to content
Saad Sultan
Back to Projects

Multi-Channel Outreach Campaign System

LinkedIn and email, multiple steps, different rate limits. Keeping campaigns in sync without throttling or duplicate sends.

Overview

I built the backend and core frontend for a multi-channel outreach product inside an HR tech platform. Recruiters define sequences of steps (e.g. LinkedIn message, then email, then follow-up), set schedules and timezone, and launch campaigns. The system talks to a LinkedIn automation partner and to Nylas for email; we store campaign config, step state, and events so we can show progress and analytics without hitting external APIs on every page load.

Tech Stack

Node.jsTypeScriptNestJSMongoDBNylasSegmentReact

Impact & Scale

  • Recruiters run sequenced outreach across LinkedIn and email from one place
  • Campaigns respect timezone and rate limits to avoid throttling and improve deliverability
  • Real-time dashboards show sends, opens, and replies per campaign

Key Challenges & Solutions

Orchestrating steps and channels

Each step could be a different channel (LinkedIn vs email) with different rate limits and APIs. We modelled steps as a ordered list with a state machine (pending, sent, opened, replied, skipped) and a job processor that respected per-channel limits and backoff.

Timezone and send windows

Sending at the wrong time hurt engagement. We stored the recruiter’s timezone and computed send windows per step so the job runner only dispatched when it was within the allowed window in that timezone.

Keeping analytics in sync

Opens and replies came back asynchronously from the email and LinkedIn providers. We ingested those events (webhooks and polling where needed), normalised them, and updated campaign and step state so the UI could show live metrics without extra API calls.

Technical Highlights

  • Campaign and sequence model with step ordering, channel type, and template content
  • Background job processor with per-channel rate limiting and timezone-aware scheduling
  • Integration with Nylas for email and a LinkedIn automation partner for messaging
  • Event pipeline for opens, clicks, and replies; aggregated metrics for campaign dashboards
  • React UI for creating sequences, attaching templates, and viewing campaign analytics

Role and scope

I designed and implemented the campaign engine and its integrations. That included the data model for campaigns and steps, the job runner that sent messages on schedule, and the ingestion of external events into our analytics. The frontend for building sequences and viewing results was built in parallel with the backend.

Architecture

Campaigns are stored in MongoDB with steps as embedded or referenced documents. A worker process runs on a schedule, evaluates which steps are due (using timezone and send windows), and calls the appropriate channel API. Incoming webhooks and polling jobs update step state and write events into a store that the API reads for dashboard and reporting.

Lessons learned

  • Rate limits are part of the domain. Modelling them explicitly (per channel, per account) avoided production throttling and made it easier to add new channels later.
  • Timezone is a first-class field. Storing it once per campaign and deriving send windows from it removed a large class of "why did this send at 3am?" bugs.
  • Events over polling. Pushing opens and replies into our system as events kept the UI fast and reduced dependency on third-party API availability for real-time views.

Interested in discussing this project or working together? Get in touch.