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

# Feature Release → Content Pipeline

> Query completed Linear issues and chain Mavera Generate calls for release notes, blog drafts, and social copy

## Scenario

When features move to "Done" in Linear, your marketing team needs release notes, a blog post draft, and social media copy. This job queries completed issues from recent cycles via GraphQL, extracts feature names and descriptions, then chains three Mavera Generate calls for release notes, a blog draft, and social copy.

**Flow:** Linear GraphQL (`completedIssues` query) → extract features → Mavera `POST /api/v1/generations` (release notes) → `POST /api/v1/generations` (blog) → `POST /api/v1/generations` (social)

## Code

<CodeGroup>
  ```python Python theme={"dark"}
  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"}

  query = """query($tk: String!) {
    issues(filter: { team: { key: { eq: $tk } }, state: { type: { eq: "completed" } },
      completedAt: { gte: "-P14D" } }, first: 50, orderBy: completedAt) {
      nodes { identifier title description labels { nodes { name } } project { name } estimate }
    }
  }"""

  r = requests.post(LB, headers=LH, json={"query": query, "variables": {"tk": "ENG"}})
  if r.status_code == 429:
      time.sleep(int(r.headers.get("Retry-After", 60)))
      r = requests.post(LB, headers=LH, json={"query": query, "variables": {"tk": "ENG"}})
  r.raise_for_status()
  issues = r.json()["data"]["issues"]["nodes"]
  print(f"Fetched {len(issues)} completed issues")

  features = "\n\n".join(
      f"- [{i['identifier']}] {i['title']}\n  Project: {(i.get('project') or {}).get('name','None')}\n"
      f"  Labels: {', '.join(l['name'] for l in i.get('labels',{}).get('nodes',[])) or 'none'}\n"
      f"  Description: {(i.get('description') or '')[:300]}"
      for i in issues)

  time.sleep(0.3)
  notes = requests.post(f"{MB}/generations", headers=MH, json={
      "prompt": f"Technical writer. Release notes for {len(issues)} features.\n\n{features[:4000]}\n\n"
                "Group by project. 1-2 sentences each. User benefit first. Include identifier.",
  }).json()
  release_notes = notes.get("output", notes.get("content", ""))

  time.sleep(0.3)
  blog = requests.post(f"{MB}/generations", headers=MH, json={
      "prompt": f"Blog post announcing updates.\n\n{release_notes[:3000]}\n\n"
                "Headline, intro, 3-4 highlights, what's next, CTA. 400-600 words.",
  }).json()

  time.sleep(0.3)
  social = requests.post(f"{MB}/generations", headers=MH, json={
      "prompt": f"Social copy for release.\n\n{release_notes[:2000]}\n\n"
                "- Twitter/X (280 chars)\n- LinkedIn (150 words)\n- Product Hunt (3 sentences)\nMax 2 hashtags.",
  }).json()

  print(f"\nRELEASE NOTES:\n{release_notes[:1500]}")
  print(f"\nBLOG:\n{blog.get('output', blog.get('content',''))[:1500]}")
  print(f"\nSOCIAL:\n{social.get('output', social.get('content',''))[:1500]}")
  ```

  ```javascript JavaScript theme={"dark"}
  const LN = process.env.LINEAR_API_KEY, MV = process.env.MAVERA_API_KEY;
  const LB = "https://api.linear.app/graphql", MB = "https://app.mavera.io/api/v1";
  const LH = { Authorization: LN, "Content-Type": "application/json" };
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const query = `query($tk: String!) {
    issues(filter: { team: { key: { eq: $tk } }, state: { type: { eq: "completed" } },
      completedAt: { gte: "-P14D" } }, first: 50, orderBy: completedAt) {
      nodes { identifier title description labels { nodes { name } } project { name } estimate }
    }
  }`;

  let res = await fetch(LB, { method: "POST", headers: LH,
    body: JSON.stringify({ query, variables: { tk: "ENG" } }) });
  if (res.status === 429) {
    await new Promise(r => setTimeout(r, parseInt(res.headers.get("Retry-After") || "60") * 1000));
    res = await fetch(LB, { method: "POST", headers: LH,
      body: JSON.stringify({ query, variables: { tk: "ENG" } }) });
  }
  const issues = (await res.json()).data?.issues?.nodes || [];

  const features = issues.map(i =>
    `- [${i.identifier}] ${i.title}\n  Project: ${i.project?.name || "None"}\n` +
    `  Labels: ${(i.labels?.nodes || []).map(l => l.name).join(", ") || "none"}\n` +
    `  Description: ${(i.description || "").slice(0, 300)}`).join("\n\n");

  await new Promise(r => setTimeout(r, 300));
  const notes = await fetch(`${MB}/generations`, { method: "POST", headers: MH,
    body: JSON.stringify({ prompt: `Release notes for ${issues.length} features.\n\n${features.slice(0,4000)}\n\nGroup by project. User benefit first.` }),
  }).then(r => r.json());
  const releaseNotes = notes.output || notes.content || "";

  await new Promise(r => setTimeout(r, 300));
  const blog = await fetch(`${MB}/generations`, { method: "POST", headers: MH,
    body: JSON.stringify({ prompt: `Blog post.\n\n${releaseNotes.slice(0,3000)}\n\nHeadline, intro, 3-4 highlights, CTA. 400-600 words.` }),
  }).then(r => r.json());

  await new Promise(r => setTimeout(r, 300));
  const social = await fetch(`${MB}/generations`, { method: "POST", headers: MH,
    body: JSON.stringify({ prompt: `Social copy.\n\n${releaseNotes.slice(0,2000)}\n\n- Twitter/X (280 chars)\n- LinkedIn (150 words)\n- Product Hunt (3 sentences)` }),
  }).then(r => r.json());

  console.log(`RELEASE NOTES:\n${releaseNotes.slice(0,1500)}`);
  console.log(`\nBLOG:\n${(blog.output||blog.content||"").slice(0,1500)}`);
  console.log(`\nSOCIAL:\n${(social.output||social.content||"").slice(0,1500)}`);
  ```
</CodeGroup>

## Example Output

```text theme={"dark"}
Fetched 23 completed issues

RELEASE NOTES:
## Dashboard & Analytics
- Real-time webhook delivery dashboard with retry visibility (ENG-412)
  Monitor webhook health at a glance — delivery rates, failures, retries.
- Custom date range picker for all analytics views (ENG-398)

## API Platform
- GraphQL subscriptions for issue updates (ENG-407)
- Batch mutation support for bulk issue updates (ENG-391)

SOCIAL:
Twitter/X: We shipped 23 features in 2 weeks. Real-time webhooks,
batch mutations, Figma previews — all live now. #devtools

LinkedIn: Our engineering team closed 23 issues across 3 projects.
Highlights: real-time webhook monitoring, GraphQL subscriptions...
```

## Error Handling

<AccordionGroup>
  <Accordion title="Rate limits (429)">Linear allows 5,000 req/hr and 250K complexity points/hr. The code reads `Retry-After` on 429 responses. Nested queries with large `first:` values consume more points.</Accordion>
  <Accordion title="Empty results">The `gte: "-P14D"` ISO 8601 duration filters to the last 14 days. If no issues match, extend the window or verify your team's workflow states include a "completed" type.</Accordion>
  <Accordion title="Missing descriptions">Issues without descriptions produce thinner release notes. Filter to description length > 50 chars for higher quality, or enrich from linked PRs via the `attachments` connection.</Accordion>
</AccordionGroup>
