# 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

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

## Étape 1 : que doit contenir le brief ?

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

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

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

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.

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

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.

## Étape 3 : déterminer les cibles

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

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.

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

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](/fr/blog/brand-consistency-across-many-accounts) explique où la variation aide et où elle nuit.

## Étape 5 : créer le brouillon

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

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

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

## Étape 7 : la programmation

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

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

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

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

## 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](https://app.shager.ai/api/v1/openapi.json), qui recense chaque opération, chaque paramètre et chaque scope. La [page agents](/fr/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](/fr/blog/skill-weekly-distribution-routine), pour que le pipeline tourne selon un calendrier et non selon la mémoire de quelqu'un.

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