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

Marketing needs to know when features will actually ship — not the optimistic roadmap date, but a timeline grounded in engineering velocity. This job pulls Linear cycle history (completed story points per cycle), calculates velocity trends and remaining capacity, then sends the analysis to Mave Agent for a marketing launch timeline aligned with engineering reality. Flow: Linear GraphQL (cycles with completedIssueCountHistory) → calculate velocity → Mavera POST /api/v1/mave/chat → Marketing timeline

Code

import os, requests, time

LN, MV = os.environ["LINEAR_API_KEY"], os.environ["MAVERA_API_KEY"]
LB, MB = "https://api.linear.app/graphql", "https://app.mavera.io/api/v1"
LH = {"Authorization": LN, "Content-Type": "application/json"}
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

TEAM_KEY = "ENG"

cycle_q = """query($tk: String!) {
  cycles(filter: { team: { key: { eq: $tk } } }, first: 12, orderBy: startsAt) {
    nodes { number name startsAt endsAt progress completedScopeHistory scopeHistory }
  }
}"""

r = requests.post(LB, headers=LH, json={"query": cycle_q, "variables": {"tk": TEAM_KEY}})
if r.status_code == 429:
    time.sleep(int(r.headers.get("Retry-After", 60)))
    r = requests.post(LB, headers=LH, json={"query": cycle_q, "variables": {"tk": TEAM_KEY}})
r.raise_for_status()
cycles = r.json()["data"]["cycles"]["nodes"]

time.sleep(0.3)
pipe_q = """query($tk: String!) {
  issues(filter: { team: { key: { eq: $tk } },
    state: { type: { in: ["started", "unstarted"] } } }, first: 100) {
    nodes { identifier title estimate project { name } }
  }
}"""
pipe_issues = requests.post(LB, headers=LH,
    json={"query": pipe_q, "variables": {"tk": TEAM_KEY}}).json()["data"]["issues"]["nodes"]

vel = [{"cycle": c.get("number", "?"),
        "dates": f"{(c.get('startsAt') or '')[:10]}{(c.get('endsAt') or '')[:10]}",
        "pts": (c.get("completedScopeHistory") or [0])[-1],
        "scope": (c.get("scopeHistory") or [0])[-1],
        "pct": round((c.get("progress") or 0) * 100, 1)} for c in cycles]

recent = vel[-6:] if len(vel) >= 6 else vel
avg = sum(v["pts"] for v in recent) / max(len(recent), 1)
trend = "increasing" if len(recent) >= 3 and recent[-1]["pts"] > recent[0]["pts"] else "decreasing"

by_proj, total = {}, 0
for i in pipe_issues:
    p = (i.get("project") or {}).get("name", "Unassigned")
    pts = i.get("estimate") or 0
    by_proj.setdefault(p, {"n": 0, "pts": 0})
    by_proj[p]["n"] += 1; by_proj[p]["pts"] += pts; total += pts

vel_txt = "\n".join(f"  Cycle {v['cycle']} ({v['dates']}): {v['pts']}/{v['scope']} pts ({v['pct']}%)" for v in vel)
pipe_txt = "\n".join(f"  {p}: {d['n']} issues, {d['pts']} pts" for p, d in sorted(by_proj.items(), key=lambda x: -x[1]["pts"]))
print(f"Avg velocity: {avg:.1f} pts/cycle ({trend}), Pipeline: {total} pts")

time.sleep(0.3)
plan = requests.post(f"{MB}/mave/chat", headers=MH, json={
    "message": f"Marketing launch planner.\n\nVELOCITY ({len(vel)} cycles):\n{vel_txt}\n\n"
        f"AVG: {avg:.1f} pts/cycle ({trend})\n\nPIPELINE:\n{pipe_txt}\nTotal: {total} pts\n\n"
        "Produce: 1) Delivery forecast per project 2) Confidence levels 3) Marketing milestone "
        "timeline (prep, teaser, launch, follow-up) 4) Risk factors 5) Big bang vs stagger "
        "recommendation 6) Content production schedule. Use measured velocity, not optimism.",
}).json()

print(f"\n{'='*60}\nMARKETING LAUNCH TIMELINE\n{'='*60}")
print(plan.get("content", "")[:3000])

Example Output

Avg velocity: 34.2 pts/cycle (increasing), Pipeline: 142 pts

MARKETING LAUNCH TIMELINE
============================================================

## Delivery Forecast
| Project | Remaining | Est. Cycles | Ship Date | Confidence |
|---------|-----------|-------------|-----------|------------|
| API v3 | 48 pts | 1.4 cycles | Mar 28 | High |
| Dashboard Redesign | 62 pts | 1.8 cycles | Apr 11 | Medium |
| Mobile App | 32 pts | 0.9 cycles | Mar 21 | High |

## Marketing Milestones — API v3 (ship ~Mar 28)
- Mar 14: Begin blog draft + API migration guide
- Mar 21: Teaser on Twitter
- Mar 28: Launch — blog, changelog, developer email
- Apr 4: Follow-up case study with beta tester

## Recommended Strategy
STAGGER. Ship Mobile App first (smallest), then API v3 one week
later, then Dashboard. 1 week between launches for content prep.

## Content Schedule
| Content | API v3 | Dashboard | Mobile |
|---------|--------|-----------|--------|
| Blog draft | Mar 14 | Mar 28 | Mar 7 |
| Email copy | Mar 21 | Apr 4 | Mar 14 |
| Social | Mar 25 | Apr 8 | Mar 18 |

Error Handling

Cycles without estimates return empty completedScopeHistory. Enable estimates in Linear (Settings → Teams → Estimation) for accurate velocity tracking.
Linear auto-names cycles by date range. The code uses number as identifier. Adjust if your team uses custom names.
Holidays or crunch sprints skew averages. The code uses the last 6 cycles — consider trimming the highest and lowest values for robustness.