Skip to main content

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.

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/generationsPOST /tasks/{id}/stories (attach result as comment)

Code

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)

Example Output

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

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