O pipeline de conteúdo do agente, de ponta a ponta: do briefing aos cliques, com cada chamada ao Shager na ordem
Um pipeline completo de agente no Shager em TypeScript: briefing, mídia, legenda por conta, rascunho, aprovação, agendamento escalonado e cliques.

Um pipeline de conteúdo de ponta a ponta com agente no Shager tem oito etapas: ler o briefing, deixar a mídia em uma URL pública, definir as contas de destino, escrever uma legenda para cada conta, criar um rascunho com link rastreável, obter a aprovação, agendar com envio escalonado e, por fim, conferir a entrega e puxar os cliques por conta. O agente cuida das etapas repetitivas; uma pessoa cuida do briefing e da aprovação. Cada passo no Shager é uma chamada REST ou uma ferramenta MCP com o mesmo nome.
A seguir está a cadeia inteira como um único programa em TypeScript usando a API REST, com a lógica de cada etapa. Se você preferir conduzir tudo por um cliente de agente, as chamadas de ferramentas são idênticas.
Quais são as etapas, e quem cuida de cada uma?
| Etapa | Responsável | Chamada ao Shager |
|---|---|---|
| 1. Briefing | Pessoa | Nenhuma |
| 2. Mídia | Agente ou o seu pipeline de arquivos | upload_media_from_url |
| 3. Destinos | Agente | list_channels, list_groups |
| 4. Legendas | Agente (o seu modelo) | Nenhuma |
| 5. Rascunho | Agente | create_post |
| 6. Aprovação | Cliente | request_approval |
| 7. Agendamento | Agente com publish, ou uma pessoa |
schedule_post |
| 8. Conferência e medição | Agente | get_post, get_analytics |
A etapa 4 é a única que precisa de um modelo de linguagem. Todo o resto é encanamento determinístico e deve ser escrito como código, em vez de ficar por conta da improvisação do modelo. Essa divisão é a maior lição de confiabilidade na distribuição com agentes: deixe o modelo escrever e deixe o código orquestrar.

Etapa 1: o que vai no briefing?
Um briefing com o qual o pipeline consegue trabalhar tem oito campos. Com menos que isso, o agente começa a adivinhar.
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"
};
O publishAt traz o fuso de propósito. O Shager exige ISO 8601 com offset em schedule_at, e deixar o offset escrito no briefing evita que alguém precise lembrar a qual fuso horário aquele "8h" se referia.
Preparação: uma função auxiliar para todas as chamadas
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");
}
Só o 429 é repetido. Um 403 ou um 422 é informação, e tentar de novo só repete a mesma recusa. O limite é de 120 requisições por minuto por chave, e este pipeline nem chega perto disso, porque um único create_post cobre todas as contas.
Etapa 2: mídia
O Shager importa mídia a partir de uma URL, então o que quer que tenha produzido o arquivo (um editor, um job de renderização, uma ferramenta de geração) só precisa deixá-lo em algum lugar acessível publicamente.
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,
});
Largura, altura e duração fazem diferença em vídeo vertical. Com esses dados, o Shager valida Reels, Shorts e stories segundo as regras de cada rede antes do agendamento. Assim, um vídeo longo demais para um formato falha agora, com um motivo legível, em vez de falhar em uma rede na hora do envio.

Etapa 3: definir os destinos
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");
Filtrar por ACTIVE aqui permite avisar logo de cara quais contas estão desconectadas. Uma página com token expirado é assunto para conversar com o cliente, não algo para descobrir depois que o post falhar.
Etapa 4: uma legenda por conta
É aqui que o seu modelo mostra a que veio. A função abaixo é um esboço de propósito; conecte o modelo e o SDK que a sua equipe usa.
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);
Duas regras precisam estar nesse prompt sempre. Os mesmos fatos em todas as contas, porque um preço ou uma data que muda de uma página para outra é um erro de verdade. E nada de links soltos, porque o link é anexado à parte para que cada conta receba a sua própria versão rastreável. Nosso guia sobre consistência de marca em muitas contas mostra onde a variação ajuda e onde atrapalha.
Etapa 5: criar o rascunho
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],
});
O cta_url dá a cada conta o seu próprio link curto, com um utm_content que identifica a conta. O first_comment é publicado logo depois que cada post entra no ar. O stagger_seconds espaça as contas da mesma rede por um intervalo aleatório de 30 a 90 segundos, que também é o padrão. O resultado é um DRAFT.
Etapa 6: aprovação
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);
O revisor abre o link e aprova sem fazer login. O pipeline para aqui e retoma depois: aprovação leva horas, não milissegundos. Guarde o draft.id e retome em uma segunda execução que consulta get_post até encontrar APPROVED. Se quiser entender por que esse ponto de controle fica depois do rascunho e não depois da publicação, o artigo sobre fluxos de aprovação do cliente explica.
Etapa 7: agendamento
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;
}
}
Isso exige uma chave com o escopo publish, concedido por uma pessoa em https://app.shager.ai/dashboard/agents. Nesse momento, o Shager valida cada destino de acordo com as regras da sua rede, então um 422 rejected chega antes de qualquer coisa entrar na fila.
Etapa 8: conferir a entrega e medir
Depois do horário de publicação, confira cada destino. Os estados de entrega são PENDING, QUEUED, PROCESSING, PUBLISHED, FAILED e 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);
Os cliques por conta são onde termina a parte do Shager e começa a do cliente. Como cruzá-los com leads no CRM usando o utm_content está explicado em atribuição de leads por conta.
Perguntas frequentes
Isso deve rodar em um cliente de agente ou em um script?
Os dois funcionam. Um script é melhor para as etapas fixas e para tudo o que roda em horário marcado. Um cliente de agente como o Claude Code é melhor quando o briefing está vago e você quer iterar nas legendas de forma interativa. Muitas equipes usam o agente na etapa 4 e código em todo o resto.
E se o briefing mirar contas de vários clientes?
Rode o pipeline uma vez por espaço de trabalho. Os espaços de trabalho separam os clientes de propósito: contas, aprovações, links e analytics nunca se misturam, e é exatamente isso que o cliente espera quando lê o relatório dele.
Onde encontro a definição exata dos campos?
No documento OpenAPI, que lista cada operação, entrada e escopo. A página para agentes traz as mesmas informações de forma mais resumida, para os agentes e para quem os constrói.
Próximo passo
Rode o pipeline uma vez com uma única conta de teste e um rascunho que você nunca vai agendar. Leia o rascunho no Shager, confira os links de cada conta e só então amplie para o grupo inteiro. Quando isso estiver funcionando, empacote as etapas em uma rotina semanal, para que o pipeline rode em um horário fixo e não dependa da memória de ninguém.


