# Pipeline Konten Agen dari Awal sampai Akhir: Brief Masuk, Postingan per Akun Keluar, Data Klik Kembali, Lengkap dengan Urutan Setiap Panggilan Shager

_2026-10-11 · 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.

```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` 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

```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");
}
```

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.

```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,
});
```

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

```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");
```

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.

```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);
```

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](/en/blog/brand-consistency-across-many-accounts) membahas kapan variasi membantu dan kapan justru merugikan.

## Tahap 5: membuat draf

```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` 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

```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);
```

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](/en/blog/client-approval-workflow-for-agencies) menjelaskan alasannya.

## Tahap 7: menjadwalkan

```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;
  }
}
```

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

```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);
```

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](/en/blog/utm-per-account-lead-attribution).

## 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](https://app.shager.ai/api/v1/openapi.json), yang mencantumkan setiap operasi, input, dan cakupan. [Halaman agen](/en/agents) 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](/en/blog/skill-weekly-distribution-routine) agar pipeline berjalan sesuai jadwal, bukan bergantung pada ingatan seseorang.

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