Modern Agent Engineering

Cómo crear una aplicación de blog moderna con Astro, Cloudflare D1 y Cloudflare Containers para agentes Agno

Una guía práctica para ingenieros y arquitectos que diseñan una plataforma de blogs con Astro, flujos de borrador y publicación en D1, un editor ligero de markdown y un despliegue en Cloudflare Containers para un servicio de investigación con Agno/AgentOS.

31 min de lectura

La mayoría de los sitios de blogs con Astro comienzan como proyectos de contenido basados en archivos, y por lo general es la decisión correcta. Los archivos markdown en git son simples, rápidos y fáciles de publicar. Pero en cuanto necesitas una interfaz editorial, estados de borrador y publicado, creación de contenido desde el navegador o generación asistida por IA, el enfoque cambia. El contenido deja de centrarse en los archivos y pasa a centrarse en el estado de la aplicación.

Es ahí donde Astro + Cloudflare D1 + Cloudflare Containers se vuelve interesante.

Astro nos da una capa de UI rápida y un modelo de servidor claro. D1 nos da una fuente de verdad SQL ligera para artículos, estados y metadatos. Cloudflare Containers nos permite ejecutar un servicio de Agno / AgentOS basado en Python cerca del resto de la aplicación, sin tener que montar un stack de Kubernetes independiente. El principal desafío de diseño no es si estas herramientas pueden funcionar juntas. Es decidir qué datos pertenecen a D1, cuáles pertenecen al runtime del agente y cuáles nunca deben existir únicamente en el sistema de archivos de un contenedor.

Al April 2, 2026, la documentación oficial respalda los componentes clave que necesita esta arquitectura:

  • El adaptador de Cloudflare para Astro admite rutas renderizadas bajo demanda, server islands, actions y sesiones en Cloudflare Workers.
  • Cloudflare D1 admite bindings de Workers, migraciones SQL, sentencias preparadas, desarrollo local con Wrangler y despliegue en producción mediante Workers.
  • Cloudflare Containers puede ejecutar imágenes de contenedor arbitrarias detrás de Workers, y cada contenedor se administra mediante un plano de control respaldado por Durable Objects.
  • Agno admite SQLite para desarrollo y PostgreSQL para producción, e incluye plantillas de despliegue de AgentOS basadas en Docker.

TL;DR

  • Si solo publicas archivos markdown desde git, Astro Content Collections sigue siendo la solución más simple.
  • Si necesitas crear y editar en el navegador, distinguir entre borradores y contenido publicado y generar borradores con IA, usa renderizado de servidor de Astro en Cloudflare Workers y D1 como fuente de verdad editorial.
  • Usa Astro Actions para guardar cambios en el editor, publicar y validar. Esto simplifica el flujo de formularios frente a conectar endpoints de API manuales por todas partes.
  • Comienza con una sola tabla articles para modelar el blog y agrega article_versions solo cuando el historial de revisiones se convierta en una necesidad real del producto.
  • Ejecuta el servicio de Agno / AgentOS dentro de Cloudflare Containers, pero considera el disco local del contenedor como efímero. Sirve para experimentos locales, pero no como única base de datos de producción.
  • Para almacenar la memoria y las sesiones del servicio del agente, usa SQLite para desarrollo y PostgreSQL para producción, que es lo que recomienda Agno en su documentación.
  • Separación recomendada: Astro + D1 para la aplicación editorial, Cloudflare Container + AgentOS para investigación y redacción, y revisión humana antes de publicar.

Qué aprenderás aquí

  • Cómo decidir cuándo un blog de Astro debe dejar de basarse en archivos y pasar a una base de datos.
  • Cómo modelar artículos en borrador y publicados en Cloudflare D1.
  • Cómo conectar un editor ligero de markdown y un flujo de guardado con Astro Actions.
  • Cómo desplegar un servicio de investigación en Python con Agno / AgentOS mediante Cloudflare Containers.
  • Cómo separar los datos editoriales persistentes del estado del runtime del agente.
  • Qué decisiones de diseño importan más para ingenieros, arquitectos y PMs que evalúan este stack.

El producto que estamos diseñando

Queremos una aplicación de blog con cuatro capacidades:

  1. Los autores pueden crear y editar artículos desde el navegador.
  2. Los artículos pueden estar en dos estados: draft y published.
  3. El editor es un formulario simple de markdown, no un editor pesado basado en bloques.
  4. Un servicio de Agno / AgentOS puede investigar un tema en la web y generar automáticamente un borrador de artículo para revisión humana.

Ese producto implica tres cargas de trabajo diferentes:

  • CRUD editorial: formularios, validación, autenticación, guardar como borrador y publicar.
  • Entrega de contenido: páginas públicas de artículos y páginas de índice.
  • Generación con agentes: investigación de larga duración y generación de borradores con acceso a la web.

Es posible intentar meter las tres en un solo runtime, pero eso lleva a límites de responsabilidad poco claros. Una arquitectura más limpia mantiene la aplicación de blog ligera y determinista, y deja el trabajo probabilístico al runtime del agente.

Por qué Astro sigue siendo una buena opción

A menudo se presenta a Astro como una herramienta para sitios estáticos, pero eso no le hace justicia. Para esta aplicación, Astro es valioso porque nos ofrece:

  • Un modelo de UI rápido y centrado en el contenido.
  • Interactividad parcial mediante islands, en lugar de la sobrecarga de una SPA del lado del cliente en toda la aplicación.
  • Renderizado de servidor en Cloudflare Workers cuando necesitamos páginas autenticadas y procesamiento de formularios.
  • Astro Actions para manejadores de formularios tipados del lado del servidor con validación mediante Zod.

La decisión arquitectónica importante es esta:

Static content site
  -> Astro content collections + markdown files in git

Editorial application
  -> Astro server rendering + D1 + auth + actions

En otras palabras, Astro sigue siendo el framework adecuado, pero cambia la fuente de verdad.

La arquitectura recomendada

Este es el diseño de referencia que usaría.

                    +-----------------------------+
                    |       Browser Clients       |
                    | editors, reviewers, readers |
                    +-------------+---------------+
                                  |
                                  v
                    +-----------------------------+
                    |   Astro app on Workers      |
                    | - public routes             |
                    | - admin routes              |
                    | - Astro Actions             |
                    +------+------+---------------+
                           |      |
                           |      +--------------------+
                           |                           |
                           v                           v
                +--------------------+      +------------------------+
                | Cloudflare D1      |      | Worker route to        |
                | articles, authors, |      | Cloudflare Container   |
                | statuses, metadata |      | Agno / AgentOS app     |
                +----------+---------+      +-----------+------------+
                           ^                            |
                           |                            v
                           |                 +------------------------+
                           |                 | Agno tools + model API |
                           |                 | web search + drafting  |
                           |                 +-----------+------------+
                           |                             |
                           |                             v
                           |                 +------------------------+
                           +-----------------| Agent runtime DB       |
                                             | SQLite dev / Postgres  |
                                             | prod for sessions      |
                                             +------------------------+

La separación de responsabilidades es simple:

  • D1 es responsable de los datos del producto editorial.
  • La base de datos del agente es responsable del estado del runtime del agente.
  • El sistema de archivos del contenedor no es responsable de nada que requiera persistencia a largo plazo.

Por qué D1 debería ser la base de datos del blog

Para este producto específico, D1 encaja bien porque el modelo de datos principal es relacional y de tamaño moderado:

  • artículos
  • autores
  • etiquetas
  • estado de publicación
  • marcas de tiempo
  • metadatos opcionales de revisión o generación

D1 también se alinea operativamente con Astro en Workers:

  • los bindings se configuran en wrangler.jsonc
  • las consultas se ejecutan dentro de la misma aplicación de Cloudflare
  • el desarrollo local está integrado en Wrangler
  • las migraciones son archivos SQL que viven en el repositorio

Este es exactamente el tipo de aplicación en la que una solución SQL «pequeña, predecible y desplegada globalmente» suele ser más adecuada que montar demasiado pronto una plataforma de base de datos más grande.

¿Por qué no seguir usando archivos markdown en git?

Si tus únicos autores son desarrolladores, el markdown basado en archivos puede seguir siendo una excelente solución. Pero para este producto, el contenido basado en archivos genera fricción:

  • guardar un borrador implica escribir en git, no en el estado de la aplicación
  • los flujos de publicación se convierten en flujos de PR, no en flujos editoriales
  • los borradores generados con IA necesitan acceso de escritura al sistema de archivos y a git
  • crear contenido desde el navegador se vuelve incómodo
  • las consultas de metadatos se convierten en búsquedas en el repositorio en lugar de consultas SQL

Eso no significa que el contenido respaldado por git sea incorrecto. Significa que está optimizado para un modelo operativo diferente.

Configuración de Astro para esta aplicación

Para una aplicación editorial real, pondría Astro en modo servidor en Cloudflare Workers:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
import react from '@astrojs/react';

export default defineConfig({
  output: 'server',
  adapter: cloudflare(),
  integrations: [react()],
});

Esta es la decisión adecuada porque:

  • las páginas del editor son dinámicas
  • el envío de formularios mediante actions requiere ejecución en el servidor
  • las comprobaciones de autenticación deben hacerse en el servidor
  • las rutas públicas de artículos pueden seguir siendo muy rápidas incluso con renderizado de servidor

Si más adelante quieres prerenderizar algunas rutas públicas, puedes hacerlo de forma selectiva. Lo importante es dejar de tratar toda la aplicación como estática.

Configuración de Wrangler para D1

Como mínimo, el Worker necesita un binding de D1:

{
  "name": "editorial-platform",
  "compatibility_date": "2026-04-02",
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "editorial-db",
      "database_id": "REPLACE_ME"
    }
  ]
}

Desde Astro, los bindings de Cloudflare están disponibles a través del runtime de Cloudflare:

import { env } from 'cloudflare:workers';

const db = env.DB;

Esa ruta de importación es una de las mejores partes de la integración de Astro + Workers. El código de la aplicación puede mantenerse claro y directo, sin pasar bindings manualmente por cada capa.

El modelo de datos de D1

Para la versión inicial, no modeles más de lo necesario. Comienza con una tabla principal articles.

CREATE TABLE articles (
  id TEXT PRIMARY KEY,
  slug TEXT NOT NULL UNIQUE,
  title TEXT NOT NULL,
  excerpt TEXT,
  body_markdown TEXT NOT NULL,
  status TEXT NOT NULL CHECK (status IN ('draft', 'published')),
  author_id TEXT NOT NULL,
  topic TEXT,
  generated_by_agent INTEGER NOT NULL DEFAULT 0,
  source_list_json TEXT,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  published_at TEXT
);

CREATE INDEX idx_articles_status_updated
  ON articles (status, updated_at DESC);

CREATE INDEX idx_articles_status_published
  ON articles (status, published_at DESC);

Este esquema es suficiente para permitir:

  • guardar borradores
  • publicar artículos
  • listar borradores en la administración
  • listar artículos publicados de forma pública
  • atribuir contenido a una persona o a un flujo asistido por un agente

Más adelante, si crecen las necesidades editoriales, agrega:

  • article_versions
  • article_tags
  • article_generation_jobs
  • article_review_events

Pero yo no empezaría por ahí.

Flujo de migraciones recomendado

Las migraciones de D1 son simples y fáciles de gestionar en el repositorio:

npx wrangler d1 migrations create editorial-db editorial_schema
npx wrangler d1 migrations apply editorial-db --local
npx wrangler d1 migrations apply editorial-db --remote

Una buena regla para los equipos es:

  • usar --local libremente durante el desarrollo
  • usar --remote de forma deliberada
  • nunca editar manualmente el esquema de producción desde el dashboard si se supone que el repositorio es la fuente de verdad

Guardar borradores y publicar con Astro Actions

Astro Actions encaja muy bien con este problema porque el flujo de guardado se basa en formularios y requiere mucha validación.

// src/actions/index.ts
import { defineAction, ActionError } from 'astro:actions';
import { z } from 'astro/zod';
import { env } from 'cloudflare:workers';

export const server = {
  saveArticle: defineAction({
    accept: 'form',
    input: z.object({
      id: z.string().min(1),
      slug: z.string().regex(/^[a-z0-9-]+$/),
      title: z.string().min(3),
      excerpt: z.string().max(280).optional(),
      bodyMarkdown: z.string().min(1),
      authorId: z.string().min(1),
      intent: z.enum(['draft', 'publish']),
    }),
    handler: async (input, context) => {
      if (!context.locals.user) {
        throw new ActionError({ code: 'UNAUTHORIZED' });
      }

      const now = new Date().toISOString();
      const publishedAt = input.intent === 'publish' ? now : null;

      await env.DB.prepare(`
        INSERT INTO articles (
          id, slug, title, excerpt, body_markdown,
          status, author_id, created_at, updated_at, published_at
        )
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        ON CONFLICT(id) DO UPDATE SET
          slug = excluded.slug,
          title = excluded.title,
          excerpt = excluded.excerpt,
          body_markdown = excluded.body_markdown,
          status = excluded.status,
          author_id = excluded.author_id,
          updated_at = excluded.updated_at,
          published_at = excluded.published_at
      `)
        .bind(
          input.id,
          input.slug,
          input.title,
          input.excerpt ?? null,
          input.bodyMarkdown,
          input.intent,
          input.authorId,
          now,
          now,
          publishedAt,
        )
        .run();

      return { id: input.id, status: input.intent };
    },
  }),
};

Esto te da una acción determinista que puede atender ambos botones:

  • Save draft
  • Publish

El navegador simplemente envía el mismo formulario con un valor diferente de intent.

El editor puede ser simple

Para esta aplicación, resistiría la tentación de comenzar con un framework de edición pesado.

El editor útil más simple tiene:

  • un campo para el título
  • un campo para el slug
  • un campo para el extracto
  • un <textarea> para markdown
  • un panel de vista previa en vivo
  • botones para Save draft y Publish

Eso es suficiente para validar el producto rápidamente.

---
export const prerender = false;
import { actions } from 'astro:actions';
---

<form method="POST" action={actions.saveArticle} class="editor-form">
  <input type="hidden" name="id" value={article.id} />
  <input type="hidden" name="authorId" value={user.id} />

  <label>
    Title
    <input name="title" value={article.title} required />
  </label>

  <label>
    Slug
    <input name="slug" value={article.slug} required pattern="^[a-z0-9-]+$" />
  </label>

  <label>
    Excerpt
    <textarea name="excerpt">{article.excerpt}</textarea>
  </label>

  <label>
    Markdown
    <textarea name="bodyMarkdown" rows="24">{article.bodyMarkdown}</textarea>
  </label>

  <button type="submit" name="intent" value="draft">Save draft</button>
  <button type="submit" name="intent" value="publish">Publish</button>
</form>

Si el equipo quiere una vista previa en vivo, agrega una pequeña island de React junto al formulario. Mantén explícito el pipeline de vista previa:

textarea markdown
  -> markdown parser
  -> HTML sanitizer
  -> preview pane

El error más común aquí es renderizar el HTML generado desde markdown sin sanitizarlo. No es un problema de markdown. Es un problema de seguridad de la aplicación.

Lectura de contenido público

Las páginas públicas deben leer solo contenido publicado:

SELECT slug, title, excerpt, body_markdown, published_at
FROM articles
WHERE status = 'published' AND slug = ?;

Y las páginas de listado deben filtrar de la misma manera:

SELECT slug, title, excerpt, published_at
FROM articles
WHERE status = 'published'
ORDER BY published_at DESC
LIMIT ? OFFSET ?;

Esa separación es importante. La visibilidad de los borradores nunca debe depender únicamente de la UI. También debe garantizarse en la capa de consultas.

Qué esperar del desarrollo local

La experiencia actual de desarrollo local de D1 en Cloudflare es buena, pero hay dos detalles operativos que los equipos deben recordar:

  • wrangler dev usa el modo local de forma predeterminada.
  • Los datos locales y remotos de D1 son independientes, a menos que configures el acceso remoto deliberadamente.

Eso es una característica, no un error. Te permite iterar de forma segura en local. También significa que tu equipo debe definir claramente:

  • los datos de prueba
  • el orden de las migraciones
  • la estrategia para restablecer el entorno local
  • cuándo es aceptable trabajar con bindings remotos

Dónde entran Cloudflare Containers

La aplicación editorial no debería ser el lugar donde ejecutamos búsquedas web en Python y lógica de investigación de larga duración. Eso pertenece a un entorno de ejecución separado.

Cloudflare Containers es interesante aquí porque nos ofrece:

  • un runtime basado en Docker para Python
  • enrutamiento y control basados en Workers
  • escalado y gestión del ciclo de vida cerca del resto de la aplicación de Cloudflare
  • una forma clara de mantener el servicio del agente detrás de rutas HTTP controladas por la aplicación

Este es el patrón central:

Astro admin clicks "Generate draft from topic"
  -> Worker route receives topic
  -> Worker chooses or creates a container instance
  -> request is proxied to AgentOS / FastAPI inside the container
  -> agent researches the web and returns markdown draft
  -> Astro app stores result in D1 as status='draft'

Esto mantiene la revisión humana justo donde corresponde:

  • el agente propone
  • el editor revisa
  • la aplicación publica

Una limitación crítica: el disco del contenedor es efímero

Esta es la advertencia de infraestructura más importante de todo el artículo.

Actualmente, Cloudflare Containers se ejecuta con disco efímero. Cuando un contenedor entra en reposo y luego se reinicia, vuelve con un disco nuevo a partir de la imagen. Eso tiene una consecuencia arquitectónica directa:

Okay for local experimentation:
  container-local SQLite file

Not okay as the only production source of truth:
  container-local SQLite file

Si ignoras esa limitación, parecerá que tu servicio del agente funciona en las pruebas, pero luego perderá estado silenciosamente en producción después de apagados por inactividad o reinicios.

La separación adecuada de bases de datos para el servicio del agente

Esta es la recomendación que daría tanto a ingenieros como a PMs:

Opción A: la separación de producción útil más simple

  • D1 almacena los artículos del blog y el estado de publicación.
  • PostgreSQL almacena las sesiones, memorias y metadatos de conocimiento de Agno.

Esta es la opción más adecuada para producción porque sigue las propias recomendaciones de Agno:

  • SQLite para desarrollo
  • PostgreSQL para producción

Opción B: servicio del agente con persistencia reducida

  • D1 almacena los borradores finales generados y los metadatos editoriales.
  • El runtime del agente mantiene un estado de sesión mínimo y de corta duración.
  • SQLite local al contenedor solo se acepta para estado no crítico o descartable.

Esto puede funcionar para herramientas internas ligeras, pero no lo convertiría en la arquitectura predeterminada de un pipeline de publicación serio.

Ejemplo de configuración de Cloudflare Container

El punto de entrada exacto del Worker depende de cómo integres el adaptador de Cloudflare para Astro con el resto de tu despliegue, pero las adiciones importantes a Wrangler son el binding de D1 y los bloques de contenedor, Durable Object y migración.

Un ejemplo de configuración combinada se ve así:

{
  "name": "astro-editorial-platform",
  "compatibility_date": "2026-04-02",
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "editorial-db",
      "database_id": "REPLACE_ME"
    }
  ],
  "containers": [
    {
      "class_name": "ResearchAgent",
      "image": "./agent/Dockerfile",
      "max_instances": 3
    }
  ],
  "durable_objects": {
    "bindings": [
      {
        "name": "RESEARCH_AGENT",
        "class_name": "ResearchAgent"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["ResearchAgent"]
    }
  ]
}

Si expones el contenedor a través de una ruta de Worker, el código de enrutamiento puede ser muy pequeño:

// src/worker.ts
import { Container, getContainer } from '@cloudflare/containers';

export class ResearchAgent extends Container {
  defaultPort = 8000;
  sleepAfter = '10m';
  envVars = {
    APP_ENV: 'production',
  };
}

export default {
  async fetch(request: Request, env: Env) {
    const url = new URL(request.url);

    if (url.pathname === '/api/research') {
      const body = await request.text();
      const container = getContainer(env.RESEARCH_AGENT, 'shared-research');

      return container.fetch('http://container.local/research', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body,
      });
    }

    return new Response('Not Found', { status: 404 });
  },
};

Hay patrones de enrutamiento más sofisticados, pero yo empezaría con algo simple:

  • una instancia compartida para uso ligero del equipo
  • un contenedor por tenant o flujo de trabajo solo si el aislamiento realmente importa

Estructura mínima de un servicio Agno / AgentOS

Dentro del contenedor, mantén la aplicación de Python sencilla.

# app.py
import os
from fastapi import FastAPI
from pydantic import BaseModel
from agno.agent import Agent
from agno.tools.duckduckgo import DuckDuckGoTools
from agno.db.postgres import PostgresDb

app = FastAPI()

db = PostgresDb(db_url=os.environ["AGNO_DB_URL"])

research_agent = Agent(
    name="Topic Researcher",
    db=db,
    tools=[DuckDuckGoTools()],
    instructions=[
        "Research the requested topic using current web results.",
        "Prefer primary sources when available.",
        "Return a blog-ready markdown article draft.",
        "Always include TL;DR, key sections, and a source list.",
        "Do not claim publication. This output is for human review.",
    ],
)


class ResearchRequest(BaseModel):
    topic: str
    audience: str = "Engineers and PMs"


@app.post("/research")
def research(request: ResearchRequest):
    result = research_agent.run(
        f"Research and draft an article about: {request.topic}. "
        f"Audience: {request.audience}."
    )
    return {
        "topic": request.topic,
        "markdown": result.content,
        "status": "draft",
    }

La aplicación de Astro no necesita saber nada sobre los detalles internos de Agno. Solo necesita un contrato HTTP estable:

  • enviar el tema
  • recibir el borrador en markdown
  • persistirlo en D1 como borrador

Ese es exactamente el tipo de límite que necesitas cuando una parte del sistema es determinista y la otra está dirigida por un modelo.

Dónde encajan Cloudflare Sandboxes

Cloudflare Sandboxes resuelve un problema diferente al de Containers.

Containers es el lugar adecuado para servicios de agentes estables y de larga duración:

  • APIs de Agno / AgentOS
  • servicios de automatización de navegadores
  • servicios personalizados de recuperación de información
  • workers de investigación programados o compartidos

Sandboxes es el lugar adecuado para la ejecución temporal y aislada de herramientas de IA:

  • ejecutar de forma segura Python o JavaScript generado por un modelo
  • analizar archivos cargados
  • generar gráficos o recursos derivados
  • ejecutar pasos de QA para artículos
  • exponer herramientas de revisión o vistas previas de corta duración

La propia documentación de Sandbox de Cloudflare presenta explícitamente Sandboxes como una opción para agentes de IA que necesitan ejecutar código de forma segura, y el producto está construido sobre la plataforma de Containers. Eso hace que la separación sea bastante natural:

Need a durable service contract and stable runtime?
  -> Container

Need isolated, per-user or per-task execution of code or tools?
  -> Sandbox

Containers vs. Sandboxes para herramientas de IA

Necesidad en la plataforma de blogsMejor opciónPor qué
API de investigación y generación de borradoresContainersRuntime estable de Python, dependencias predecibles y endpoint de servicio compartido
Ejecución de código específica de un usuario para análisis o limpiezaSandboxesUn sandbox por usuario o tarea, con sistema de archivos y espacio de procesos aislados
Notebooks temporales, generadores de gráficos y análisis de CSVSandboxesIntérprete de código, APIs de archivos y persistencia opcional respaldada por R2
Crawlers de larga duración o trabajos de ingesta programadosContainersMás adecuado para servicios compartidos en segundo plano y contratos de runtime conocidos
Microaplicaciones de revisión que se pueden compartirSandboxesexposePort() puede publicar URLs temporales de vista previa

Casos de uso reales de herramientas de IA para esta aplicación web

Estos son los casos de uso que consideraría seriamente en un producto real.

1. Auditoría de fuentes y citas

Después de que el agente de investigación de Agno genere un borrador, envía el markdown a un sandbox por usuario que extraiga enlaces, busque duplicados, valide reglas de formato y prepare una lista de comprobación para quien revise.

Por qué Sandbox es una buena opción:

  • la lógica de análisis no confiable o generada por un modelo puede ejecutarse de forma aislada
  • los archivos quedan limitados al editor o a la tarea actual
  • el resultado que recibe la aplicación web es JSON determinista
import { getSandbox, type Sandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

interface Env {
  Sandbox: DurableObjectNamespace<Sandbox>;
}

export default {
  async fetch(request: Request, env: Env) {
    const userId = await authenticate(request);
    if (!userId) {
      return new Response('Unauthorized', { status: 401 });
    }

    const { markdown } = await request.json();
    const sandbox = getSandbox(env.Sandbox, `editor-${userId}`, {
      sleepAfter: '15m',
    });
    const ctx = await sandbox.createCodeContext({ language: 'python' });

    await sandbox.writeFile('/workspace/article.md', markdown);

    const result = await sandbox.runCode(
      `
import json
import pathlib
import re

text = pathlib.Path("/workspace/article.md").read_text()
urls = sorted(set(re.findall(r'https?://[^\\s)]+', text)))

{
    "source_count": len(urls),
    "sources": urls[:25],
    "has_tldr": "## TL;DR" in text,
    "has_source_list": "## Source List" in text,
}
      `,
      { context: ctx },
    );

    return Response.json(result.results?.[0] ?? result.error);
  },
};

2. Herramienta para convertir datos en gráficos para los artículos

Los PMs y editores a menudo quieren que un agente convierta un CSV o una tabla extraída de la web en un gráfico simple que pueda insertarse en un artículo. Esta es una carga de trabajo clásica para un sandbox:

  • cargar datos
  • ejecutar Python con pandas y matplotlib
  • guardar la imagen generada
  • adjuntarla al borrador

Aquí también es donde la combinación de Sandboxes y R2 resulta potente. El sandbox puede montar un bucket de R2 para que los recursos generados sobrevivan a la destrucción del sandbox.

import { getSandbox, type Sandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

interface Env {
  Sandbox: DurableObjectNamespace<Sandbox>;
  ACCOUNT_ID: string;
  LOCAL_DEV?: string;
  R2_ACCESS_KEY_ID: string;
  R2_SECRET_ACCESS_KEY: string;
}

export default {
  async fetch(request: Request, env: Env) {
    const { articleId, csvText } = await request.json();
    const sandbox = getSandbox(env.Sandbox, `chart-${articleId}`);
    const ctx = await sandbox.createCodeContext({ language: 'python' });

    await sandbox.mountBucket('article-assets', '/data', {
      localBucket: Boolean(env.LOCAL_DEV),
      endpoint: `https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com`,
      credentials: {
        accessKeyId: env.R2_ACCESS_KEY_ID,
        secretAccessKey: env.R2_SECRET_ACCESS_KEY,
      },
    });

    await sandbox.writeFile('/workspace/input.csv', csvText);

    await sandbox.runCode(
      `
import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv("/workspace/input.csv")
ax = df.plot(x=df.columns[0], y=df.columns[1], kind="bar", legend=False)
ax.set_title("Auto-generated chart")
plt.tight_layout()
plt.savefig("/data/charts/article-${articleId}.png")
      `,
      { context: ctx },
    );

    return Response.json({
      status: 'stored',
      assetPath: `charts/article-${articleId}.png`,
    });
  },
};

En desarrollo local, Sandbox admite localBucket: true para montar buckets de R2. En producción, usa un endpoint y credenciales reales de R2.

3. QA de artículos antes de publicar

Antes de publicar, ejecuta una revisión de QA respaldada por un sandbox que compruebe:

  • estructura de markdown incorrecta
  • ausencia de una lista de fuentes
  • enlaces rotos
  • campos de frontmatter o SEO incompletos
  • formato de los bloques de código

Esta puede ser una herramienta muy pequeña relacionada con IA:

draft markdown
  -> sandbox QA command
  -> JSON report
  -> editor fixes issues
  -> publish

Me gusta especialmente Sandbox para esto porque nos permite evolucionar las herramientas de QA con el tiempo:

  • comenzar con expresiones regulares y parsers de markdown
  • agregar comprobación de enlaces
  • agregar linting asistido por modelos más adelante

sin dejar que esa complejidad se filtre a la aplicación editorial principal.

4. Aplicaciones temporales de revisión y exploradores de datos

Si los editores o las partes interesadas necesitan inspeccionar un recurso interactivo antes de que forme parte de un artículo, las URLs de vista previa de Sandbox son útiles. Puedes iniciar un pequeño servicio local dentro del sandbox, exponer un puerto y darles a quienes revisan una URL HTTPS temporal.

Esto es ideal para:

  • micrositios exclusivos para revisión
  • exploradores de datos interactivos
  • renderizadores temporales de markdown
  • depuración de resultados de agentes que son más fáciles de inspeccionar en un navegador que como JSON

La principal advertencia es operativa: las URLs de vista previa en producción necesitan un dominio personalizado con enrutamiento DNS mediante wildcard y son públicas de forma predeterminada, por lo que la autenticación a nivel de aplicación sigue siendo importante.

El mejor patrón combinado para esta aplicación

El patrón más sólido no consiste en «elegir Containers o Sandboxes». Consiste en usar ambos con funciones claramente diferenciadas:

Container layer
  -> runs the shared Agno / AgentOS research service
  -> owns stable dependencies and shared runtime behavior

Sandbox layer
  -> runs temporary AI tools safely
  -> isolates per-user or per-task execution
  -> generates derived files, QA reports, and review artifacts

Esto le da a la aplicación web un verdadero plano de herramientas de IA:

  • Container para capacidades compartidas del agente
  • Sandbox para ejecución aislada de herramientas
  • D1 para el estado editorial
  • R2 para recursos generados más grandes o resultados persistidos de los sandboxes

Consejos de implementación para el plano de herramientas de IA

Si construyes esto, mantendría las interfaces simples:

  • la aplicación web debe llamar a un conjunto pequeño de endpoints internos
  • cada endpoint debe devolver JSON tipado
  • cada herramienta debe tener un propósito acotado

Por ejemplo:

POST /api/research
  -> container-backed Agno draft generator

POST /api/tools/citation-audit
  -> sandbox-backed markdown/source audit

POST /api/tools/chart
  -> sandbox-backed CSV-to-chart generator

POST /api/tools/qa
  -> sandbox-backed pre-publish validation

Esta estructura hace que el producto sea más fácil de entender que un endpoint gigante donde «el agente puede hacer cualquier cosa».

Notas operativas sobre Sandboxes

Hay tres detalles de la plataforma que conviene tener en cuenta en el diseño:

  • El estado del sandbox también es efímero entre reinicios, a menos que persistas los resultados en otro lugar.
  • Una solicitud a un Worker que realiza muchas operaciones de sandbox puede alcanzar los límites de subsolicitudes del Worker, así que agrupa las operaciones en llamadas más amplias.
  • Los IDs de sandbox no son suficientes por sí solos para la autenticación. Usa autenticación a nivel de aplicación y, por lo general, aísla los sandboxes por usuario o tarea.

Si una herramienta hace muchas llamadas a writeFile(), readFile(), exec() y runCode() en una sola solicitud, cambia el transporte de Sandbox al modo WebSocket en lugar del modo HTTP predeterminado.

Cómo debería funcionar el flujo de borradores automáticos

No permitas que el agente publique directamente.

Este es el flujo que recomiendo:

1. Editor enters a topic
2. Astro calls /api/research
3. Worker proxies to AgentOS container
4. Agent researches the web and returns markdown + sources
5. Astro stores result in D1 as a draft article
6. Human reviews and edits in the markdown editor
7. Human clicks Publish

Esa única decisión elimina una gran cantidad de riesgo operativo:

  • menos artículos públicos con alucinaciones
  • menos problemas legales o errores de información accidentales
  • responsabilidades más claras
  • aprobación del producto más sencilla para los PMs

Flujo editorial de principio a fin sugerido

Este es el flujo de producto más simple que sigue sintiéndose moderno:

Human-authored path
  /admin/new
    -> write markdown
    -> save draft
    -> preview
    -> publish

Agent-assisted path
  /admin/generate
    -> enter topic
    -> generate draft
    -> review sources
    -> edit markdown
    -> publish

Esa simetría importa. Evita que el flujo del agente se convierta en un producto separado dentro del producto.

Notas de seguridad y confiabilidad

Hay cuatro riesgos que señalaría desde el principio.

1. La autenticación es obligatoria en las actions

Astro Actions son endpoints públicos por debajo. Trátalos como endpoints de servidor reales:

  • exige autenticación
  • exige autorización
  • agrega límites de tasa
  • registra la identidad de quien realiza los eventos de publicación

2. Sanitiza la salida de la vista previa de markdown

La vista previa renderiza contenido. Renderizar HTML controlado por el usuario sin sanitizarlo sigue siendo XSS, incluso cuando la fuente original era markdown.

3. Mantén los resultados de IA disponibles para revisión

Los borradores generados por agentes deberían almacenar:

  • tema del prompt
  • marca de tiempo de generación
  • lista de fuentes
  • indicador de generación automática

Eso les da contexto a quienes revisan, en lugar de un bloque de markdown de origen desconocido.

4. Trata los contenedores como cómputo, no como almacenamiento persistente

Vale la pena repetirlo porque es la principal trampa operativa:

  • los contenedores son excelentes para cómputo
  • la persistencia local del contenedor no forma parte de lo que el producto promete hoy

Un plan de entrega por fases

Si liderara esto como un proyecto de ingeniería y arquitectura, lo organizaría así:

Fase 1: base editorial

  • cambiar Astro a output: 'server'
  • agregar el adaptador de Cloudflare
  • agregar el binding de D1 y las migraciones
  • implementar autenticación y la estructura básica de administración
  • crear actions para crear, editar y publicar

Fase 2: experiencia de edición de markdown

  • agregar vista previa en vivo
  • agregar guardado automático o guardado manual de borradores
  • agregar un listado de artículos para borradores y contenido publicado
  • agregar validación de slugs y controles de seguridad para la publicación

Fase 3: generación de borradores asistida por agentes

  • agregar Cloudflare Container
  • desplegar AgentOS o una pequeña aplicación de FastAPI con Agno
  • agregar el flujo /admin/generate
  • persistir los borradores generados en D1

Fase 4: preparación para producción

  • migrar el estado del agente a Postgres si todavía está en SQLite
  • agregar colas para trabajos de investigación lentos
  • agregar políticas de reintentos y tiempos de espera
  • agregar registros y métricas de revisión de contenido

Esa secuencia aporta valor real a los usuarios desde temprano sin comprometerse demasiado pronto con la complejidad de la plataforma.

Mi recomendación

Si el objetivo es una aplicación editorial de blog moderna, con creación de contenido desde el navegador y redacción asistida por IA, recomendaría este stack con una regla de diseño explícita:

Astro + D1 own the publishing product.
Cloudflare Containers + Agno own the research compute.

Esto mantiene la aplicación fácil de entender.

También deja abiertas las opciones para el futuro:

  • puedes cambiar el framework del agente más adelante sin reescribir la aplicación de blog
  • puedes reemplazar D1 más adelante si el producto supera su capacidad
  • puedes agregar colas, generación programada o moderación sin romper el modelo de contenido

Lo más importante es que la arquitectura sigue siendo fácil de entender tanto para ingenieros como para PMs.

Lista de fuentes

Investigación verificada el April 2, 2026. Priorizé la documentación oficial de los productos.