> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mavera.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Story Clustering → Content Calendar

### Scenario

Perigon's `/stories/all` endpoint groups related articles into story clusters — each cluster represents a single narrative arc across multiple sources. This job pulls top story clusters for your industry, asks Mave to evaluate each cluster for content potential and timing, then generates a week's worth of content calendar entries with formats, angles, and publish dates.

**Flow:** Perigon `GET /stories/all` → Mavera `POST /mave/chat` (evaluate clusters) → `POST /generations` (calendar entries) → Content calendar

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time

  PG_KEY = os.environ["PERIGON_API_KEY"]
  PG_BASE = "https://api.goperigon.com/v1"
  MV = os.environ["MAVERA_API_KEY"]
  MV_BASE = "https://app.mavera.io/api/v1"
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  TOPIC = "artificial intelligence"
  BRAND = "Enterprise SaaS platform for marketing teams. Audience: VP Marketing, CMO. Tone: strategic, data-backed, forward-looking."

  # 1. Fetch story clusters
  r = requests.get(f"{PG_BASE}/stories/all", params={
      "apiKey": PG_KEY, "q": TOPIC, "sortBy": "createdAt",
      "size": 15, "sourceGroup": "top100",
  })
  r.raise_for_status()
  stories = r.json().get("results", [])
  print(f"Fetched {len(stories)} story clusters for '{TOPIC}'")

  # 2. Build cluster summaries
  cluster_text = []
  for s in stories:
      articles = s.get("articles", [])
      sources = [a.get("source", {}).get("name", "") for a in articles[:5]]
      cluster_text.append(
          f"CLUSTER: {s.get('name', s.get('title', 'Unnamed'))}\n"
          f"Articles: {s.get('articleCount', len(articles))}\n"
          f"Sources: {', '.join(set(sources))}\n"
          f"Summary: {s.get('summary', '')[:300]}\n"
          f"Created: {s.get('createdAt', '')[:10]}"
      )

  # 3. Mave evaluates clusters for content potential
  evaluation = requests.post(f"{MV_BASE}/mave/chat", headers=MV_H, json={
      "message": f"Content strategist. Evaluate these {len(stories)} news story clusters for content calendar potential.\n\n"
          f"BRAND: {BRAND}\n\n"
          + "\n\n".join(cluster_text)
          + "\n\nFor each cluster score content potential (1-10). For clusters 7+:\n"
          "- Suggested content format (blog, LinkedIn, webinar, infographic, podcast)\n"
          "- Angle that ties to our brand\n"
          "- Optimal publish timing (ride the wave vs. contrarian delay)\n"
          "- Target persona\n\n"
          "Return a ranked list."
  }).json()
  print(f"\nCluster Evaluation:\n{evaluation.get('content', '')[:1500]}")

  # 4. Generate calendar
  calendar = requests.post(f"{MV_BASE}/generations", headers=MV_H, json={
      "prompt": f"Build a 7-day content calendar from these evaluated story clusters.\n\n"
          f"EVALUATION:\n{evaluation.get('content', '')[:3000]}\n\n"
          f"BRAND: {BRAND}\n\n"
          "For each day (Mon-Sun):\n"
          "- Content piece title\n"
          "- Format (blog/LinkedIn/tweet thread/webinar/email)\n"
          "- Story cluster it references\n"
          "- Key message (1 sentence)\n"
          "- CTA\n"
          "- Estimated production time\n\n"
          "Ensure variety in formats. Front-load time-sensitive topics.",
  }).json()
  print(f"\n{'='*60}\nCONTENT CALENDAR\n{'='*60}")
  print(calendar.get("output", calendar.get("content", ""))[:2000])
  ```

  ```javascript JavaScript theme={"dark"}
  const PG_KEY = process.env.PERIGON_API_KEY;
  const PG_BASE = "https://api.goperigon.com/v1";
  const MV = process.env.MAVERA_API_KEY;
  const MV_BASE = "https://app.mavera.io/api/v1";
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const TOPIC = "artificial intelligence";
  const BRAND = "Enterprise SaaS for marketing teams. Audience: VP Marketing, CMO. Tone: strategic, data-backed.";

  // 1. Story clusters
  const stories = (await (await fetch(
    `${PG_BASE}/stories/all?apiKey=${PG_KEY}&q=${encodeURIComponent(TOPIC)}&sortBy=createdAt&size=15&sourceGroup=top100`
  )).json()).results || [];
  console.log(`Fetched ${stories.length} story clusters`);

  // 2. Cluster summaries
  const clusterText = stories.map(s => {
    const sources = (s.articles || []).slice(0, 5).map(a => a.source?.name || "");
    return `CLUSTER: ${s.name || s.title || "Unnamed"}\nArticles: ${s.articleCount || (s.articles||[]).length}\nSources: ${[...new Set(sources)].join(", ")}\nSummary: ${(s.summary||"").slice(0,300)}\nCreated: ${(s.createdAt||"").slice(0,10)}`;
  }).join("\n\n");

  // 3. Evaluate
  const evaluation = await fetch(`${MV_BASE}/mave/chat`, { method: "POST", headers: MV_H,
    body: JSON.stringify({ message: `Content strategist. Evaluate ${stories.length} story clusters.\n\nBRAND: ${BRAND}\n\n${clusterText}\n\nScore content potential (1-10). For 7+: format, angle, timing, persona. Ranked list.` }),
  }).then(r => r.json());
  console.log(`\nCluster Evaluation:\n${(evaluation.content||"").slice(0,1500)}`);

  // 4. Calendar
  const calendar = await fetch(`${MV_BASE}/generations`, { method: "POST", headers: MV_H,
    body: JSON.stringify({ prompt: `7-day content calendar from clusters.\n\nEVALUATION:\n${(evaluation.content||"").slice(0,3000)}\n\nBRAND: ${BRAND}\n\nPer day (Mon-Sun): title, format, cluster ref, key message, CTA, production time. Variety. Front-load time-sensitive.` }),
  }).then(r => r.json());
  console.log(`\n${"=".repeat(60)}\nCONTENT CALENDAR\n${"=".repeat(60)}`);
  console.log((calendar.output || calendar.content || "").slice(0, 2000));
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Fetched 15 story clusters for 'artificial intelligence'

Cluster Evaluation:
1. "OpenAI Enterprise Pricing Restructure" (9/10)
   Format: Blog + LinkedIn | Angle: Cost analysis for marketing leaders
   Timing: Publish within 48 hours (news cycle peak)

2. "Google Gemini Marketing Tools Launch" (8/10)
   Format: Webinar + Tweet thread | Angle: Competitive comparison
   Timing: Day 3 — let early takes publish first, then add depth

CONTENT CALENDAR
============================================================
Monday: "What OpenAI's New Pricing Means for Your Marketing Stack"
  Format: Blog (1,500 words) | CTA: ROI calculator
  Production: 4 hours

Tuesday: "Thread: 5 Things Marketers Missed About Gemini's Launch"
  Format: Tweet thread (8 tweets) | CTA: Newsletter signup
  Production: 1 hour
```

### Error Handling

<AccordionGroup>
  <Accordion title="Empty clusters">Some queries return clusters with zero articles in the response. Check `articleCount` before processing. Fall back to `/all` for raw articles.</Accordion>
  <Accordion title="Rate limits">Starter plans: 100 req/day. This job uses 1 Perigon call + 2 Mavera calls. Safe for daily runs on any plan.</Accordion>
  <Accordion title="Cluster freshness">Clusters with `updatedAt` older than 72 hours may have peaked. Use `sortBy=createdAt` to prioritize fresh narratives.</Accordion>
</AccordionGroup>
