Pipeline Konten Agen dari Awal sampai Akhir: Brief Masuk, Postingan per Akun Keluar, Data Klik Kembali, Lengkap dengan Urutan Setiap Panggilan Shager
Pipeline agen lengkap di Shager dengan TypeScript: brief, media, caption per akun, draf, persetujuan klien, jadwal bertahap, cek pengiriman, dan klik.

Pipeline konten agen yang utuh di Shager punya delapan tahap: membaca brief, menyiapkan media di URL publik, menentukan akun tujuan, menulis satu caption per akun, membuat draf dengan tautan terlacak, mendapatkan persetujuan, menjadwalkan dengan pengiriman bertahap, lalu memeriksa pengiriman dan menarik data klik per akun. Agen memegang tahap-tahap yang berulang; manusia memegang brief dan persetujuan. Setiap langkah di Shager adalah satu panggilan REST atau satu tool MCP dengan nama yang sama.
Di bawah ini seluruh rangkaiannya dalam satu program TypeScript yang memanggil REST API, beserta alasan di balik setiap tahap. Kalau Anda lebih suka menjalankannya dari klien agen, panggilan tool-nya persis sama.
Apa saja tahapnya, dan siapa yang memegang masing-masing?
| Tahap | Pemegang | Panggilan Shager |
|---|---|---|
| 1. Brief | Manusia | Tidak ada |
| 2. Media | Agen atau pipeline aset Anda | upload_media_from_url |
| 3. Tujuan | Agen | list_channels, list_groups |
| 4. Caption | Agen (model Anda) | Tidak ada |
| 5. Draf | Agen | create_post |
| 6. Persetujuan | Klien | request_approval |
| 7. Jadwal | Agen dengan publish, atau manusia |
schedule_post |
| 8. Verifikasi dan pengukuran | Agen | get_post, get_analytics |
Tahap 4 satu-satunya yang butuh model bahasa. Sisanya adalah pekerjaan teknis yang deterministik, dan sebaiknya ditulis sebagai kode, bukan diserahkan ke improvisasi model. Pembagian inilah pelajaran keandalan terbesar dalam distribusi berbasis agen: biarkan model yang menulis, biarkan kode yang mengatur alurnya.

Tahap 1: apa isi brief?
Brief yang bisa langsung dieksekusi pipeline punya delapan kolom. Kurang dari itu, agen mulai menebak-nebak.
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 sengaja menyertakan offset. Shager mewajibkan format ISO 8601 dengan offset untuk schedule_at, dan dengan menuliskan offset di brief, tidak ada yang perlu mengingat "jam 8 pagi" itu maksudnya zona waktu mana.
Persiapan: satu helper untuk semua panggilan
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");
}
Hanya 429 yang dicoba ulang. Respons 403 atau 422 adalah informasi, dan mencobanya lagi hanya mengulang penolakan yang sama. Batasnya 120 permintaan per menit per kunci, dan pipeline ini tidak akan mendekatinya karena satu create_post sudah mencakup semua akun.
Tahap 2: media
Shager mengimpor media dari URL, jadi apa pun yang menghasilkan aset itu (editor, proses render, alat generatif) cukup menaruhnya di tempat yang bisa diakses publik.
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,
});
Lebar, tinggi, dan durasi penting untuk video vertikal. Dengan data itu, Shager memvalidasi Reel, Short, dan Story terhadap aturan setiap platform sebelum dijadwalkan. Klip yang terlalu panjang untuk suatu format langsung gagal sekarang dengan alasan yang jelas, bukannya gagal di satu platform saat pengiriman.

Tahap 3: menentukan akun tujuan
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");
Menyaring hanya yang ACTIVE di sini membuat Anda bisa melaporkan akun yang terputus sejak awal. Halaman yang token-nya kedaluwarsa adalah bahan obrolan dengan klien, bukan sesuatu yang baru ketahuan setelah postingannya gagal.
Tahap 4: satu caption per akun
Di sinilah model Anda benar-benar berperan. Fungsi di bawah sengaja dibiarkan kosong; sambungkan dengan model dan SDK mana pun yang dipakai tim Anda.
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);
Ada dua aturan yang wajib selalu ada di prompt itu. Pertama, fakta harus sama di mana-mana, karena harga atau tanggal yang berbeda antarhalaman adalah kesalahan nyata. Kedua, tidak ada tautan mentah, karena tautan dipasang terpisah agar setiap akun mendapat versi terlacaknya sendiri. Panduan kami tentang konsistensi merek di banyak akun membahas kapan variasi membantu dan kapan justru merugikan.
Tahap 5: membuat draf
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 memberi setiap akun tautan pendeknya sendiri dengan utm_content yang menandai akun tersebut. first_comment dipasang tepat setelah setiap postingan tayang. stagger_seconds memberi jeda acak 30 sampai 90 detik antarakun di platform yang sama, yang juga merupakan nilai default-nya. Hasilnya adalah sebuah DRAFT.
Tahap 6: persetujuan
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);
Peninjau membuka tautan dan menyetujui tanpa perlu masuk. Pipeline berhenti di sini dan dilanjutkan nanti; persetujuan butuh hitungan jam, bukan milidetik. Simpan draft.id dan lanjutkan di proses kedua yang memeriksa get_post sampai statusnya APPROVED. Kalau Anda ingin tahu kenapa titik pemeriksaan ini diletakkan setelah penyusunan draf dan bukan setelah publikasi, artikel alur persetujuan klien menjelaskan alasannya.
Tahap 7: menjadwalkan
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;
}
}
Tahap ini butuh kunci dengan cakupan publish, yang diberikan oleh manusia di https://app.shager.ai/dashboard/agents. Di sini Shager memvalidasi setiap tujuan terhadap aturan platformnya, sehingga 422 rejected muncul sebelum ada apa pun yang masuk antrean.
Tahap 8: memeriksa pengiriman dan mengukur hasil
Setelah waktu publikasi lewat, periksa setiap tujuan. Status pengirimannya adalah PENDING, QUEUED, PROCESSING, PUBLISHED, FAILED, dan 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);
Data klik per akun adalah akhir dari bagian Shager dan awal dari bagian klien. Cara menghubungkannya ke prospek lewat utm_content di CRM dibahas di atribusi prospek per akun.
FAQ
Sebaiknya dijalankan klien agen atau skrip?
Keduanya bisa. Skrip lebih cocok untuk tahap-tahap yang tetap dan apa pun yang berjalan terjadwal. Klien agen seperti Claude Code lebih cocok saat brief-nya masih samar dan Anda ingin menyempurnakan caption secara interaktif. Banyak tim memakai agen untuk tahap 4 dan kode untuk semua sisanya.
Bagaimana kalau brief menargetkan akun dari beberapa klien?
Jalankan pipeline sekali per ruang kerja. Ruang kerja memang sengaja memisahkan klien: akun, persetujuan, tautan, dan analitik mereka tidak pernah tercampur, dan itu juga yang diharapkan klien saat membaca laporannya.
Di mana saya bisa menemukan definisi kolom yang lengkap?
Di dokumen OpenAPI, yang mencantumkan setiap operasi, input, dan cakupan. Halaman agen berisi informasi yang sama dalam bentuk lebih ringkas untuk agen dan pembuatnya.
Langkah berikutnya
Jalankan pipeline sekali dengan satu akun uji dan draf yang tidak pernah Anda jadwalkan. Baca draf itu di Shager, periksa tautan per akun, lalu perluas ke seluruh grup. Setelah semuanya berjalan mulus, kemas tahap-tahapnya dalam rutinitas mingguan agar pipeline berjalan sesuai jadwal, bukan bergantung pada ingatan seseorang.


