# Die Content-Pipeline eines Agenten von A bis Z: Briefing rein, Beiträge pro Konto raus, Klicks zurück – mit jedem Shager-Aufruf in der richtigen Reihenfolge

_2026-10-11 · Eine komplette Agenten-Pipeline auf Shager in TypeScript: Briefing, Medien, Texte pro Konto, Entwurf, Kundenfreigabe, versetzte Planung, Zustellung, Klicks._

Eine durchgängige Content-Pipeline für einen Agenten auf Shager hat acht Stufen: Briefing lesen, Medien unter einer öffentlichen URL bereitstellen, Zielkonten ermitteln, einen Text pro Konto schreiben, einen Entwurf mit Tracking-Link anlegen, Freigabe einholen, mit zeitversetztem Versand planen, anschließend die Zustellung prüfen und die Klicks pro Konto abrufen. Der Agent übernimmt die repetitiven Stufen, ein Mensch verantwortet Briefing und Freigabe. Jeder Shager-Schritt ist ein REST-Aufruf oder ein gleichnamiges MCP-Tool.

Unten findest du die gesamte Kette als ein einziges TypeScript-Programm gegen die REST-API, mit der Begründung für jede Stufe. Wenn du lieber einen Agenten-Client steuerst: Die Tool-Aufrufe sind identisch.

## Welche Stufen gibt es, und wer verantwortet was?

| Stufe | Verantwortlich | Shager-Aufruf |
|---|---|---|
| 1. Briefing | Mensch | Keiner |
| 2. Medien | Agent oder deine Asset-Pipeline | `upload_media_from_url` |
| 3. Ziele | Agent | `list_channels`, `list_groups` |
| 4. Texte | Agent (dein Modell) | Keiner |
| 5. Entwurf | Agent | `create_post` |
| 6. Freigabe | Kunde | `request_approval` |
| 7. Planen | Agent mit `publish` oder Mensch | `schedule_post` |
| 8. Prüfen und messen | Agent | `get_post`, `get_analytics` |

Nur Stufe 4 braucht ein Sprachmodell. Alles andere ist deterministische Verdrahtung und sollte als Code geschrieben werden, statt es der Improvisation des Modells zu überlassen. Diese Aufteilung ist die wichtigste Lektion für zuverlässige Agenten-Distribution: Das Modell schreibt, der Code orchestriert.

## Stufe 1: Was gehört ins Briefing?

Ein Briefing, mit dem die Pipeline arbeiten kann, hat acht Felder. Fehlt etwas, fängt der Agent an zu raten.

```typescript
type Brief = {
  workspace: string;          // "Cedar Clinics"
  group: string;              // "All clinics"
  message: string;            // the facts, written by a human
  mediaUrl: string;           // public URL of the finished asset
  media: { width: number; height: number; durationMs?: number };
  link: string;               // landing page to track
  campaign: string;           // "flu-shots-2026"
  publishAt: string;          // "2026-10-27T08:00:00-05:00"
};
```

`publishAt` enthält bewusst einen Zeitzonen-Offset. Shager verlangt für `schedule_at` ISO 8601 mit Offset, und wenn der Offset schon im Briefing steht, muss sich niemand merken, welche Zeitzone mit „8 Uhr“ gemeint war.

## Setup: ein Helfer für alle Aufrufe

```typescript
const API = "https://app.shager.ai/api/v1";

class ShagerError extends Error {
  constructor(public status: number, public code: string, message: string) {
    super(`${status} ${code}: ${message}`);
  }
}

async function shager<T>(method: string, path: string, body?: unknown): Promise<T> {
  for (let attempt = 0; attempt < 3; attempt++) {
    const res = await fetch(`${API}${path}`, {
      method,
      headers: {
        Authorization: `Bearer ${process.env.SHAGER_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: body ? JSON.stringify(body) : undefined,
    });
    const json = await res.json();
    if (res.ok) return json.data as T;
    if (res.status === 429) {
      await new Promise((r) => setTimeout(r, 60_000));
      continue;
    }
    throw new ShagerError(res.status, json.error.code, json.error.message);
  }
  throw new Error("Rate limited three times in a row");
}
```

Wiederholt wird nur bei 429. Ein 403 oder 422 ist eine Information, und ein erneuter Versuch wiederholt nur dieselbe Ablehnung. Das Limit liegt bei 120 Anfragen pro Minute und Schlüssel – davon ist diese Pipeline weit entfernt, weil ein einziges `create_post` alle Konten abdeckt.

## Stufe 2: Medien

Shager importiert Medien von einer URL. Was auch immer das Asset erzeugt hat (ein Editor, ein Render-Job, ein Generierungstool), muss es also nur irgendwo öffentlich erreichbar ablegen.

```typescript
const workspaces = await shager<Array<{ id: string; name: string }>>("GET", "/workspaces");
const ws = workspaces.find((w) => w.name === brief.workspace);
if (!ws) throw new Error(`No workspace named ${brief.workspace}`);

const media = await shager<{ id: string; kind: string }>("POST", `/workspaces/${ws.id}/media`, {
  url: brief.mediaUrl,
  width: brief.media.width,
  height: brief.media.height,
  duration_ms: brief.media.durationMs,
});
```

Breite, Höhe und Dauer sind bei Hochformatvideos wichtig. Damit prüft Shager Reels, Shorts und Storys schon vor dem Planen gegen die Regeln des jeweiligen Netzwerks. Ein Clip, der für ein Format zu lang ist, scheitert also jetzt mit einer verständlichen Begründung – und nicht erst beim Versand auf einem einzelnen Netzwerk.

## Stufe 3: Ziele ermitteln

```typescript
type Channel = { id: string; network: string; name: string; status: string; formats: string[] };

const groups = await shager<Array<{ id: string; name: string; channel_ids: string[] }>>(
  "GET", `/workspaces/${ws.id}/groups`,
);
const group = groups.find((g) => g.name === brief.group);
if (!group) throw new Error(`No group named ${brief.group}`);

const channels = (await shager<Channel[]>("GET", `/workspaces/${ws.id}/channels`))
  .filter((c) => group.channel_ids.includes(c.id) && c.status === "ACTIVE");
```

Wenn du hier auf `ACTIVE` filterst, kannst du getrennte Konten gleich zu Beginn melden. Eine Seite mit abgelaufenem Token ist ein Gespräch mit dem Kunden – nichts, was man erst bemerken sollte, nachdem der Beitrag gescheitert ist.

## Stufe 4: ein Text pro Konto

Hier verdient sich dein Modell seinen Platz. Die Funktion unten ist absichtlich nur ein Gerüst; häng das Modell und SDK an, das dein Team nutzt.

```typescript
async function writeCaptions(brief: Brief, channels: Channel[]): Promise<Map<string, string>> {
  // Prompt your model with: the brief's facts, each channel's name and network,
  // and these rules: keep facts identical, vary the opening and structure,
  // use the local detail in the channel name, no raw URLs, network-appropriate length.
  // Return channel_id -> caption, and validate that every channel got one.
  throw new Error("connect your model here");
}

const captions = await writeCaptions(brief, channels);
```

Zwei Regeln gehören jedes Mal in diesen Prompt. Überall dieselben Fakten, denn ein Preis oder Datum, das zwischen Seiten abweicht, ist ein echter Fehler. Und keine nackten Links, denn der Link wird separat angehängt, damit jedes Konto seine eigene getrackte Version bekommt. Unser Leitfaden zur [Markenkonsistenz über viele Konten](/en/blog/brand-consistency-across-many-accounts) zeigt, wo Variation hilft und wo sie schadet.

## Stufe 5: Entwurf anlegen

```typescript
type PostView = {
  id: string;
  status: string;
  destinations: Array<{ channel_id: string; account: string; network: string; format: string; state: string; url: string | null; error: string | null }>;
};

const draft = await shager<PostView>("POST", `/workspaces/${ws.id}/posts`, {
  label: `${brief.campaign} ${brief.publishAt.slice(0, 10)}`,
  caption: brief.message,
  channel_ids: channels.map((c) => c.id),
  media_ids: [media.id],
  per_channel: channels.map((c) => ({ channel_id: c.id, caption: captions.get(c.id)! })),
  cta_url: brief.link,
  campaign: brief.campaign,
  first_comment: `Details: ${brief.link}`,
  stagger_seconds: [30, 90],
});
```

`cta_url` gibt jedem Konto einen eigenen Kurzlink, dessen `utm_content` das Konto kennzeichnet. `first_comment` wird direkt veröffentlicht, sobald der jeweilige Beitrag live ist. `stagger_seconds` verteilt Konten im selben Netzwerk mit einem zufälligen Abstand von 30 bis 90 Sekunden, was auch der Standard ist. Das Ergebnis ist ein `DRAFT`.

## Stufe 6: Freigabe

```typescript
const approval = await shager<{ approval_url: string; emailed: boolean }>(
  "POST", `/workspaces/${ws.id}/posts/${draft.id}/approval`,
  { reviewer_name: "Priya Shah", reviewer_email: "priya@cedarclinics.example", send_email: true },
);
console.log("Approval link:", approval.approval_url);
```

Die prüfende Person öffnet den Link und gibt frei, ohne sich anzumelden. Die Pipeline hält hier an und läuft später weiter, denn eine Freigabe dauert Stunden, keine Millisekunden. Speichere `draft.id` und nimm den Faden in einem zweiten Durchlauf wieder auf, der per `get_post` auf `APPROVED` prüft. Warum dieser Kontrollpunkt nach dem Entwurf und nicht nach der Veröffentlichung sitzt, erklärt der Artikel zu [Freigabe-Workflows für Kunden](/en/blog/client-approval-workflow-for-agencies).

## Stufe 7: planen

```typescript
const post = await shager<PostView>("GET", `/workspaces/${ws.id}/posts/${draft.id}`);
if (post.status !== "APPROVED") {
  console.log(`Not approved yet: ${post.status}`);
} else {
  try {
    await shager<PostView>("POST", `/workspaces/${ws.id}/posts/${draft.id}/schedule`, {
      schedule_at: brief.publishAt,
    });
  } catch (e) {
    if (e instanceof ShagerError && e.code === "rejected") {
      console.error("A network rule failed:", e.message); // fix input, do not loop
    } else throw e;
  }
}
```

Dafür braucht es einen Schlüssel mit der Berechtigung `publish`, die ein Mensch unter https://app.shager.ai/dashboard/agents erteilt. Shager prüft an dieser Stelle jedes Ziel gegen die Regeln seines Netzwerks, sodass ein 422 `rejected` ankommt, bevor irgendetwas in die Warteschlange geht.

## Stufe 8: Zustellung prüfen und messen

Prüfe nach dem Veröffentlichungszeitpunkt jedes Ziel. Die Zustellstatus sind `PENDING`, `QUEUED`, `PROCESSING`, `PUBLISHED`, `FAILED` und `SKIPPED`.

```typescript
const after = await shager<PostView>("GET", `/workspaces/${ws.id}/posts/${draft.id}`);
for (const d of after.destinations) {
  if (d.state === "FAILED" || d.state === "SKIPPED") console.warn(d.account, d.state, d.error);
}

const stats = await shager<{ total_clicks: number; by_account: Array<{ account: string; network: string; clicks: number }> }>(
  "GET", `/workspaces/${ws.id}/analytics?from=2026-10-27T00:00:00-05:00&to=2026-11-03T00:00:00-05:00`,
);
console.table(stats.by_account);
```

Mit den Klicks pro Konto endet der Teil von Shager, und der Teil des Kunden beginnt. Wie du sie im CRM über `utm_content` mit Leads verknüpfst, steht in [Lead-Zuordnung pro Konto](/en/blog/utm-per-account-lead-attribution).

## FAQ

### Sollte das ein Agenten-Client ausführen oder ein Skript?

Beides funktioniert. Ein Skript eignet sich besser für die festen Stufen und alles, was nach Zeitplan läuft. Ein Agenten-Client wie Claude Code ist besser, wenn das Briefing unscharf ist und du die Texte interaktiv verfeinern willst. Viele Teams nutzen den Agenten für Stufe 4 und Code für den Rest.

### Was, wenn das Briefing Konten mehrerer Kunden betrifft?

Lass die Pipeline einmal pro Workspace laufen. Workspaces trennen Kunden bewusst: Ihre Konten, Freigaben, Links und Auswertungen vermischen sich nie – und genau das erwartet der Kunde auch, wenn er seinen Report liest.

### Wo finde ich die genauen Felddefinitionen?

Im [OpenAPI-Dokument](https://app.shager.ai/api/v1/openapi.json), das jede Operation, jede Eingabe und jede Berechtigung auflistet. Die [Agenten-Seite](/en/agents) enthält dieselben Informationen in kürzerer Form für Agenten und ihre Entwickler.

## Nächster Schritt

Lass die Pipeline einmal mit einem einzigen Testkonto und einem Entwurf laufen, den du nie planst. Sieh dir den Entwurf in Shager an, prüf die Links pro Konto und erweitere dann auf die ganze Gruppe. Wenn das stabil läuft, pack die Stufen in [eine wöchentliche Routine](/en/blog/skill-weekly-distribution-routine), damit die Pipeline nach Zeitplan läuft und nicht davon abhängt, dass jemand daran denkt.

---
Source: https://shager.ai/de/blog/agent-content-pipeline-end-to-end
