Le pipeline de contenu d'un agent, de A à Z : du brief aux publications par compte, puis aux clics, avec chaque appel Shager dans l'ordre

Un pipeline d'agent complet sur Shager en TypeScript : brief, médias, légendes par compte, brouillon, validation client, programmation échelonnée et clics.

Sur Shager, un pipeline de contenu piloté par un agent comporte huit étapes : lire le brief, mettre les médias à disposition à une URL publique, déterminer les comptes cibles, rédiger une légende par compte, créer un brouillon avec un lien suivi, obtenir la validation, programmer avec un envoi échelonné, puis vérifier la livraison et récupérer les clics par compte. L'agent se charge des étapes répétitives ; un humain garde la main sur le brief et la validation. Chaque étape côté Shager correspond à un appel REST, ou à un outil MCP du même nom.

Voici toute la chaîne sous la forme d'un seul programme TypeScript qui s'appuie sur l'API REST, avec la logique de chaque étape. Si vous préférez la piloter depuis un client d'agent, les appels d'outils sont identiques.

Quelles sont les étapes, et qui est responsable de chacune ?

Étape Responsable Appel Shager
1. Brief Humain Aucun
2. Médias Agent ou votre chaîne de production de visuels upload_media_from_url
3. Cibles Agent list_channels, list_groups
4. Légendes Agent (votre modèle) Aucun
5. Brouillon Agent create_post
6. Validation Client request_approval
7. Programmation Agent disposant de publish, ou humain schedule_post
8. Vérification et mesure Agent get_post, get_analytics

L'étape 4 est la seule qui nécessite un modèle de langage. Tout le reste relève de la tuyauterie déterministe, et doit être écrit en code plutôt que laissé à l'improvisation du modèle. C'est la principale leçon de fiabilité en matière de diffusion par agent : laissez le modèle écrire, laissez le code orchestrer.

Schéma : brief, brouillon par compte, validation du client, publication étalée et rapport de clics par compte.
Cinq étapes fixes. La validation de chaque publication est facultative selon le client ; pour publier, il faut un propriétaire humain et un droit de publication distinct sur la clé.

Étape 1 : que doit contenir le brief ?

Un brief exploitable par le pipeline comporte huit champs. S'il en manque, l'agent commence à deviner.

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 porte un décalage horaire, et ce n'est pas un hasard. Shager exige pour schedule_at une date au format ISO 8601 avec décalage. L'inscrire dans le brief évite d'avoir à se souvenir à quel fuseau horaire correspondait « 8 h ».

Mise en place : une seule fonction utilitaire pour tous les appels

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

Seule l'erreur 429 est relancée. Une 403 ou une 422 est une information : la relancer ne fait que répéter le même refus. La limite est de 120 requêtes par minute et par clé, un plafond que ce pipeline n'approchera pas, puisqu'un seul create_post couvre tous les comptes.

Étape 2 : les médias

Shager importe les médias depuis une URL. Ce qui a produit le fichier (un monteur, un job de rendu, un outil de génération) n'a donc qu'à le déposer à un endroit accessible publiquement.

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

La largeur, la hauteur et la durée comptent pour la vidéo verticale. Grâce à elles, Shager vérifie les Reels, les Shorts et les stories au regard des règles de chaque réseau avant la programmation. Un clip trop long pour un format échoue donc tout de suite, avec une explication lisible, au lieu d'échouer sur un réseau au moment de l'envoi.

Diagramme en barres : clics sur les liens suivis de Shager par compte, pour voir quelle page a apporté du trafic.
Ce que l'agent obtient de get_analytics. (Illustration, pas de vraies données)

Étape 3 : déterminer les cibles

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

Filtrer sur ACTIVE à ce stade permet de signaler d'emblée les comptes déconnectés. Une page dont le jeton a expiré, c'est un échange à avoir avec le client, pas une surprise à découvrir après l'échec de la publication.

Étape 4 : une légende par compte

C'est là que votre modèle justifie sa présence. La fonction ci-dessous est volontairement un squelette ; branchez-y le modèle et le SDK qu'utilise votre équipe.

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

Deux règles doivent figurer dans ce prompt à chaque fois. Les mêmes faits partout, car un prix ou une date qui varie d'une page à l'autre est une véritable erreur. Et aucun lien brut, car le lien est joint séparément pour que chaque compte reçoive sa propre version suivie. Notre guide sur la cohérence de marque sur de nombreux comptes explique où la variation aide et où elle nuit.

Étape 5 : créer le brouillon

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 attribue à chaque compte son propre lien court, avec un utm_content qui identifie le compte. first_comment est publié juste après la mise en ligne de chaque publication. stagger_seconds espace les comptes d'un même réseau d'un délai aléatoire de 30 à 90 secondes, ce qui est aussi la valeur par défaut. Le résultat est un DRAFT.

Étape 6 : la validation

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

Le relecteur ouvre le lien et valide sans avoir à se connecter. Le pipeline s'arrête là et reprend plus tard : une validation prend des heures, pas des millisecondes. Conservez draft.id et reprenez-le lors d'une seconde exécution qui vérifie avec get_post que le statut est APPROVED. Pour comprendre pourquoi ce point de contrôle se situe après la rédaction du brouillon et non après la publication, lisez notre article sur les circuits de validation client.

Étape 7 : la programmation

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

Cette étape nécessite une clé dotée du scope publish, accordé par un humain sur https://app.shager.ai/dashboard/agents. C'est à ce moment que Shager vérifie chaque destination au regard des règles de son réseau : une erreur 422 rejected arrive donc avant que quoi que ce soit ne soit mis en file d'attente.

Étape 8 : vérifier la livraison et mesurer

Une fois l'heure de publication passée, vérifiez chaque destination. Les états de livraison sont PENDING, QUEUED, PROCESSING, PUBLISHED, FAILED et 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);

Les clics par compte marquent la fin du travail de Shager et le début de celui du client. Pour les rapprocher des prospects grâce à utm_content dans le CRM, consultez notre article sur l'attribution des prospects par compte.

FAQ

Faut-il faire tourner ce pipeline dans un client d'agent ou dans un script ?

Les deux fonctionnent. Un script convient mieux aux étapes fixes et à tout ce qui tourne à heure fixe. Un client d'agent comme Claude Code est préférable quand le brief est flou et que vous voulez retravailler les légendes de manière interactive. Beaucoup d'équipes confient l'étape 4 à l'agent et tout le reste au code.

Et si le brief cible des comptes de plusieurs clients ?

Lancez le pipeline une fois par espace de travail. Les espaces de travail séparent les clients à dessein : leurs comptes, leurs validations, leurs liens et leurs statistiques ne se mélangent jamais. C'est d'ailleurs ce qu'attend le client quand il lit son rapport.

Où trouver la définition exacte des champs ?

Dans le document OpenAPI, qui recense chaque opération, chaque paramètre et chaque scope. La page agents présente les mêmes informations sous une forme plus courte, à l'intention des agents et de ceux qui les conçoivent.

Prochaine étape

Faites tourner le pipeline une première fois avec un seul compte de test et un brouillon que vous ne programmerez jamais. Relisez le brouillon dans Shager, vérifiez les liens de chaque compte, puis étendez au groupe entier. Une fois que tout tient, intégrez les étapes dans une routine hebdomadaire, pour que le pipeline tourne selon un calendrier et non selon la mémoire de quelqu'un.

Essayez sur vos propres comptes.

Gratuit pendant le lancement, sans carte. Connectez, écrivez une fois, et Shager publie et mesure.