# La pipeline di contenuti di un agente, dall'inizio alla fine: brief in entrata, post per ogni account in uscita, clic di ritorno, con ogni chiamata a Shager in ordine

_2026-10-11 · Una pipeline completa per agenti su Shager in TypeScript: brief, media, testi per account, bozza, approvazione, programmazione scaglionata, verifiche e clic._

Una pipeline di contenuti completa per un agente su Shager ha otto fasi: leggere il brief, preparare i media a un URL pubblico, individuare gli account di destinazione, scrivere un testo per ogni account, creare una bozza con un link tracciato, ottenere l'approvazione, programmare con invio scaglionato e infine verificare la consegna e raccogliere i clic per account. L'agente si prende le fasi ripetitive; una persona resta responsabile del brief e dell'approvazione. Ogni passaggio su Shager è una chiamata REST o uno strumento MCP con lo stesso nome.

Qui sotto trovi l'intera catena come un unico programma TypeScript che usa l'API REST, con il ragionamento dietro ogni fase. Se preferisci guidarla da un client per agenti, le chiamate agli strumenti sono identiche.

## Quali sono le fasi e chi è responsabile di ciascuna?

| Fase | Responsabile | Chiamata a Shager |
|---|---|---|
| 1. Brief | Persona | Nessuna |
| 2. Media | Agente o la tua pipeline dei materiali | `upload_media_from_url` |
| 3. Destinazioni | Agente | `list_channels`, `list_groups` |
| 4. Testi | Agente (il tuo modello) | Nessuna |
| 5. Bozza | Agente | `create_post` |
| 6. Approvazione | Cliente | `request_approval` |
| 7. Programmazione | Agente con `publish`, oppure una persona | `schedule_post` |
| 8. Verifica e misurazione | Agente | `get_post`, `get_analytics` |

La fase 4 è l'unica che richiede un modello linguistico. Tutto il resto è idraulica deterministica e va scritto come codice, non lasciato all'improvvisazione del modello. Questa divisione è la lezione più importante sull'affidabilità nella distribuzione con agenti: il modello scrive, il codice orchestra.

## Fase 1: cosa va nel brief?

Un brief su cui la pipeline possa lavorare ha otto campi. Con meno, l'agente comincia a tirare a indovinare.

```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` contiene un offset di proposito. Shager richiede per `schedule_at` il formato ISO 8601 con offset, e scriverlo già nel brief evita a chiunque di dover ricordare a quale fuso orario si riferissero "le 8".

## Configurazione: un'unica funzione per tutte le chiamate

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

Si ritenta solo il 429. Un 403 o un 422 è un'informazione: ritentare significa solo ricevere lo stesso rifiuto. Il limite è di 120 richieste al minuto per chiave, e questa pipeline non ci si avvicina nemmeno, perché un solo `create_post` copre tutti gli account.

## Fase 2: i media

Shager importa i media da un URL, quindi qualunque cosa abbia prodotto il materiale (un editor, un job di rendering, uno strumento di generazione) deve solo lasciarlo in un posto raggiungibile pubblicamente.

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

Larghezza, altezza e durata contano per i video verticali. Con questi dati Shager verifica Reel, Short e storie rispetto alle regole di ogni social prima della programmazione, così una clip troppo lunga per un formato fallisce subito con un motivo leggibile, invece di fallire su un social al momento dell'invio.

## Fase 3: individuare le destinazioni

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

Filtrare qui sugli account `ACTIVE` ti permette di segnalare subito quelli scollegati. Una pagina con il token scaduto è una conversazione da fare con il cliente, non una sorpresa da scoprire dopo che il post è fallito.

## Fase 4: un testo per ogni account

È qui che il tuo modello si guadagna il posto. La funzione qui sotto è volutamente uno stub: collega il modello e l'SDK che usa il tuo team.

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

Due regole vanno in quel prompt ogni volta. Gli stessi fatti ovunque, perché un prezzo o una data che cambiano da una pagina all'altra sono un errore vero. E niente link in chiaro, perché il link si aggiunge a parte, in modo che ogni account abbia la sua versione tracciata. La nostra guida sulla [coerenza del marchio su molti account](/en/blog/brand-consistency-across-many-accounts) spiega dove la variazione aiuta e dove fa danni.

## Fase 5: creare la bozza

```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` assegna a ogni account il suo link breve, con un `utm_content` che identifica l'account. `first_comment` viene pubblicato subito dopo che ogni post va online. `stagger_seconds` distanzia gli account dello stesso social di un intervallo casuale tra 30 e 90 secondi, che è anche il valore predefinito. Il risultato è un `DRAFT`.

## Fase 6: l'approvazione

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

Il revisore apre il link e approva senza accedere. Qui la pipeline si ferma e riprende più tardi: un'approvazione richiede ore, non millisecondi. Salva `draft.id` e riprendilo in una seconda esecuzione che controlla con `get_post` lo stato `APPROVED`. Se vuoi sapere perché questo controllo sta dopo la bozza e non dopo la pubblicazione, lo spieghiamo in [flussi di approvazione per i clienti](/en/blog/client-approval-workflow-for-agencies).

## Fase 7: programmare

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

Qui serve una chiave con il permesso `publish`, concesso da una persona su https://app.shager.ai/dashboard/agents. In questa fase Shager verifica ogni destinazione rispetto alle regole del suo social, quindi un 422 `rejected` arriva prima che qualcosa finisca in coda.

## Fase 8: verificare la consegna e misurare

Dopo l'orario di pubblicazione, controlla ogni destinazione. Gli stati di consegna sono `PENDING`, `QUEUED`, `PROCESSING`, `PUBLISHED`, `FAILED` e `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);
```

I clic per account sono il punto in cui finisce il lavoro di Shager e comincia quello del cliente. Come collegarli ai contatti tramite `utm_content` nel CRM lo spieghiamo in [attribuzione dei contatti per account](/en/blog/utm-per-account-lead-attribution).

## Domande frequenti

### Conviene farla girare a un client per agenti o a uno script?

Funzionano entrambi. Uno script è meglio per le fasi fisse e per tutto ciò che parte a orario. Un client per agenti come Claude Code è meglio quando il brief è vago e vuoi lavorare sui testi in modo interattivo. Molti team usano l'agente per la fase 4 e il codice per tutto il resto.

### E se il brief riguarda account di più clienti?

Esegui la pipeline una volta per workspace. I workspace separano i clienti di proposito: account, approvazioni, link e analytics non si mescolano mai, ed è anche quello che il cliente si aspetta quando legge il suo report.

### Dove trovo le definizioni esatte dei campi?

Nel [documento OpenAPI](https://app.shager.ai/api/v1/openapi.json), che elenca ogni operazione, input e permesso. La [pagina per gli agenti](/en/agents) riporta le stesse informazioni in forma più sintetica, per gli agenti e per chi li sviluppa.

## Prossimo passo

Esegui la pipeline una volta con un solo account di prova e una bozza che non programmerai mai. Leggi la bozza in Shager, controlla i link per account e poi allarga al gruppo completo. Quando tutto regge, racchiudi le fasi in [una routine settimanale](/en/blog/skill-weekly-distribution-routine), così la pipeline parte in base a un calendario e non alla memoria di qualcuno.

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