Alejandro Rioja.
AI Agents Growth

AI 에이전트로 뉴스레터를 자동화하는 방법

Alejandro Rioja
Alejandro Rioja
4 분 읽기
TL;DR

Claude 에이전트가 콘텐츠 큐를 읽고, 주간 최적의 각도를 선택하며, 내 목소리로 뉴스레터를 초안 작성하고, 참여도 계층별로 목록을 세분화하며, Kit API를 통해 발송을 예약합니다 — 내가 에디터를 열지 않아도 됩니다. 렌더링된 미리보기를 확인하고 승인을 누르면 됩니다. 어려운 창의적 작업은 내 몫이고, 기계적 실행은 에이전트의 몫입니다.

무료 뉴스레터

매주 수요일. 28,400명+ 구독자. 핵심만.

목차

2026년 6월 업데이트.

TL;DR: Claude 에이전트가 콘텐츠 큐를 읽고, 주간 최적의 각도를 선택하며, 내 목소리로 뉴스레터를 초안 작성하고, 참여도 계층별로 목록을 세분화하며, Kit API를 통해 발송을 예약합니다 — 내가 에디터를 열지 않아도 됩니다. 렌더링된 미리보기를 확인하고 승인을 누르면 됩니다. 어려운 창의적 작업은 내 몫이고, 기계적 실행은 에이전트의 몫입니다.

[운영자 노트] 일관되게 발송되는 뉴스레터가 “더 좋지만” 영감이 올 때만 발송되는 것보다 낫습니다. 제약은 아이디어가 아니라 실행 오버헤드였습니다. 아이디어는 있었지만 매주 형식을 잡고, 일정을 잡고, 세분화할 여력이 없었습니다. 에이전트가 그 격차를 없앴습니다.

대부분의 뉴스레터 워크플로우의 실제 병목

대부분의 뉴스레터 자동화 조언은 잘못된 것에 집중합니다: 환영 시퀀스, 자동화, 태깅 로직. 나쁘지 않지만 주 단위 창작 문제를 해결하지 못합니다.

실제 걸림돌은 이것입니다: 무슨 말을 할지는 알지만, 앉아서 형식을 잡고, 제목 줄 변형을 작성하고, 올바른 세그먼트를 선택하고, 올바른 시간에 예약하면 주당 2-3시간의 컨텍스트 전환이 필요합니다. 52주를 곱하면 뉴스레터를 발송하는 데만 꼬박 1주일을 쓴 셈입니다.

에이전트는 “이번 주 각도가 무엇인지 알겠다” 이후의 모든 단계를 처리합니다.

사용 중인 스택

Kit를 사용하지 않더라도 브로드캐스트를 생성하고 예약할 REST API가 있는 플랫폼이라면 같은 패턴이 작동합니다.

1단계: 콘텐츠 큐

에이전트는 “무엇을 쓸 것인지”에 대한 진실의 원천이 필요합니다. 제 것은 다음 열을 가진 Airtable 테이블입니다:

매주 10분을 들여 큐에 2-3개의 주제를 추가합니다. 그것이 제 창의적 기여입니다. 나머지는 에이전트의 일입니다.

2단계: 초안 에이전트

typescript
// workers/newsletter-agent/index.ts
import Anthropic from "@anthropic-ai/sdk";
import Airtable from "airtable";

const client = new Anthropic();

const VOICE_SYSTEM = `You are writing a weekly newsletter for Alejandro Rioja's subscribers.
His audience: founders and operators interested in AI agents, SEO, and growing a one-person business.
Voice: direct, first-person, practitioner. No hype, no "exciting times," no excessive bullet lists.
Structure every newsletter as:
1. One-sentence hook (the problem or observation)
2. The core insight (3–5 paragraphs, no headers, conversational)
3. One concrete action the reader can take this week
4. A short sign-off (2 sentences max)
Subject line: specific, outcome-oriented, under 50 chars. No clickbait.
Return JSON: { "subject": "...", "preheader": "...", "body": "..." }`;

async function getNextTopic(): Promise<{ id: string; topic: string; notes: string; tier: string }> {
  const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base(process.env.AIRTABLE_BASE_ID!);
  const records = await base("Newsletter Queue")
    .select({ filterByFormula: "{Status} = 'Queue'", sort: [{ field: "Created", direction: "asc" }], maxRecords: 1 })
    .firstPage();
  if (!records.length) throw new Error("Queue is empty. Add topics.");
  const r = records[0];
  return { id: r.id, topic: r.get("Topic") as string, notes: (r.get("Notes") as string) ?? "", tier: (r.get("Tier") as string) ?? "all" };
}

async function draftNewsletter(topic: string, notes: string): Promise<{ subject: string; preheader: string; body: string }> {
  const msg = await client.messages.create({
    model: "claude-sonnet-4-6",
    max_tokens: 2048,
    system: VOICE_SYSTEM,
    messages: [{ role: "user", content: `Write this week's newsletter on: "${topic}". Additional notes: ${notes || "none"}` }],
  });
  const text = (msg.content[0] as any).text.replace(/```json\n?/, "").replace(/```/, "").trim();
  return JSON.parse(text);
}

async function scheduleWithKit(draft: { subject: string; preheader: string; body: string }, tier: string): Promise<string> {
  const segmentId = tier === "engaged" ? process.env.KIT_ENGAGED_SEGMENT_ID : null;
  const sendAt = new Date();
  sendAt.setDate(sendAt.getDate() + ((4 - sendAt.getDay() + 7) % 7)); // next Thursday
  sendAt.setHours(9, 0, 0, 0); // 9am CT

  const payload: any = {
    broadcast: {
      subject: draft.subject,
      content: draft.body,
      description: draft.preheader,
      send_at: sendAt.toISOString(),
      email_layout_template: "minimal",
    },
  };
  if (segmentId) payload.broadcast.segment_id = segmentId;

  const res = await fetch("https://api.kit.com/v4/broadcasts", {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Kit-Api-Key": process.env.KIT_API_KEY! },
    body: JSON.stringify(payload),
  });
  const data = await res.json();
  return data.broadcast?.id ?? "";
}

export default {
  async scheduled(_event: ScheduledEvent, env: Env) {
    // Inject env vars
    Object.assign(process.env, env);
    const { id, topic, notes, tier } = await getNextTopic();
    const draft = await draftNewsletter(topic, notes);
    const broadcastId = await scheduleWithKit(draft, tier);

    // Mark as Approved in Airtable (not Sent — human reviews the Kit preview before confirm)
    const base = new Airtable({ apiKey: env.AIRTABLE_API_KEY }).base(env.AIRTABLE_BASE_ID);
    await base("Newsletter Queue").update(id, { Status: "Approved", KitBroadcastId: broadcastId });

    console.log(`Scheduled broadcast ${broadcastId} for topic: ${topic}`);
  },
};

3단계: 승인 단계

에이전트는 Kit의 초안 상태로 브로드캐스트를 생성하고 Airtable 레코드를 “Approved”로 표시합니다. Kit가 미리보기 링크가 포함된 알림을 보냅니다. 클릭해서 읽고, 괜찮아 보이면 발송을 확인합니다. 변경하고 싶으면 Kit에서 직접 편집합니다.

이것이 에이전트가 발신 이메일에서 완전히 자율적이 되지 않도록 막는 게이트입니다. 초안을 약 90%의 시간 동안 신뢰합니다. 검토에서 발견하는 10% — 약간 어긋난 톤, 확인하고 싶은 통계, 추가하고 싶은 링크 — 는 3분 검토의 가치가 있습니다.

에이전트가 처리하는, 다시는 하고 싶지 않은 것들

여전히 내 것인 것

아이디어. 큐의 주제는 내 것입니다. 각도는 내 것입니다. 에이전트는 명확한 브리프의 훌륭한 실행자입니다. 전략 레이어가 아닙니다. 큐에 나쁜 주제를 넣으면 나쁜 주제에 대해 잘 쓰인 뉴스레터를 얻습니다.

또한: 첫 번째 검토 게이트. 모든 발송은 내 눈을 통해 나가기 전에 확인됩니다. 이것은 바뀌지 않을 것입니다.

운영자의 결론

뉴스레터 기계적 작업 — 형식 지정, 일정 잡기, 세분화 — 에 주당 1시간 이상을 쓰고 있다면 자동화해야 합니다. Kit API는 깔끔하고, Worker cron 트리거는 바위처럼 견고하며, Claude 초안 품질은 약 90%의 첫 초안을 변경 없이 승인할 수 있을 만큼 높습니다. Airtable에서 큐를 구축하고, Worker를 연결하고, 발송을 실행하는 대신 아이디어를 만드는 일로 돌아가십시오.

계속 읽기

AI 플레이북을 받아보세요

매주 수요일. 28,400명+ 구독자. 핵심만.

↵ 전체 결과 보기 esc esc 닫기