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
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.
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
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.
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
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.
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 zeigt, wo Variation hilft und wo sie schadet.
Stufe 5: Entwurf anlegen
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
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.
Stufe 7: planen
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.
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.
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, das jede Operation, jede Eingabe und jede Berechtigung auflistet. Die Agenten-Seite 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, damit die Pipeline nach Zeitplan läuft und nicht davon abhängt, dass jemand daran denkt.


