> ## 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.

# Epic Progress → Campaign Alignment

> Pull active epics from Jira, calculate child issue progress, and generate campaign strategy with Mave Agent

## Epic Progress → Campaign Alignment

### Scenario

Epics represent major product initiatives — the kind marketing needs to build campaigns around. This job pulls active epics via JQL, calculates progress as percentage of child issues completed, then sends the data to Mave Agent for a campaign strategy aligned with engineering velocity.

**Flow:** Jira `POST /search` (type=Epic) → child issue progress → Mavera `POST /api/v1/mave/chat` → Campaign strategy

### Code

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

  DOMAIN, EMAIL = os.environ["JIRA_DOMAIN"], os.environ["JIRA_EMAIL"]
  TOKEN, MV = os.environ["JIRA_API_TOKEN"], os.environ["MAVERA_API_KEY"]
  JB, MB = f"https://{DOMAIN}.atlassian.net/rest/api/3", "https://app.mavera.io/api/v1"
  cred = base64.b64encode(f"{EMAIL}:{TOKEN}".encode()).decode()
  JH = {"Authorization": f"Basic {cred}", "Content-Type": "application/json"}
  MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
  PROJECT_KEY = "PROJ"

  r = requests.post(f"{JB}/search", headers=JH, json={
      "jql": f"project = {PROJECT_KEY} AND issuetype = Epic AND statusCategory != Done ORDER BY rank ASC",
      "startAt": 0, "maxResults": 20,
      "fields": ["summary", "status", "duedate"],
  })
  if r.status_code == 429:
      time.sleep(int(r.headers.get("Retry-After", 30)))
  r.raise_for_status()
  epics = r.json().get("issues", [])
  print(f"Found {len(epics)} active epics")

  epic_progress = []
  for epic in epics:
      time.sleep(0.5)
      cr = requests.post(f"{JB}/search", headers=JH, json={
          "jql": f'"Epic Link" = {epic["key"]}', "startAt": 0, "maxResults": 100, "fields": ["status"],
      })
      if cr.status_code == 429:
          time.sleep(int(cr.headers.get("Retry-After", 30)))
          continue
      children = cr.json().get("issues", [])
      total = len(children)
      done = sum(1 for c in children if c["fields"]["status"]["statusCategory"]["key"] == "done")
      pct = round((done / total) * 100, 1) if total else 0
      f = epic["fields"]
      epic_progress.append({"key": epic["key"], "summary": f["summary"],
          "due": f.get("duedate", "No date"), "total": total, "done": done, "pct": pct})

  progress_txt = "\n".join(
      f"  {e['key']}: {e['summary']} | {e['pct']}% ({e['done']}/{e['total']}) | Due: {e['due']}"
      for e in epic_progress)

  time.sleep(0.3)
  strategy = requests.post(f"{MB}/mave/chat", headers=MH, json={
      "message": f"Campaign strategist. Align marketing with {len(epic_progress)} epics.\n\n"
                 f"EPIC PROGRESS:\n{progress_txt}\n\n"
                 "Produce: 1) Ship date estimates 2) Campaign priority ranking 3) Pre-launch "
                 "checklist for >75% epics 4) Week-by-week timeline 5) Content matrix per epic "
                 "6) Risk assessment 7) Stagger vs big-bang recommendation",
  }).json()

  print(f"\n{'='*60}\nCAMPAIGN ALIGNMENT\n{'='*60}")
  print(strategy.get("content", "")[:3000])
  ```

  ```javascript JavaScript theme={"dark"}
  const DOMAIN = process.env.JIRA_DOMAIN, EMAIL = process.env.JIRA_EMAIL;
  const TOKEN = process.env.JIRA_API_TOKEN, MV = process.env.MAVERA_API_KEY;
  const JB = `https://${DOMAIN}.atlassian.net/rest/api/3`, MB = "https://app.mavera.io/api/v1";
  const cred = btoa(`${EMAIL}:${TOKEN}`);
  const JH = { Authorization: `Basic ${cred}`, "Content-Type": "application/json" };
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  let res = await fetch(`${JB}/search`, { method: "POST", headers: JH,
    body: JSON.stringify({ jql: "project = PROJ AND issuetype = Epic AND statusCategory != Done ORDER BY rank ASC",
      startAt: 0, maxResults: 20, fields: ["summary", "status", "duedate"] }),
  });
  if (res.status === 429) await new Promise(r => setTimeout(r, 30000));
  const epics = (await res.json()).issues || [];

  const epicProgress = [];
  for (const epic of epics) {
    await new Promise(r => setTimeout(r, 500));
    const cr = await fetch(`${JB}/search`, { method: "POST", headers: JH,
      body: JSON.stringify({ jql: `"Epic Link" = ${epic.key}`, startAt: 0, maxResults: 100, fields: ["status"] }),
    });
    if (cr.status === 429) continue;
    const children = (await cr.json()).issues || [];
    const done = children.filter(c => c.fields.status.statusCategory.key === "done").length;
    const pct = children.length ? Math.round((done / children.length) * 1000) / 10 : 0;
    epicProgress.push({ key: epic.key, summary: epic.fields.summary,
      due: epic.fields.duedate || "No date", total: children.length, done, pct });
  }

  const txt = epicProgress.map(e =>
    `  ${e.key}: ${e.summary} | ${e.pct}% (${e.done}/${e.total}) | Due: ${e.due}`).join("\n");

  const strategy = await fetch(`${MB}/mave/chat`, { method: "POST", headers: MH,
    body: JSON.stringify({ message: `Campaign strategist. ${epicProgress.length} epics.\n\n${txt}\n\n1) Ship dates 2) Priority ranking 3) Pre-launch for >75% 4) Timeline 5) Content matrix 6) Risks 7) Stagger vs big-bang.` }),
  }).then(r => r.json());

  console.log((strategy.content || "").slice(0, 3000));
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Found 6 active epics

## Ship Date Estimates
| Epic | Progress | Ship Date | Confidence |
|------|----------|-----------|------------|
| PROJ-135: SSO/SCIM | 91.7% (11/12) | Mar 19 | Very High |
| PROJ-100: API v3 | 88.5% (23/26) | Mar 24 | High |
| PROJ-112: Dashboard | 62.0% (31/50) | Apr 14 | Medium |

## Campaign Priority
1. SSO/SCIM (91.7%) — Start content NOW. Security angle.
2. API v3 (88.5%) — Developer blog + migration guide this week.

## Recommendation: STAGGER
Ship SSO first, API v3 one week later. Dashboard in April.
```

### Error Handling

<AccordionGroup>
  <Accordion title="Epic Link field name">In Jira Cloud, the Epic Link field may use a custom field ID (e.g. `customfield_10014`). If JQL fails, query `GET /rest/api/3/field` to discover the correct name for your instance.</Accordion>
  <Accordion title="Empty epics">Epics with zero child issues return 0% progress. Filter them out or flag as "not yet scoped" in the campaign strategy.</Accordion>
  <Accordion title="Status category mapping">Jira status categories (`done`, `indeterminate`, `new`) differ from status names. The code uses `statusCategory.key` for reliable progress regardless of custom workflows.</Accordion>
</AccordionGroup>
