Back to blogs
AI / Retail
8 min read
May 5, 2025

AI-Generated Weekly Sales Campaigns with a Human in the Loop

How Afto uses customer segmentation, LLM-generated copy, and Canva API flyers to produce weekly campaigns for retailers—with an approval workflow and a 3-strike 24-hour reminder system before auto-execution.

AILLMCanva APICampaign AutomationSegmentationHuman in the LoopWhatsApp
Table of contents

One of Afto's core value propositions is giving SMB retailers the marketing capabilities of a large brand team—without the team. The weekly campaign feature sits at the center of that promise: every week, the system automatically proposes a targeted campaign for each retailer, generates the copy and flyer, and waits for a human thumbs-up before sending.

Here's the full pipeline.

The Weekly Campaign Cycle

The pipeline runs every Sunday evening, ahead of the Monday morning campaign send window:

1. Segment customers by purchase history and recency
2. Select a campaign angle (promotion type, product focus)
3. Generate campaign copy with GPT-4o
4. Generate flyer image via Canva API
5. Create approval task for retailer
6. Send 3 reminders over 24 hours
7. Auto-execute or expire based on retailer action

Customer Segmentation

We segment each retailer's customer base into four tiers before generating any content:

type CustomerSegment =
  | "champions"      // bought recently, high frequency, high value
  | "at_risk"        // used to buy regularly, gone quiet
  | "new_customers"  // first or second purchase in last 30 days
  | "sleepers";      // no purchase in 90+ days
 
function classifyCustomer(customer: CustomerMetrics): CustomerSegment {
  const daysSinceLast = daysBetween(customer.last_purchase_date, new Date());
  const purchaseCount = customer.total_purchases;
  const avgOrderValue = customer.total_spend / purchaseCount;
 
  if (daysSinceLast <= 14 && purchaseCount >= 5 && avgOrderValue >= 500) {
    return "champions";
  }
  if (daysSinceLast > 30 && daysSinceLast <= 90 && purchaseCount >= 3) {
    return "at_risk";
  }
  if (purchaseCount <= 2 && daysSinceLast <= 30) {
    return "new_customers";
  }
  return "sleepers";
}

The campaign targeting and tone change per segment. Champions get early access messaging. At-risk customers get win-back offers. New customers get educational onboarding with a soft incentive. Sleepers get a re-engagement push.

Campaign Angle Selection

Given the segment and the retailer's recent inventory changes, we pick a campaign angle from a curated set of templates and pass it to the LLM:

type CampaignAngle =
  | "flash_sale"
  | "new_arrival"
  | "loyalty_reward"
  | "restock_alert"
  | "seasonal_offer"
  | "referral_push";
 
function selectAngle(
  segment: CustomerSegment,
  recentActivity: RetailerActivity
): CampaignAngle {
  if (recentActivity.new_products_added > 3) return "new_arrival";
  if (segment === "at_risk") return "loyalty_reward";
  if (segment === "sleepers") return "flash_sale";
  if (recentActivity.high_stock_items.length > 0) return "restock_alert";
  return "seasonal_offer";
}

This deterministic pre-selection means the LLM focuses on tone, personalization, and persuasion—not on figuring out what kind of campaign to run.

LLM Campaign Copy Generation

We give GPT-4o a structured prompt with retailer context, segment data, and the selected angle:

const CAMPAIGN_PROMPT = (ctx: CampaignContext) => `
You are writing a WhatsApp marketing message for ${ctx.retailerName}, a ${ctx.businessCategory} retailer.
 
Target segment: ${ctx.segment} (${ctx.segmentSize} customers)
Campaign angle: ${ctx.angle}
Top products to feature: ${ctx.topProducts.map((p) => p.name).join(", ")}
Retailer tone: ${ctx.brandTone} (e.g., friendly, professional, playful)
 
Write:
1. A WhatsApp message (max 160 characters, include 1 emoji)
2. An SMS fallback (max 100 characters, no emoji)
3. A short subject line for email (max 50 characters)
4. A campaign headline for the flyer (max 8 words, bold and punchy)
 
Return as JSON with keys: whatsapp_message, sms_message, email_subject, flyer_headline.
`;

The structured JSON response goes directly into our approval payload without human parsing. We validate every field length before presenting it to the retailer.

Canva API Flyer Generation

The flyer is what retailers care about most—it's the visual face of the campaign. We use Canva's Design API with a set of pre-approved brand templates:

async function generateFlyer(
  headline: string,
  productImageUrl: string,
  retailerBranding: RetailerBranding
): Promise<string> {
  const design = await canvaClient.createDesign({
    design_type: { type: "preset", name: "instagram_post" },
  });
 
  await canvaClient.autofillDesign({
    design_id: design.design.id,
    title: "Weekly Campaign Flyer",
    brand_template_id: retailerBranding.canva_template_id,
    data: {
      headline: { type: "text", text: headline },
      product_image: { type: "image", asset_id: productImageUrl },
      retailer_name: { type: "text", text: retailerBranding.name },
      discount_badge: { type: "text", text: retailerBranding.discount_text ?? "" },
    },
  });
 
  const exported = await canvaClient.exportDesign({
    design_id: design.design.id,
    format: "jpg",
    export_quality: "regular",
  });
 
  return exported.job.status === "success" ? exported.job.urls[0] : throw new Error("Canva export failed");
}

Each retailer has a Canva template pre-configured with their brand colors, logo, and font. The autofill API slots in the generated headline and the week's featured product image.

The Approval Workflow

Once the copy and flyer are ready, we create an approval task visible in the retailer's Afto dashboard. The retailer sees:

  • Campaign preview (flyer + message text)
  • Estimated send time
  • Target segment count
  • One-click Approve / Edit / Reject

If they approve, the campaign queues immediately. If they edit, they can tweak the message in a text field. If they reject, we log the reason for model fine-tuning later.

3-Strike Reminder System

Most retailers are busy. Many don't check the dashboard daily. We built a progressive reminder system:

async function scheduleApprovalReminders(campaignId: string, deadline: Date) {
  const retailer = await getCampaignRetailer(campaignId);
 
  const reminders = [
    { offsetHours: 0, channel: "whatsapp", urgency: "low" },
    { offsetHours: 12, channel: "whatsapp", urgency: "medium" },
    { offsetHours: 20, channel: "sms", urgency: "high" },
  ];
 
  for (const { offsetHours, channel, urgency } of reminders) {
    const sendAt = addHours(deadline, -offsetHours + offsetHours); // relative to send window
    await jobQueue.schedule("send_approval_reminder", sendAt, {
      campaignId,
      retailerId: retailer.id,
      channel,
      urgency,
    });
  }
 
  // Auto-execute or expire at deadline
  await jobQueue.schedule("campaign_deadline_handler", deadline, { campaignId });
}

The first reminder (at approval creation) is low-urgency: "Your weekly campaign is ready for review." The second (12 hours later) adds "It will send automatically in 12 hours unless you review it." The third (4 hours before send) is via SMS with "FINAL: Campaign sends in 4 hours."

At the deadline, if there's no action, we auto-execute the campaign. Retailers opted into this behavior during onboarding—it was the feature they specifically asked for. Most of them wanted the system to just work, with the ability to intervene, not a system that waits passively forever.

Results

In the first four weeks with pilot retailers, 78% of campaigns were approved without editing. 14% were edited (mostly small text tweaks). Only 8% were rejected. The auto-execution path kicked in for about 22% of approved campaigns—meaning the reminder system successfully got retailer attention for 78% of them within 24 hours.

Campaign click-through rates on AI-generated content outperformed the retailer's previous manual campaigns by 23% on average, largely because the segmentation was more precise than what they'd been doing manually.

Key Takeaways

  • Human-in-the-loop with a deadline is more practical than pure automation for SMBs. They want oversight but don't want to be blocked by it.
  • Deterministic angle selection + LLM tone generation produces better results than asking the LLM to choose everything. Constrain the creative space.
  • Progressive reminders with escalating urgency dramatically improve approval rates without annoying retailers.
  • Canva's autofill API is genuinely impressive for producing branded visual content at scale—no designer required per campaign.