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

# Brief → Generation Automation

### Scenario

Your content team creates brief tasks in Asana with structured descriptions: topic, target audience, content type, key messages, and SEO keywords. When a brief task moves to "Brief Complete" status, this job automatically generates a full content draft using Mavera's Generate endpoint, then attaches the draft URL as a comment on the Asana task. Editors pick up drafts instead of blank pages.

**Flow:** Asana `GET /tasks` (filter: "Brief Complete" section) → Extract brief fields → Mavera `POST /api/v1/generations` → `POST /tasks/{id}/stories` (attach result as comment)

### Code

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

  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"
  BRAND_VOICE_ID = "bv_your_voice_id"

  # 1. Get sections to find "Brief Complete"
  sections = requests.get(f"{AB}/projects/{PROJECT_GID}/sections",
      headers=AH, params={"opt_fields": "name"}).json().get("data", [])

  brief_section = next((s for s in sections if "brief complete" in s["name"].lower()), None)
  if not brief_section:
      print("No 'Brief Complete' section found. Available sections:")
      for s in sections:
          print(f"  - {s['name']} ({s['gid']})")
      raise SystemExit(1)

  # 2. Get tasks in that section
  tasks = requests.get(f"{AB}/sections/{brief_section['gid']}/tasks", headers=AH,
      params={
          "opt_fields": "name,notes,custom_fields.name,custom_fields.display_value,"
                        "assignee.name,due_on,tags.name",
          "limit": 20,
      }).json().get("data", [])

  print(f"Found {len(tasks)} tasks in '{brief_section['name']}'")

  # 3. Generate content for each brief
  for task in tasks:
      brief = task.get("notes", "")
      if len(brief) < 50:
          print(f"  Skipping '{task['name']}' — brief too short ({len(brief)} chars)")
          continue

      custom = {cf["name"]: cf.get("display_value", "") for cf in task.get("custom_fields", []) if cf.get("display_value")}
      content_type = custom.get("Content Type", "blog post")
      audience = custom.get("Target Audience", "marketing professionals")
      seo_keywords = custom.get("SEO Keywords", "")

      prompt = (
          f"Write a {content_type} based on this brief.\n\n"
          f"TITLE: {task['name']}\n"
          f"TARGET AUDIENCE: {audience}\n"
          f"BRIEF:\n{brief}\n"
      )
      if seo_keywords:
          prompt += f"\nSEO KEYWORDS: {seo_keywords}\n"
      prompt += (
          "\nRequirements:\n"
          "- Engaging introduction that hooks the reader in 2-3 sentences\n"
          "- 4-6 sections with clear headers\n"
          "- Specific examples and data points where relevant\n"
          "- Strong conclusion with a clear call-to-action\n"
          "- 1000-1500 words\n"
          "- Tone should match the brand voice provided"
      )

      gen = requests.post(f"{MB}/generations", headers=MH, json={
          "brand_voice_id": BRAND_VOICE_ID,
          "prompt": prompt,
      }).json()

      draft = gen.get("output", gen.get("content", ""))
      if not draft:
          print(f"  ✗ '{task['name']}' — generation returned empty")
          continue

      # 4. Post draft as a comment on the task
      comment = (
          f"🤖 Auto-generated draft ({len(draft)} chars)\n\n"
          f"---\n\n{draft[:15000]}"
      )
      r = requests.post(f"{AB}/tasks/{task['gid']}/stories", headers=AH,
          json={"data": {"text": comment}})

      if r.ok:
          # 5. Move task to "Draft Ready" section
          draft_section = next((s for s in sections if "draft" in s["name"].lower() and "ready" in s["name"].lower()), None)
          if draft_section:
              requests.post(f"{AB}/sections/{draft_section['gid']}/addTask",
                  headers=AH, json={"data": {"task": task["gid"]}})
              requests.post(f"{AB}/sections/{brief_section['gid']}/removeTask",
                  headers=AH, json={"data": {"task": task["gid"]}})

          print(f"  ✓ '{task['name']}' → {len(draft)} chars, comment posted")
      else:
          print(f"  ✗ '{task['name']}' — failed to post comment: {r.status_code}")

      time.sleep(0.5)
  ```

  ```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";
  const BRAND_VOICE_ID = "bv_your_voice_id";

  // 1. Find "Brief Complete" section
  const sections = await fetch(`${AB}/projects/${PROJECT_GID}/sections?opt_fields=name`,
    { headers: AH }).then(r => r.json()).then(d => d.data || []);

  const briefSection = sections.find(s => s.name.toLowerCase().includes("brief complete"));
  if (!briefSection) {
    console.log("No 'Brief Complete' section. Available:", sections.map(s => s.name));
    process.exit(1);
  }

  // 2. Tasks in section
  const tasks = await fetch(
    `${AB}/sections/${briefSection.gid}/tasks?` + new URLSearchParams({
      opt_fields: "name,notes,custom_fields.name,custom_fields.display_value,assignee.name,due_on",
      limit: "20",
    }), { headers: AH },
  ).then(r => r.json()).then(d => d.data || []);

  console.log(`Found ${tasks.length} tasks in '${briefSection.name}'`);

  // 3. Generate per task
  for (const task of tasks) {
    const brief = task.notes || "";
    if (brief.length < 50) { console.log(`  Skip '${task.name}' — brief too short`); continue; }

    const custom = Object.fromEntries(
      (task.custom_fields || []).filter(cf => cf.display_value).map(cf => [cf.name, cf.display_value])
    );

    const prompt = `Write a ${custom["Content Type"] || "blog post"} based on this brief.\n\n` +
      `TITLE: ${task.name}\nAUDIENCE: ${custom["Target Audience"] || "marketing pros"}\n` +
      `BRIEF:\n${brief}\n${custom["SEO Keywords"] ? `\nSEO: ${custom["SEO Keywords"]}` : ""}\n` +
      `\n4-6 sections, 1000-1500 words, intro + CTA conclusion. Match brand voice.`;

    const gen = await fetch(`${MB}/generations`, { method: "POST", headers: MH,
      body: JSON.stringify({ brand_voice_id: BRAND_VOICE_ID, prompt }),
    }).then(r => r.json());

    const draft = gen.output || gen.content || "";
    if (!draft) { console.log(`  ✗ '${task.name}' — empty generation`); continue; }

    // 4. Comment
    const res = await fetch(`${AB}/tasks/${task.gid}/stories`, {
      method: "POST", headers: { ...AH, "Content-Type": "application/json" },
      body: JSON.stringify({ data: { text: `Auto-generated draft (${draft.length} chars)\n\n---\n\n${draft.slice(0, 15000)}` } }),
    });

    if (res.ok) {
      // 5. Move to Draft Ready
      const draftSection = sections.find(s => s.name.toLowerCase().includes("draft") && s.name.toLowerCase().includes("ready"));
      if (draftSection) {
        await fetch(`${AB}/sections/${draftSection.gid}/addTask`, {
          method: "POST", headers: { ...AH, "Content-Type": "application/json" },
          body: JSON.stringify({ data: { task: task.gid } }),
        });
      }
      console.log(`  ✓ '${task.name}' → ${draft.length} chars`);
    }
    await new Promise(r => setTimeout(r, 500));
  }
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Found 4 tasks in 'Brief Complete'
  ✓ 'Q3 Product Launch Blog Post' → 1847 chars, comment posted
  ✓ 'Customer Success Case Study: FinCo' → 2103 chars, comment posted
  Skip 'Social Graphics Brief' — brief too short (23 chars)
  ✓ 'Email Nurture Sequence #4' → 1238 chars, comment posted
```

### Error Handling

<AccordionGroup>
  <Accordion title="Section names vary">The code searches for sections containing "brief complete" (case-insensitive). If your workflow uses different names like "Ready for Writing" or "Approved Brief", adjust the search string.</Accordion>
  <Accordion title="Comment character limits">Asana story (comment) text is limited to 100,000 characters. Long drafts are truncated to 15,000 chars. For very long content, post a summary comment with a link to the full draft in a shared doc.</Accordion>
  <Accordion title="Moving tasks between sections">The `addTask`/`removeTask` section endpoints are separate calls. If the "Draft Ready" section doesn't exist, the task stays in "Brief Complete" — no error is thrown, just no movement.</Accordion>
  <Accordion title="Duplicate runs">Running this job twice generates duplicate drafts. Add a check: before generating, fetch task stories and skip if a comment containing "Auto-generated draft" already exists.</Accordion>
</AccordionGroup>
