Todos hablan de agentes de IA hoy en día, pero la mayoría de los tutoriales se quedan en “llamar a un LLM en un bucle”. Si realmente quieres un agente que funcione sin supervisión —que busque datos, los analice, genere contenido y lo publique— necesitas un backend de verdad, no solo un prompt.

Este artículo recorre la arquitectura que utilizo exactamente para eso: un agente programado que encuentra información fresca, redacta publicaciones para redes sociales con Claude y las publica, construido completamente sobre Hono.js corriendo en Cloudflare Workers.

Usaremos como ejemplo la publicación de noticias financieras en LinkedIn y Reddit, pero el patrón se generaliza a cualquier agente del tipo “observar → pensar → actuar → publicar”.

Por qué Hono.js para backends de agentes

Hono es un framework web pequeño y rápido que corre en Cloudflare Workers, Deno, Bun y Node. Para cargas de trabajo de agentes, tres características lo hacen ideal:

  • Cron Triggers nativos de Cloudflare — los agentes que corren en un horario no necesitan un job scheduler separado ni un servidor siempre encendido.
  • Edge runtime, cold start casi cero — tu agente se despierta, hace su trabajo y desaparece. Pagas por ejecución, no por tiempo de inactividad.
  • Modelo de middleware — autenticación, logging y rate limiting para las rutas administrativas de tu agente vienen incluidos.

La arquitectura

Cron Trigger (Hono en Cloudflare Workers)

   ├── 1. Fetch step   → obtener datos crudos de una API externa
   ├── 2. Reasoning step → Claude decide qué vale la pena publicar
   ├── 3. Generation step → Claude redacta copia específica para cada plataforma
   ├── 4. Dedup check   → Postgres/Neon, saltar lo ya publicado
   └── 5. Publish step  → API social (o un proveedor unificado)

Cada paso es una función asíncrona simple. Sin framework de agentes, sin máquina de estado oculta — solo un pipeline que puedes leer de principio a fin y hacer unit testing.

Configurando el proyecto

npm create hono@latest social-agent
cd social-agent
npm install

Elige la plantilla cloudflare-workers cuando te lo pida. Luego agrega lo necesario:

npm install @anthropic-ai/sdk drizzle-orm @neondatabase/serverless

Paso 1: El cron trigger

En wrangler.toml, define cuándo se despierta el agente:

[triggers]
crons = ["0 * * * 1-5"] # cada hora, solo días de semana

En tu app Hono, maneja el evento scheduled separadamente de las rutas HTTP:

import { Hono } from 'hono'

const app = new Hono()

export default {
  fetch: app.fetch,
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    ctx.waitUntil(runAgentCycle(env))
  },
}

ctx.waitUntil es importante —le indica al runtime de Workers que mantenga la instancia viva hasta que tu trabajo asíncrono termine, incluso sin una respuesta HTTP que esperar.

Paso 2: Obtener datos frescos

Mantén este paso simple. Debe devolver datos estructurados, no decidir nada.

interface RawEvent {
  id: string
  title: string
  payload: Record<string, unknown>
}

async function fetchLatestEvents(env: Env): Promise<RawEvent[]> {
  const res = await fetch('https://api.example.com/events?window=today', {
    headers: { Authorization: `Bearer ${env.SOURCE_API_KEY}` },
  })
  if (!res.ok) throw new Error(`Source fetch failed: ${res.status}`)
  const data = await res.json()
  return data.events
}

Paso 3: Que Claude decida qué publicar

Esta es la parte que muchos omiten y luego se arrepienten. No redactes automáticamente una publicación para cada elemento —primero haz que el modelo haga un triage. Es más barato y evita que tu feed parezca un dump de bot.

import Anthropic from '@anthropic-ai/sdk'

async function triageEvents(events: RawEvent[], env: Env) {
  const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_KEY })

  const message = await anthropic.messages.create({
    model: 'claude-sonnet-4-6',
    max_tokens: 1024,
    system:
      'Evalúa eventos para determinar su valor en redes sociales. Solo marca elementos con un ángulo genuinamente interesante — una sorpresa, un patrón, un número que destaque. Devuelve JSON estricto, sin prosa.',
    messages: [
      {
        role: 'user',
        content: JSON.stringify(events),
      },
    ],
  })

  const text = message.content.find((c) => c.type === 'text')?.text ?? '[]'
  return JSON.parse(text) as { id: string; angle: string }[]
}

Pedir “JSON estricto, sin prosa” desde el principio te evita un paso frágil de regex después.

Paso 4: Generar copia específica para cada plataforma

LinkedIn y Reddit tienen normas diferentes —LinkedIn premia un tono analítico y seguro; Reddit castiga cualquier cosa que parezca copy de marketing. Genera ambas versiones en una sola llamada, pero instruye al modelo para que adapte explícitamente el tono.

async function draftPosts(item: RawEvent, angle: string, env: Env) {
  const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_KEY })

  const message = await anthropic.messages.create({
    model: 'claude-sonnet-4-6',
    max_tokens: 800,
    system: `Escribe dos versiones de una publicación sobre este evento.
LinkedIn: 80-150 palabras, tono analítico, una mención suave de contexto relevante, sin spam de hashtags.
Reddit: redactado como iniciador de discusión, sin lenguaje promocional, termina con una pregunta genuina.
Devuelve JSON: { "linkedin": "...", "reddit": "..." }`,
    messages: [
      { role: 'user', content: `Evento: ${item.title}\nÁngulo: ${angle}\nDatos: ${JSON.stringify(item.payload)}` },
    ],
  })

  const text = message.content.find((c) => c.type === 'text')?.text ?? '{}'
  return JSON.parse(text) as { linkedin: string; reddit: string }
}

Paso 5: Deduplicación con Postgres

Nada mata la credibilidad más rápido que publicar lo mismo dos veces porque un cron se superpuso. Usa Neon, el driver serverless que funciona sobre HTTP —importante en Workers porque no tienes una conexión TCP persistente.

import { neon } from '@neondatabase/serverless'

async function alreadyPosted(env: Env, eventId: string) {
  const sql = neon(env.DATABASE_URL)
  const rows = await sql`SELECT 1 FROM posted_events WHERE event_id = ${eventId}`
  return rows.length > 0
}

async function markPosted(env: Env, eventId: string, platform: string, postId: string) {
  const sql = neon(env.DATABASE_URL)
  await sql`
    INSERT INTO posted_events (event_id, platform, post_id, posted_at)
    VALUES (${eventId}, ${platform}, ${postId}, now())
  `
}

Paso 6: Publicar

Tienes dos opciones reales:

  1. APIs nativas de cada plataforma. LinkedIn requiere una Company Page aprobada y el scope w_member_social; Reddit requiere su propia app OAuth y respeta límites de tasa estrictos. Ambas son funcionales pero lentas de configurar la primera vez.
  2. Un proveedor de publicación unificado (ej. Ayrshare) que abstrae múltiples plataformas detrás de una API. Mucho más rápido para lanzar un MVP.
async function publish(platform: 'linkedin' | 'reddit', content: string, env: Env) {
  const res = await fetch('https://api.ayrshare.com/api/post', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${env.AYRSHARE_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ post: content, platforms: [platform] }),
  })
  if (!res.ok) throw new Error(`Publish failed on ${platform}: ${res.status}`)
  return res.json()
}

Uniendo todo

async function runAgentCycle(env: Env) {
  const events = await fetchLatestEvents(env)
  const flagged = await triageEvents(events, env)

  for (const { id, angle } of flagged) {
    if (await alreadyPosted(env, id)) continue

    const source = events.find((e) => e.id === id)!
    const { linkedin, reddit } = await draftPosts(source, angle, env)

    const li = await publish('linkedin', linkedin, env)
    await markPosted(env, id, 'linkedin', li.id)

    // Reddit: retener para revisión humana
    await queueForReview(env, id, reddit)
  }
}

El guardarraíl que realmente importa

Automatizar el pipeline de fetch → draft es seguro. Automatizar la publicación en Reddit no lo es, al menos al principio. La mayoría de los subreddits activos tienen reglas estrictas contra la autopromoción, y una cuenta que publica en un horario predecible con tono promocional es detectada como bot rápidamente.

Dos cosas solucionan esto:

  • Humano-en-el-loop para Reddit específicamente. Poner el draft en cola (Slack, email, una ruta admin en la misma app Hono) y requerir aprobación manual antes de publicar.
  • Mantener el lenguaje descriptivo, no asesorativo. Para cualquier tema financiero, “los ingresos superaron las estimaciones en X%” es comentario; “deberías comprar esto” se acerca a un consejo del que no quieres ser responsable.

LinkedIn es más tolerante con una cadencia de publicación consistente, así que es la plataforma más segura para automatizar primero.

Hacia dónde ir desde aquí

Este mismo esqueleto —cron trigger, fetch, triage, generar, dedup, publicar— funciona para mucho más que noticias financieras. Cambia el paso de fetch por releases de GitHub, reseñas de productos, CFPs de conferencias o las métricas de uso de tu propio producto, y tienes un agente diferente con las mismas garantías de confiabilidad.

La parte que vale la pena hacer bien desde el principio es el paso de triage. Un agente que publica sobre todo es solo ruido con pasos extra; un agente que solo habla cuando hay un ángulo genuino es al que la gente realmente sigue.

“El mejor agente no es el que más publica, sino el que sabe cuándo hacerlo.”

Si estás construyendo algo similar, nos encantaría saber qué estás automatizando. Sigue explorando estos temas en el blog de DojoFullStack.