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

# Campaign Task Tracking → Content Status

### Scenario

Your marketing team runs campaigns as Asana projects — each task is a content piece with custom fields for Content Type, Channel, Priority, and Due Date. You need a weekly status report that goes beyond "X tasks completed": you want insight into velocity trends, bottleneck stages, and content mix analysis. This job pulls all tasks from a campaign project, analyzes the pipeline state, and sends it to Mave for a strategic status report.

**Flow:** Asana `GET /projects/{id}/tasks` → Aggregate by status/custom fields → Mavera `POST /api/v1/mave/chat` → Campaign status report

### Code

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

  ASANA = os.environ["ASANA_PAT"]
  MV = os.environ["MAVERA_API_KEY"]
  AB = "https://app.asana.com/api/1.0"
  MB = "https://app.mavera.io/api/v1"
  AH = {"Authorization": f"Bearer {ASANA}"}
  MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  PROJECT_GID = "1234567890123456"

  # 1. Fetch all tasks with custom fields
  tasks = []
  offset = None
  while True:
      params = {
          "project": PROJECT_GID,
          "opt_fields": "name,completed,completed_at,due_on,assignee.name,"
                        "custom_fields.name,custom_fields.display_value,"
                        "memberships.section.name,tags.name",
          "limit": 100,
      }
      if offset:
          params["offset"] = offset
      r = requests.get(f"{AB}/tasks", headers=AH, params=params)
      if r.status_code == 429:
          retry = int(r.headers.get("Retry-After", 30))
          time.sleep(retry)
          continue
      r.raise_for_status()
      data = r.json()
      tasks.extend(data.get("data", []))
      offset = data.get("next_page", {})
      offset = offset.get("offset") if offset else None
      if not offset:
          break

  print(f"Fetched {len(tasks)} tasks from campaign project")

  # 2. Analyze task pipeline
  completed = [t for t in tasks if t.get("completed")]
  active = [t for t in tasks if not t.get("completed")]
  overdue = [t for t in active if t.get("due_on") and t["due_on"] < datetime.now().strftime("%Y-%m-%d")]

  sections = Counter()
  content_types = Counter()
  for t in tasks:
      for mem in t.get("memberships", []):
          sec = mem.get("section", {}).get("name", "No Section")
          sections[sec] += 1
      for cf in t.get("custom_fields", []):
          if cf.get("name") == "Content Type" and cf.get("display_value"):
              content_types[cf["display_value"]] += 1

  pipeline = "\n".join(f"  {sec}: {n} tasks" for sec, n in sections.most_common())
  mix = "\n".join(f"  {ct}: {n}" for ct, n in content_types.most_common())

  # 3. Build task details
  task_details = []
  for t in tasks[:50]:
      assignee = t.get("assignee", {})
      section = next((m.get("section", {}).get("name", "") for m in t.get("memberships", [])), "")
      custom = {cf["name"]: cf.get("display_value", "") for cf in t.get("custom_fields", []) if cf.get("display_value")}
      status = "Done" if t.get("completed") else ("OVERDUE" if t.get("due_on") and t["due_on"] < datetime.now().strftime("%Y-%m-%d") else "Active")
      task_details.append(
          f"- [{status}] {t['name']} | Section: {section} | "
          f"Assignee: {assignee.get('name', 'Unassigned')} | Due: {t.get('due_on', 'None')} | "
          f"{', '.join(f'{k}: {v}' for k, v in custom.items())}"
      )

  # 4. Mave status report
  report = requests.post(f"{MB}/mave/chat", headers=MH, json={
      "message": (
          f"Campaign status analyst. Produce a strategic status report for this campaign project.\n\n"
          f"SUMMARY: {len(tasks)} total | {len(completed)} done | {len(active)} active | {len(overdue)} overdue\n\n"
          f"PIPELINE BY SECTION:\n{pipeline}\n\n"
          f"CONTENT MIX:\n{mix}\n\n"
          f"TASK DETAILS:\n" + "\n".join(task_details[:40]) + "\n\n"
          "Generate:\n"
          "1. **Executive Summary** (3 sentences — health, velocity, risk)\n"
          "2. **Velocity Analysis** — Are we on pace? Compare completed vs remaining vs time left\n"
          "3. **Bottleneck Detection** — Which sections/stages have the most stuck tasks?\n"
          "4. **Content Mix Assessment** — Is the mix aligned with campaign goals?\n"
          "5. **Overdue Task Triage** — Each overdue task with recommended action\n"
          "6. **Assignee Load Balancing** — Who's overloaded? Who has capacity?\n"
          "7. **Recommendations** — 3-5 specific actions for this week"
      ),
  }).json()

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

  ```javascript JavaScript theme={"dark"}
  const ASANA = process.env.ASANA_PAT;
  const MV = process.env.MAVERA_API_KEY;
  const AB = "https://app.asana.com/api/1.0";
  const MB = "https://app.mavera.io/api/v1";
  const AH = { Authorization: `Bearer ${ASANA}` };
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const PROJECT_GID = "1234567890123456";

  // 1. Fetch tasks with pagination
  const tasks = [];
  let offset = null;
  do {
    const params = new URLSearchParams({
      project: PROJECT_GID,
      opt_fields: "name,completed,completed_at,due_on,assignee.name,custom_fields.name,custom_fields.display_value,memberships.section.name",
      limit: "100",
    });
    if (offset) params.set("offset", offset);
    const res = await fetch(`${AB}/tasks?${params}`, { headers: AH });
    if (res.status === 429) {
      await new Promise(r => setTimeout(r, parseInt(res.headers.get("Retry-After") || "30") * 1000));
      continue;
    }
    const data = await res.json();
    tasks.push(...(data.data || []));
    offset = data.next_page?.offset || null;
  } while (offset);

  console.log(`Fetched ${tasks.length} tasks`);

  // 2. Analyze
  const today = new Date().toISOString().slice(0, 10);
  const completed = tasks.filter(t => t.completed);
  const active = tasks.filter(t => !t.completed);
  const overdue = active.filter(t => t.due_on && t.due_on < today);

  const sections = {};
  const contentTypes = {};
  tasks.forEach(t => {
    (t.memberships || []).forEach(m => {
      const sec = m.section?.name || "No Section";
      sections[sec] = (sections[sec] || 0) + 1;
    });
    (t.custom_fields || []).forEach(cf => {
      if (cf.name === "Content Type" && cf.display_value)
        contentTypes[cf.display_value] = (contentTypes[cf.display_value] || 0) + 1;
    });
  });

  const pipeline = Object.entries(sections).sort(([,a],[,b]) => b - a).map(([s,n]) => `  ${s}: ${n}`).join("\n");
  const mix = Object.entries(contentTypes).sort(([,a],[,b]) => b - a).map(([c,n]) => `  ${c}: ${n}`).join("\n");

  // 3. Task details
  const taskDetails = tasks.slice(0, 50).map(t => {
    const sec = (t.memberships || [])[0]?.section?.name || "";
    const custom = (t.custom_fields || []).filter(cf => cf.display_value).map(cf => `${cf.name}: ${cf.display_value}`).join(", ");
    const status = t.completed ? "Done" : (t.due_on && t.due_on < today ? "OVERDUE" : "Active");
    return `- [${status}] ${t.name} | ${sec} | ${t.assignee?.name || "Unassigned"} | Due: ${t.due_on || "None"} | ${custom}`;
  }).join("\n");

  // 4. Report
  const report = await fetch(`${MB}/mave/chat`, { method: "POST", headers: MH,
    body: JSON.stringify({
      message: `Campaign status report.\n\nSUMMARY: ${tasks.length} total | ${completed.length} done | ${active.length} active | ${overdue.length} overdue\n\nPIPELINE:\n${pipeline}\n\nMIX:\n${mix}\n\nTASKS:\n${taskDetails}\n\nGenerate: 1) Exec summary 2) Velocity 3) Bottlenecks 4) Content mix 5) Overdue triage 6) Load balancing 7) Recommendations`,
    }),
  }).then(r => r.json());

  console.log(`\n${"=".repeat(60)}\nCAMPAIGN STATUS REPORT`);
  console.log((report.content || "").slice(0, 3000));
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Fetched 47 tasks from campaign project

CAMPAIGN STATUS REPORT
============================================================

## Executive Summary
Campaign is 63% complete (30/47 tasks) with 5 overdue items
concentrated in the Design Review stage. Velocity has slowed —
only 4 tasks completed this week vs 8 last week. Three blog
posts are at risk of missing the April 1 launch deadline.

## Velocity Analysis
- Weeks 1-2: 16 tasks completed (8/week)
- Weeks 3-4: 14 tasks completed (7/week)
- Current week: 4 completed, 17 remaining, 2 weeks left
- Required pace: 8.5/week — achievable if bottleneck clears

## Bottleneck Detection
- "Design Review" section: 8 tasks queued (3 overdue)
- Only 1 designer assigned to all 8 → capacity constraint
- "Copy Approval" has 4 tasks waiting on VP sign-off

## Recommendations
1. Reassign 3 Design Review tasks to contractor
2. Schedule 15-min VP approval session for queued copy
3. Deprioritize 2 low-priority social posts to free capacity
4. Move launch date for case study to April 8 (dependency chain)
```

### Error Handling

<AccordionGroup>
  <Accordion title="Rate limits (429)">Free plans allow \~150 req/min. The code reads the `Retry-After` header and waits. For large projects, use `opt_fields` to minimize response size and reduce call count.</Accordion>
  <Accordion title="Custom fields not returned">Custom fields only appear if you include them in `opt_fields`. If a custom field name doesn't match, check the exact name via `GET /projects/{gid}/custom_field_settings`.</Accordion>
  <Accordion title="Task pagination">Projects with 100+ tasks require pagination. The code follows `next_page.offset` automatically. Very large projects (1000+) should filter by `modified_since` to reduce data.</Accordion>
  <Accordion title="Section membership">Tasks can belong to multiple sections (if multi-homed). The code uses the first membership. For accurate pipeline views, filter by the primary project.</Accordion>
</AccordionGroup>
