El pipeline de contenido de un agente, de punta a punta: entra un brief, salen publicaciones por cuenta y vuelven los clics, con cada llamada a Shager en orden
Un pipeline completo de agente en Shager con TypeScript: brief, medios, textos por cuenta, borrador, aprobación, programación escalonada, entrega y clics.

Un pipeline de contenido completo para un agente en Shager tiene ocho etapas: leer el brief, dejar los medios en una URL pública, resolver las cuentas de destino, escribir un texto por cuenta, crear un borrador con un enlace con seguimiento, conseguir la aprobación, programar con envío escalonado y, por último, comprobar la entrega y traer los clics por cuenta. El agente se encarga de las etapas repetitivas; una persona se encarga del brief y de la aprobación. Cada paso en Shager es una llamada REST o una herramienta MCP con el mismo nombre.
A continuación está la cadena entera como un único programa en TypeScript contra la API REST, con el porqué de cada etapa. Si prefieres manejarla desde un cliente de agente, las llamadas a herramientas son idénticas.
¿Cuáles son las etapas y quién se encarga de cada una?
| Etapa | Responsable | Llamada a Shager |
|---|---|---|
| 1. Brief | Persona | Ninguna |
| 2. Medios | Agente o tu pipeline de materiales | upload_media_from_url |
| 3. Destinos | Agente | list_channels, list_groups |
| 4. Textos | Agente (tu modelo) | Ninguna |
| 5. Borrador | Agente | create_post |
| 6. Aprobación | Cliente | request_approval |
| 7. Programación | Agente con publish, o una persona |
schedule_post |
| 8. Verificación y medición | Agente | get_post, get_analytics |
La etapa 4 es la única que necesita un modelo de lenguaje. Todo lo demás es fontanería determinista y debería escribirse como código, no dejarse a la improvisación del modelo. Esa división es la lección de confiabilidad más importante de la distribución con agentes: que el modelo escriba y que el código orqueste.

Etapa 1: ¿qué lleva el brief?
Un brief con el que el pipeline pueda trabajar tiene ocho campos. Con menos, el agente empieza a adivinar.
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 lleva una zona horaria a propósito. Shager exige ISO 8601 con desfase horario en schedule_at, y escribir el desfase en el brief evita que alguien tenga que recordar a qué zona horaria se refería "las 8".
Preparación: una sola función auxiliar para todas las llamadas
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");
}
Solo se reintenta el 429. Un 403 o un 422 es información, y reintentarlo solo repite el mismo rechazo. El límite es de 120 solicitudes por minuto por clave, algo que este pipeline ni se acerca a tocar, porque un solo create_post cubre todas las cuentas.
Etapa 2: medios
Shager importa los medios desde una URL, así que lo que sea que haya producido el material (un editor, un render, una herramienta de generación) solo tiene que dejarlo en un lugar accesible públicamente.
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,
});
El ancho, el alto y la duración son importantes en el video vertical. Con esos datos, Shager valida los Reels, Shorts e historias contra las reglas de cada red antes de programar, así que un clip demasiado largo para un formato falla ahora, con un motivo legible, en lugar de fallar en una sola red en el momento del envío.

Etapa 3: resolver los 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 en este punto te permite avisar desde el principio de las cuentas desconectadas. Una página cuyo token venció es una conversación con el cliente, no algo que se descubre después de que la publicación falle.
Etapa 4: un texto por cuenta
Aquí es donde tu modelo se gana su lugar. La función de abajo es un esqueleto a propósito; conecta el modelo y el SDK que use tu equipo.
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);
Hay dos reglas que deben ir en ese prompt siempre. Los mismos datos en todas partes, porque un precio o una fecha que cambia de una página a otra es un error real. Y nada de enlaces sueltos, porque el enlace se adjunta aparte para que cada cuenta reciba su propia versión con seguimiento. Nuestra guía sobre coherencia de marca en muchas cuentas explica dónde ayuda la variación y dónde perjudica.
Etapa 5: crear el borrador
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 le da a cada cuenta su propio enlace corto, con un utm_content que identifica la cuenta. first_comment se publica justo después de que cada publicación sale en vivo. stagger_seconds separa las cuentas de una misma red entre 30 y 90 segundos al azar, que además es el valor predeterminado. El resultado es un DRAFT.
Etapa 6: aprobación
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);
El revisor abre el enlace y aprueba sin iniciar sesión. El pipeline se detiene aquí y retoma más tarde: una aprobación tarda horas, no milisegundos. Guarda draft.id y recógelo en una segunda ejecución que consulte get_post hasta ver APPROVED. Si quieres saber por qué este punto de control va después del borrador y no después de publicar, los flujos de aprobación del cliente lo explican.
Etapa 7: programar
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;
}
}
Esto requiere una clave con el permiso publish, otorgado por una persona en https://app.shager.ai/dashboard/agents. En este paso Shager valida cada destino contra las reglas de su red, así que un 422 rejected llega antes de que haya algo en cola.
Etapa 8: comprobar la entrega y medir
Pasada la hora de publicación, revisa cada destino. Los estados de entrega son PENDING, QUEUED, PROCESSING, PUBLISHED, FAILED y 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);
Los clics por cuenta son donde termina la parte de Shager y empieza la del cliente. Cómo cruzarlos con los leads a través de utm_content en el CRM se explica en atribución de leads por cuenta.
Preguntas frecuentes
¿Conviene que esto lo ejecute un cliente de agente o un script?
Las dos opciones funcionan. Un script es mejor para las etapas fijas y para todo lo que corre con temporizador. Un cliente de agente como Claude Code es mejor cuando el brief es difuso y quieres iterar los textos de forma interactiva. Muchos equipos usan el agente para la etapa 4 y código para todo lo demás.
¿Y si el brief apunta a cuentas de varios clientes?
Ejecuta el pipeline una vez por cliente. Los clientes están separados a propósito: sus cuentas, aprobaciones, enlaces y analíticas nunca se mezclan, que es también lo que el cliente espera cuando lee su reporte.
¿Dónde encuentro la definición exacta de los campos?
En el documento OpenAPI, que lista cada operación, entrada y permiso. La página de agentes tiene la misma información en versión breve para los agentes y quienes los construyen.
Siguiente paso
Ejecuta el pipeline una vez con una sola cuenta de prueba y un borrador que nunca programes. Revisa el borrador en Shager, comprueba los enlaces de cada cuenta y luego amplíalo al grupo completo. Cuando eso funcione, envuelve las etapas en una rutina semanal para que el pipeline corra con un calendario fijo y no dependa de la memoria de nadie.


