Skip to content

feat: Lemlist integration — personalized cold email (issue #20)#75

Open
jackulau wants to merge 1 commit into
wespreadjam:mainfrom
jackulau:20
Open

feat: Lemlist integration — personalized cold email (issue #20)#75
jackulau wants to merge 1 commit into
wespreadjam:mainfrom
jackulau:20

Conversation

@jackulau

Copy link
Copy Markdown

Closes #20

Overview

Adds a Lemlist cold-email integration to @jam-nodes/nodes with six operations, an API-key credential, fully-typed Zod input/output schemas, and 57 unit tests. Follows the existing devto integration pattern exactly — defineApiKeyCredential, defineNode, shared fetchWithRetry, colocated __tests__/.

What's in here

Six nodes

Node HTTP Endpoint
lemlistAddLeadNode POST /campaigns/{campaignId}/leads/
lemlistGetCampaignsNode GET /campaigns?version=v2
lemlistGetActivityNode GET /activities?version=v2
lemlistPauseLeadNode POST /leads/pause/{leadId}[?campaignId=]
lemlistResumeLeadNode POST /leads/start/{leadId}[?campaignId=]
lemlistMarkAsInterestedNode POST /campaigns/{campaignId}/leads/{leadIdOrEmail}/interested

Each node is defined in its own file under packages/nodes/src/integrations/lemlist/, registered in the local index.ts, the integrations barrel (integrations/index.ts), the package root (src/index.ts), and the builtInNodes array.

Credential

lemlistCredential is defined via defineApiKeyCredential with a Zod schema ({ apiKey: z.string().min(1) }). NodeCredentials in @jam-nodes/core was extended with lemlist?: { apiKey: string } so executors access credentials type-safely via context.credentials?.lemlist?.apiKey.

Authentication

Lemlist's documented auth scheme is HTTP Basic with an empty username and the API key as the password — not a bearer token or raw header (the issue's example credential snippet was a simplification). A shared buildLemlistAuthHeader() helper in utils.ts computes \Basic ${base64(':' + apiKey)}`at request time. The credential definition'sauthenticate.properties` records the header name for UI/documentation purposes; the runtime encoding happens in the helper.

Zod schemas

schemas.ts defines three shared response schemas (LemlistLeadSchema, LemlistCampaignSchema, LemlistActivitySchema) and one input/output pair per node. Response-shape fields that Lemlist may omit use .nullable().optional() so we stay permissive without resorting to z.any() or as any (both forbidden per CLAUDE.md). All outputs are validated with LemlistXSchema.parse(...) before returning.

Tests — 57 new, all passing

packages/nodes/src/integrations/lemlist/__tests__/lemlist.test.ts covers:

  • Credential — metadata assertions, schema rejects empty key, schema accepts valid key
  • UtilsbuildLemlistAuthHeader encoding round-trip (decode the base64 portion and assert it equals ':<key>'), buildLemlistHeaders returns both Authorization and Content-Type, LEMLIST_API_BASE constant
  • Per-node — for each of the six nodes: Zod input validation, missing-credential path, success path (asserting URL, method, headers, body), error paths (non-2xx 400/401/404/405/422, server 500), URL-encoding of path identifiers (email addresses for mark-as-interested, special characters for pause-lead/resume-lead), optional query param forwarding, response normalization
  • Package-level exports — dynamic import from @jam-nodes/nodes root to prove all six nodes and lemlistCredential are exported, and that builtInNodes contains all six
  • Cross-node integration tests (4) — all six nodes loop-tested for: correct shape (category, capabilities, description, unique type prefix), identical "missing credential" behavior, Basic auth header derivation verified by decoding the header back to :test-api-key, 429-then-200 retry proving fetchWithRetry is wired in on every node

Drive-by fix

packages/nodes/src/integrations/google-sheets/googleSheets.ts had a pre-existing broken import path ('../types/credentials.js' — a file that does not exist) that prevented any code path from dynamically loading the package root. Corrected to '@jam-nodes/core'. This was required to enable the cross-package export tests to load the root index.ts without crashing, and as a side-effect reduces the project's pre-existing typecheck error count from 18 to 17.

Cross-cutting concerns / architectural compliance

Per the repo's CLAUDE.md and the decisions in issue #37 / PR #39:

  • Nodes stay pure — no retry/cache/timeout/rate-limit logic lives inside any Lemlist node.
  • All HTTP goes through the shared fetchWithRetry helper, which handles 429 backoff (honoring retry-after), 5xx retries with exponential backoff, auth-error short-circuits, and configurable timeouts.
  • No z.any() or as any anywhere in the new code.

Test results

Test Files  10 passed (10)
     Tests  252 passed (252)
  Duration  ~5s

Baseline: 195 passing (main). New total: 252 (+57 lemlist). Zero regressions in any pre-existing test file.

Typecheck note

Pre-existing tsc --noEmit errors in apify/, google-sheets/ (unrelated to my changes), and slack/ (NodeCredentials.slack not yet added to the core types) are still present. None of them are in any file I touched. Baseline: 18 errors → this branch: 17 errors (one fewer, from the google-sheets import fix above). Zero new errors introduced.

Files changed

Added (11):

  • packages/nodes/src/integrations/lemlist/credentials.ts
  • packages/nodes/src/integrations/lemlist/utils.ts
  • packages/nodes/src/integrations/lemlist/schemas.ts
  • packages/nodes/src/integrations/lemlist/add-lead.ts
  • packages/nodes/src/integrations/lemlist/get-campaigns.ts
  • packages/nodes/src/integrations/lemlist/get-activity.ts
  • packages/nodes/src/integrations/lemlist/pause-lead.ts
  • packages/nodes/src/integrations/lemlist/resume-lead.ts
  • packages/nodes/src/integrations/lemlist/mark-as-interested.ts
  • packages/nodes/src/integrations/lemlist/index.ts
  • packages/nodes/src/integrations/lemlist/__tests__/lemlist.test.ts

Modified (4):

  • packages/core/src/types/node.ts — added lemlist entry to NodeCredentials
  • packages/nodes/src/integrations/index.ts — Lemlist export block
  • packages/nodes/src/index.ts — value exports, type exports, import block, builtInNodes array
  • packages/nodes/src/integrations/google-sheets/googleSheets.ts — fix pre-existing broken import path

Acceptance Criteria (from issue #20)

  • Credential definition
  • All 6 operations
  • Zod schemas
  • Unit tests

Adds a Lemlist cold-email integration with 6 operations backed by 57 unit
tests:
- lemlistAddLeadNode:          POST /campaigns/{id}/leads/
- lemlistGetCampaignsNode:     GET  /campaigns?version=v2
- lemlistGetActivityNode:      GET  /activities?version=v2
- lemlistPauseLeadNode:        POST /leads/pause/{leadId}
- lemlistResumeLeadNode:       POST /leads/start/{leadId}
- lemlistMarkAsInterestedNode: POST /campaigns/{id}/leads/{leadIdOrEmail}/interested

Also includes lemlistCredential (apiKey) and Zod schemas for all inputs/outputs.
Auth uses HTTP Basic with empty username + API key as password (Lemlist's
real auth scheme, not the simplified example in the issue). All HTTP goes
through the shared fetchWithRetry helper for retries/timeouts/rate-limit
handling — nodes stay pure per the architectural decision in CLAUDE.md.

URL-encoding of path identifiers (leadId, campaignId, leadIdOrEmail) uses
encodeURIComponent, with test coverage for special characters including
email-form leads and characters like '+' and '/'.

Drive-by: fixes a pre-existing broken import path in
packages/nodes/src/integrations/google-sheets/googleSheets.ts
('../types/credentials.js' -> '@jam-nodes/core') that was blocking
package-root module loads and therefore blocking cross-package export
verification for the new Lemlist tests.

Tests: 252 passing (+57 new), 0 regressions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Integration] Lemlist - Personalized cold email

1 participant