Back to Blog
ai-agentsoptimizationworkflows

5 AI Workflows for Ad Optimization

Five practical AI workflows for advertising optimization: budget allocation, creative testing, audience discovery, automated reporting, and anomaly detection. Includes code examples using Xylo.

Xylo Team|February 27, 2026|9 min read

Beyond Manual Ad Management

Most advertising optimization follows predictable patterns: check performance, identify problems, make adjustments, verify results. These patterns are well-suited for AI automation -- not because they are simple, but because they require analyzing data and making decisions based on rules and context.

Here are five practical AI workflows you can implement today using Xylo's API or MCP server. Each workflow includes the logic, code examples, and guidance on when to use it.

Workflow 1: Intelligent Budget Allocation

Problem: Budget is distributed evenly across campaigns regardless of performance, wasting spend on underperformers.

AI approach: Reallocate budget proportionally based on conversion efficiency, with guardrails to prevent over-concentration.

const TOTAL_DAILY_BUDGET = 1000;
const MIN_CAMPAIGN_BUDGET = 20;
const MAX_BUDGET_SHARE = 0.40; // no single campaign gets more than 40%

async function allocateBudgets() {
  const response = await fetch(
    "https://api.xyloapi.dev/v1/campaigns?date_preset=last_7d&status=active",
    {
      headers: {
        "x-api-key": process.env.XYLO_API_KEY!,
        "x-ad-account": process.env.AD_ACCOUNT!,
      },
    }
  );
  const { data: campaigns } = await response.json();

  // Calculate efficiency scores
  const scored = campaigns
    .filter((c: any) => c.insights.spend > 0)
    .map((c: any) => ({
      id: c.id,
      name: c.name,
      currentBudget: c.daily_budget,
      conversions: c.insights.conversions,
      spend: c.insights.spend,
      efficiency: c.insights.conversions / c.insights.spend,
    }));

  // Zero-conversion campaigns get minimum budget
  const zeroConversion = scored.filter((c: any) => c.conversions === 0);
  const converting = scored.filter((c: any) => c.conversions > 0);

  const reservedForZero = zeroConversion.length * MIN_CAMPAIGN_BUDGET;
  const availableBudget = TOTAL_DAILY_BUDGET - reservedForZero;

  // Allocate proportionally to efficiency
  const totalEfficiency = converting.reduce((sum: number, c: any) => sum + c.efficiency, 0);
  const maxBudget = TOTAL_DAILY_BUDGET * MAX_BUDGET_SHARE;

  const allocations = converting.map((c: any) => ({
    ...c,
    newBudget: Math.min(
      Math.max((c.efficiency / totalEfficiency) * availableBudget, MIN_CAMPAIGN_BUDGET),
      maxBudget
    ),
  }));

  // Apply allocations
  for (const campaign of [...allocations, ...zeroConversion.map((c: any) => ({ ...c, newBudget: MIN_CAMPAIGN_BUDGET }))]) {
    if (Math.abs(campaign.newBudget - campaign.currentBudget) > 1) {
      await fetch(`https://api.xyloapi.dev/v1/campaigns/${campaign.id}`, {
        method: "PATCH",
        headers: {
          "x-api-key": process.env.XYLO_API_KEY!,
          "x-ad-account": process.env.AD_ACCOUNT!,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ daily_budget: Math.round(campaign.newBudget * 100) / 100 }),
      });
      console.log(`${campaign.name}: $${campaign.currentBudget} -> $${campaign.newBudget.toFixed(2)}`);
    }
  }
}

When to run: Daily, before peak traffic hours. Avoid running more than once per day -- Google and Meta's algorithms need time to stabilize after budget changes.

Guardrails: Cap individual campaign budgets at 40% of total. Maintain minimum budgets for testing. Limit daily changes to 30% of current budget.

Workflow 2: Creative Testing Framework

Problem: Ads run indefinitely without systematic testing. Winning creative is not identified quickly enough, and losing creative wastes spend.

AI approach: Monitor ad-level performance, flag statistical winners and losers, and pause underperformers automatically.

interface AdPerformance {
  id: string;
  name: string;
  adSetId: string;
  impressions: number;
  clicks: number;
  conversions: number;
  spend: number;
  ctr: number;
  cpa: number;
}

async function evaluateCreatives() {
  const response = await fetch(
    "https://api.xyloapi.dev/v1/ads?date_preset=last_7d&status=active",
    {
      headers: {
        "x-api-key": process.env.XYLO_API_KEY!,
        "x-ad-account": process.env.AD_ACCOUNT!,
      },
    }
  );
  const { data: ads } = await response.json();

  // Group ads by ad set for fair comparison
  const adsBySet: Record<string, AdPerformance[]> = {};
  for (const ad of ads) {
    const setId = ad.ad_set_id;
    if (!adsBySet[setId]) adsBySet[setId] = [];
    adsBySet[setId].push({
      id: ad.id,
      name: ad.name,
      adSetId: setId,
      impressions: ad.insights.impressions,
      clicks: ad.insights.clicks,
      conversions: ad.insights.conversions,
      spend: ad.insights.spend,
      ctr: ad.insights.ctr,
      cpa: ad.insights.conversions > 0
        ? ad.insights.spend / ad.insights.conversions
        : Infinity,
    });
  }

  // Evaluate each ad set
  for (const [setId, setAds] of Object.entries(adsBySet)) {
    if (setAds.length < 2) continue; // need at least 2 ads to compare

    // Only evaluate ads with enough data
    const eligible = setAds.filter((a) => a.impressions >= 1000);
    if (eligible.length < 2) continue;

    // Sort by CPA (lower is better)
    eligible.sort((a, b) => a.cpa - b.cpa);

    const best = eligible[0];
    const worst = eligible[eligible.length - 1];

    // If the worst performer has 2x+ the CPA of the best, pause it
    if (worst.cpa > best.cpa * 2 && worst.spend > 50) {
      await fetch(`https://api.xyloapi.dev/v1/ads/${worst.id}`, {
        method: "PATCH",
        headers: {
          "x-api-key": process.env.XYLO_API_KEY!,
          "x-ad-account": process.env.AD_ACCOUNT!,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ status: "paused" }),
      });
      console.log(
        `Paused "${worst.name}" (CPA: $${worst.cpa.toFixed(2)}) in favor of ` +
        `"${best.name}" (CPA: $${best.cpa.toFixed(2)})`
      );
    }
  }
}

When to run: Every 2-3 days. Creative needs enough impressions to be statistically meaningful.

Guardrails: Require minimum 1,000 impressions before evaluating. Never pause the last active ad in an ad set. Log all decisions for review.

Workflow 3: Audience Discovery

Problem: You are targeting the same audiences you always have. New high-performing segments exist but are undiscovered.

AI approach: Analyze conversion data to identify top-performing demographics, build lookalike audiences from converters, and test automatically.

async function discoverAudiences() {
  // Pull detailed insight breakdowns
  const response = await fetch(
    "https://api.xyloapi.dev/v1/insights?date_preset=last_30d&breakdowns=age,gender",
    {
      headers: {
        "x-api-key": process.env.XYLO_API_KEY!,
        "x-ad-account": process.env.AD_ACCOUNT!,
      },
    }
  );
  const { data: insights } = await response.json();

  // Find top-converting segments
  const segments = insights
    .filter((s: any) => s.conversions > 5 && s.spend > 50)
    .map((s: any) => ({
      age: s.age,
      gender: s.gender,
      conversions: s.conversions,
      spend: s.spend,
      cpa: s.spend / s.conversions,
      roas: s.conversion_value / s.spend,
    }))
    .sort((a: any, b: any) => a.cpa - b.cpa);

  console.log("Top converting segments:");
  for (const segment of segments.slice(0, 5)) {
    console.log(
      `  ${segment.gender}, ${segment.age}: ` +
      `CPA $${segment.cpa.toFixed(2)}, ${segment.conversions} conversions`
    );
  }

  // Create a lookalike from recent purchasers
  const lookalikeResponse = await fetch("https://api.xyloapi.dev/v1/audiences", {
    method: "POST",
    headers: {
      "x-api-key": process.env.XYLO_API_KEY!,
      "x-ad-account": process.env.AD_ACCOUNT!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: `Auto-Lookalike - Purchasers ${new Date().toISOString().slice(0, 10)}`,
      type: "lookalike",
      source_audience_id: process.env.PURCHASER_AUDIENCE_ID,
      country: "US",
      ratio: 0.01,
    }),
  });

  const { data: lookalike } = await lookalikeResponse.json();
  console.log(`Created lookalike audience: ${lookalike.id}`);
}

When to run: Monthly. Audience analysis needs a large enough data window to be meaningful.

Workflow 4: Automated Reporting

Problem: Manual reporting takes hours each week. By the time the report is ready, the data is stale.

AI approach: Pull cross-platform data, calculate derived metrics, identify trends, and generate a structured summary.

interface ReportSection {
  title: string;
  metrics: Record<string, number | string>;
  highlights: string[];
  concerns: string[];
}

async function generateWeeklyReport(): Promise<ReportSection[]> {
  const accounts = [
    { platform: "meta", id: "act_123456789" },
    { platform: "google", id: "customers/1234567890" },
  ];

  const sections: ReportSection[] = [];

  for (const account of accounts) {
    // This week
    const currentResponse = await fetch(
      "https://api.xyloapi.dev/v1/campaigns?date_preset=last_7d",
      {
        headers: {
          "x-api-key": process.env.XYLO_API_KEY!,
          "x-ad-account": account.id,
        },
      }
    );
    const current = await currentResponse.json();

    const totalSpend = current.data.reduce((s: number, c: any) => s + c.insights.spend, 0);
    const totalConversions = current.data.reduce((s: number, c: any) => s + c.insights.conversions, 0);
    const blendedCPA = totalConversions > 0 ? totalSpend / totalConversions : 0;
    const activeCampaigns = current.data.filter((c: any) => c.status === "active").length;

    const highlights: string[] = [];
    const concerns: string[] = [];

    // Best performers
    const sorted = current.data
      .filter((c: any) => c.insights.conversions > 0)
      .sort((a: any, b: any) => a.insights.cost_per_conversion - b.insights.cost_per_conversion);

    if (sorted.length > 0) {
      highlights.push(
        `Best campaign: "${sorted[0].name}" with $${sorted[0].insights.cost_per_conversion.toFixed(2)} CPA`
      );
    }

    // Zero-conversion campaigns
    const wasters = current.data.filter(
      (c: any) => c.status === "active" && c.insights.spend > 50 && c.insights.conversions === 0
    );
    if (wasters.length > 0) {
      concerns.push(
        `${wasters.length} campaigns spent $${wasters.reduce((s: number, c: any) => s + c.insights.spend, 0).toFixed(2)} with zero conversions`
      );
    }

    sections.push({
      title: `${account.platform.charAt(0).toUpperCase() + account.platform.slice(1)} Ads`,
      metrics: {
        "Total Spend": `$${totalSpend.toFixed(2)}`,
        "Conversions": totalConversions,
        "Blended CPA": `$${blendedCPA.toFixed(2)}`,
        "Active Campaigns": activeCampaigns,
      },
      highlights,
      concerns,
    });
  }

  return sections;
}

When to run: Weekly (Monday morning) and monthly (first business day).

Output options: Send to Slack, email, or store in a database for dashboarding.

Workflow 5: Anomaly Detection

Problem: Performance drops go unnoticed until the weekly review. By then, days of budget have been wasted.

AI approach: Compare current performance against recent baselines and alert when metrics deviate significantly.

interface Anomaly {
  campaignName: string;
  metric: string;
  currentValue: number;
  baselineValue: number;
  deviationPct: number;
  severity: "warning" | "critical";
}

async function detectAnomalies(): Promise<Anomaly[]> {
  // Current period (yesterday)
  const currentResponse = await fetch(
    "https://api.xyloapi.dev/v1/campaigns?date_preset=yesterday&status=active",
    {
      headers: {
        "x-api-key": process.env.XYLO_API_KEY!,
        "x-ad-account": process.env.AD_ACCOUNT!,
      },
    }
  );
  const current = await currentResponse.json();

  // Baseline period (last 7 days average, excluding yesterday)
  const baselineResponse = await fetch(
    "https://api.xyloapi.dev/v1/campaigns?date_preset=last_7d&status=active",
    {
      headers: {
        "x-api-key": process.env.XYLO_API_KEY!,
        "x-ad-account": process.env.AD_ACCOUNT!,
      },
    }
  );
  const baseline = await baselineResponse.json();

  const anomalies: Anomaly[] = [];

  for (const campaign of current.data) {
    const baselineCampaign = baseline.data.find((c: any) => c.id === campaign.id);
    if (!baselineCampaign) continue;

    // Calculate daily averages from 7-day baseline
    const avgSpend = baselineCampaign.insights.spend / 7;
    const avgConversions = baselineCampaign.insights.conversions / 7;
    const avgCTR = baselineCampaign.insights.ctr;

    // Check CPA spike (cost per conversion)
    if (campaign.insights.conversions > 0 && avgConversions > 0) {
      const currentCPA = campaign.insights.cost_per_conversion;
      const baselineCPA = baselineCampaign.insights.cost_per_conversion;
      const deviation = ((currentCPA - baselineCPA) / baselineCPA) * 100;

      if (deviation > 50) {
        anomalies.push({
          campaignName: campaign.name,
          metric: "CPA",
          currentValue: currentCPA,
          baselineValue: baselineCPA,
          deviationPct: deviation,
          severity: deviation > 100 ? "critical" : "warning",
        });
      }
    }

    // Check CTR drop
    if (campaign.insights.impressions > 500) {
      const ctrDeviation = ((avgCTR - campaign.insights.ctr) / avgCTR) * 100;
      if (ctrDeviation > 30) {
        anomalies.push({
          campaignName: campaign.name,
          metric: "CTR",
          currentValue: campaign.insights.ctr,
          baselineValue: avgCTR,
          deviationPct: -ctrDeviation,
          severity: ctrDeviation > 50 ? "critical" : "warning",
        });
      }
    }

    // Check spend surge
    if (avgSpend > 0) {
      const spendDeviation = ((campaign.insights.spend - avgSpend) / avgSpend) * 100;
      if (spendDeviation > 50) {
        anomalies.push({
          campaignName: campaign.name,
          metric: "Spend",
          currentValue: campaign.insights.spend,
          baselineValue: avgSpend,
          deviationPct: spendDeviation,
          severity: spendDeviation > 100 ? "critical" : "warning",
        });
      }
    }
  }

  return anomalies;
}

// Usage
const anomalies = await detectAnomalies();

if (anomalies.length > 0) {
  console.log(`Found ${anomalies.length} anomalies:`);
  for (const a of anomalies) {
    console.log(
      `[${a.severity.toUpperCase()}] ${a.campaignName}: ` +
      `${a.metric} is ${a.deviationPct > 0 ? "+" : ""}${a.deviationPct.toFixed(1)}% ` +
      `vs baseline (${a.currentValue.toFixed(2)} vs ${a.baselineValue.toFixed(2)})`
    );
  }
  // Send alerts via Slack, email, or PagerDuty
}

When to run: Daily, early morning. Catch yesterday's anomalies before today's budget is spent.

Alert thresholds:

  • Warning: 50%+ deviation from 7-day baseline
  • Critical: 100%+ deviation from baseline

Combining Workflows

These workflows are more powerful when combined:

  1. Morning: Anomaly detection catches issues from yesterday
  2. Mid-morning: Budget allocation adjusts based on latest performance
  3. Every 2-3 days: Creative testing pauses underperformers
  4. Weekly: Automated reporting summarizes performance and trends
  5. Monthly: Audience discovery identifies new targeting opportunities

Each workflow uses the same Xylo API, making the integration straightforward.

Getting Started

  1. Sign up for Xylo and connect your ad accounts.
  2. Start with anomaly detection (Workflow 5) -- it is read-only and provides immediate value.
  3. Add automated reporting (Workflow 4) next.
  4. Graduate to budget allocation (Workflow 1) when you are comfortable with automated changes.

For the AI agent approach using MCP, see building AI agents for ads. For cross-platform data handling, read our cross-platform reporting guide. Check the API documentation for endpoint details.

Ready to simplify your ads API integration?

Get started with Xylo in minutes. One API key for every ad platform.