エージェントのコンテンツパイプライン完全版:ブリーフからアカウント別投稿、クリック計測まで Shager の呼び出しを順番に解説
TypeScript で組む Shager のエージェントパイプライン。ブリーフ、メディア、アカウント別キャプション、承認、分散予約、配信確認、クリック計測まで。

Shager でエンドツーエンドのエージェント・コンテンツパイプラインを組むと、8つの段階になります。ブリーフを読む、メディアを公開 URL に置く、投稿先アカウントを特定する、アカウントごとにキャプションを1本ずつ書く、計測リンク付きの下書きを作る、承認を得る、配信タイミングをずらして予約する、そして配信を確認してアカウント別のクリック数を取得する、という流れです。繰り返しの作業はエージェントが担い、ブリーフと承認は人が持ちます。Shager 側の各ステップは、REST の呼び出し1回、または同じ名前の MCP ツール1つに対応しています。
以下では、この一連の流れを REST API に対する1本の TypeScript プログラムとして示し、各段階の考え方を説明します。エージェントクライアントから動かしたい場合も、ツールの呼び出しはまったく同じです。
どんな段階があり、それぞれ誰が担当するのか
| 段階 | 担当 | Shager の呼び出し |
|---|---|---|
| 1. ブリーフ | 人 | なし |
| 2. メディア | エージェント、または自社の素材パイプライン | upload_media_from_url |
| 3. 投稿先 | エージェント | list_channels、list_groups |
| 4. キャプション | エージェント(自社のモデル) | なし |
| 5. 下書き | エージェント | create_post |
| 6. 承認 | クライアント | request_approval |
| 7. 予約 | publish 権限を持つエージェント、または人 |
schedule_post |
| 8. 確認と計測 | エージェント | get_post、get_analytics |
言語モデルが必要なのは段階4だけです。それ以外は決まった処理をつなぐ配管にすぎず、モデルのアドリブに任せずにコードとして書くべきです。この役割分担こそ、エージェントによる配信で信頼性を保つための最大の教訓です。書くのはモデル、段取りはコード。

段階1:ブリーフには何を書くか
パイプラインが動ける形のブリーフには、8つのフィールドがあります。これより少ないと、エージェントは推測で埋め始めます。
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 にオフセットを含めているのは意図的です。Shager の schedule_at はオフセット付きの ISO 8601 形式でなければなりません。ブリーフの段階でオフセットを書いておけば、「朝8時」がどのタイムゾーンのことだったか、誰も覚えておく必要がなくなります。
準備:すべての呼び出しに使うヘルパー
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");
}
リトライするのは 429 だけです。403 や 422 は情報であり、リトライしても同じ拒否が繰り返されるだけです。上限はキー1つあたり毎分120リクエストですが、create_post 1回で全アカウントをカバーするので、このパイプラインが上限に近づくことはありません。
段階2:メディア
Shager は URL からメディアを取り込みます。そのため、素材を作った側(編集担当、レンダリングジョブ、生成ツールなど)は、公開でアクセスできる場所に置いておくだけで済みます。
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,
});
縦型動画では、幅、高さ、再生時間が重要です。これらがあれば、Shager は予約の前にリール、ショート、ストーリーズを各ネットワークのルールに照らして検証します。フォーマットに対して長すぎるクリップは、配信時にどこか1つのネットワークで失敗するのではなく、この時点で理由がわかる形でエラーになります。

段階3:投稿先を特定する
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");
ここで ACTIVE に絞り込んでおけば、接続が切れているアカウントを最初に報告できます。トークンが期限切れになったページは、クライアントと話し合うべき事柄です。投稿が失敗してから気づくものではありません。
段階4:アカウントごとにキャプションを1本
ここでモデルが本領を発揮します。下の関数はあえて中身を空にしてあります。チームで使っているモデルと SDK を組み込んでください。
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);
このプロンプトには、毎回必ず入れるべきルールが2つあります。1つは、事実はどこでも同じにすること。ページによって価格や日付が食い違えば、それは明確な誤りです。もう1つは、URL をそのまま書かないこと。リンクは別に付けるので、アカウントごとに専用の計測リンクが割り当てられます。どこでバリエーションが効き、どこで逆効果になるかは、多数のアカウントでブランドの一貫性を保つガイドで解説しています。
段階5:下書きを作成する
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 を指定すると、アカウントごとに専用の短縮リンクが作られ、utm_content でどのアカウントかを識別できます。first_comment は、各投稿が公開された直後に投稿されます。stagger_seconds は、同じネットワーク上のアカウント同士の間隔を30〜90秒のランダムな値で空けるもので、これはデフォルト値でもあります。結果は DRAFT になります。
段階6:承認
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);
承認者はリンクを開き、ログインせずに承認できます。パイプラインはここでいったん止まり、あとで再開します。承認にかかるのはミリ秒ではなく数時間です。draft.id を保存しておき、2回目の実行で get_post が APPROVED になっているかを確認して続きを処理します。このチェックポイントを公開後ではなく下書き作成後に置く理由は、エージェンシーのクライアント承認フローで詳しく説明しています。
段階7:予約
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;
}
}
この段階には publish スコープを持つキーが必要で、その権限は人が https://app.shager.ai/dashboard/agents で付与します。Shager はここですべての投稿先を各ネットワークのルールに照らして検証するので、422 rejected は何かがキューに入る前に返ってきます。
段階8:配信を確認して計測する
公開時刻を過ぎたら、投稿先ごとに状態を確認します。配信の状態は PENDING、QUEUED、PROCESSING、PUBLISHED、FAILED、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);
アカウント別のクリック数で Shager の役割は終わり、そこからはクライアント側の仕事です。CRM で utm_content を使ってクリックとリードを結びつける方法は、アカウント別のリード貢献分析で解説しています。
よくある質問
これを動かすのはエージェントクライアントとスクリプトのどちらがよいですか?
どちらでも動きます。決まった段階や定時実行するものはスクリプトが向いています。ブリーフがまだ固まっておらず、キャプションを対話しながら詰めたいときは、Claude Code のようなエージェントクライアントが向いています。段階4はエージェント、それ以外はコード、という使い分けをしているチームも多くあります。
ブリーフの投稿先が複数のクライアントにまたがる場合は?
ワークスペースごとに1回ずつパイプラインを実行してください。ワークスペースは意図的にクライアントを分けています。アカウント、承認、リンク、分析が混ざることはありません。クライアントがレポートを読むときに期待しているのも、まさにその状態です。
フィールドの正確な定義はどこで確認できますか?
OpenAPI ドキュメントに、すべての操作、入力、スコープが載っています。エージェント向けページには、同じ内容がエージェントとその開発者向けに短くまとめられています。
次のステップ
まずはテスト用アカウント1つと、予約しない下書きでパイプラインを1回動かしてみてください。Shager で下書きを確認し、アカウント別のリンクをチェックしてから、グループ全体に広げます。それが安定したら、各段階を週次ルーティンにまとめましょう。そうすれば、パイプラインは誰かの記憶ではなくスケジュールに沿って動くようになります。


