I Built An A2UI System

• By Rich Martinez

Building an AI Agent That Writes Its Own UI.

The Problem

I had a workflow problem that was driving me crazy. Every time I published a blog post on StoryChief, I'd have to:

  1. Copy the post title
  2. Paste it into my admin dashboard
  3. Write a newsletter summary
  4. Copy the summary
  5. Send the broadcast

It was tedious, manual, and honestly, a waste of time. I wanted a "Zero-Touch" system where the AI would draft the newsletter for me and present it in a beautiful approval card on my dashboard.

But here's the twist: I didn't just want the AI to generate text. I wanted it to generate a UI component that I could click to approve and send.

The Solution

I built an Agent-to-UI (A2UI) system. Here's how it works:

1. The Newsletter Agent

When a new blog post is published, a Gemini 2.0 agent analyzes the content and generates:

  • A compelling subject line
  • A brief summary
  • A JSON payload describing a UI card
// src/lib/agent/newsletter-agent.ts
export class NewsletterAgent {
  async generateDraft(postSlug: string): Promise<BroadcastDraft> {
    const post = await getR2BlogPost(postSlug, this.env);
    
    const prompt = `Analyze this blog post and create a newsletter draft.
    
    Title: ${post.title}
    Content: ${post.content}
    
    Generate:
    1. A compelling subject line (max 60 chars)
    2. A 2-3 sentence summary
    3. An A2UI JSON payload for a review card
    
    Return JSON only.`;
    
    const result = await this.model.generateContent(prompt);
    return JSON.parse(result.response.text());
  }
}

2. The A2UI Schema

The agent doesn't just return text—it returns a declarative UI specification:

// src/lib/agent/a2ui-schema.ts
export interface A2UIPayload {
  type: 'container';
  components: A2UIComponent[];
}

export interface A2UIComponent {
  type: 'card' | 'text' | 'button';
  props: {
    title?: string;
    content?: string;
    label?: string;
    action?: string;
  };
}

3. The Preact Renderer

On the admin dashboard, a Preact component renders this JSON into an interactive card:

// src/components/A2UIRenderer.tsx
export default function A2UIRenderer({ draft }: { draft: Draft }) {
  const payload: A2UIPayload = JSON.parse(draft.a2ui_payload);
  
  const handleApprove = async () => {
    await fetch('/api/admin/approve', {
      method: 'POST',
      body: JSON.stringify({ draftId: draft.id })
    });
  };
  
  return (
    <div class="card">
      {payload.components.map(comp => (
        <UIComponent component={comp} />
      ))}
      <button onClick={handleApprove}>Approve & Send</button>
    </div>
  );
}

4. The Database Layer

I added a new broadcast_drafts table to Cloudflare D1:

CREATE TABLE broadcast_drafts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  post_slug TEXT NOT NULL,
  subject_line TEXT NOT NULL,
  summary TEXT NOT NULL,
  a2ui_payload TEXT NOT NULL, -- JSON
  status TEXT DEFAULT 'pending',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

The AI Angle

This project was a masterclass in AI-driven development. Here's what I learned:

What the AI Got Right

  1. Architecture Design: The AI suggested the A2UI pattern immediately. I described the problem ("I want the agent to generate UI"), and it proposed a JSON schema approach.
  2. Preact Integration: When the initial render failed with a NoMatchingRenderer error, the AI diagnosed it instantly and ran npx astro add preact -y to fix it.
  3. Security Gating: The AI proactively added !adminSecret to the button's disabled prop to prevent unauthorized approvals.

What I Had to Guide

  1. Event Listener Pattern: The AI initially passed adminSecret as a prop, but I wanted it to dynamically update when the user typed in the input field. I had to guide it toward a custom event (admin-secret-changed) that the Preact component listens to.
  2. D1 API Quirks: The AI used .run() and .results[0], but Cloudflare D1's newer API uses .first(). I had to correct this based on the error messages.
  3. Workspace Boundaries: The AI couldn't access my separate storychief-webhook-receiver-astro3 project, so it provided instructions instead of directly modifying it.

Key Takeaways

For Developers Building Agentic Workflows

  1. Declarative > Imperative: Instead of having the agent generate HTML strings, use a JSON schema. It's safer, easier to validate, and more flexible.
  2. State Management Matters: If your UI needs to react to user input, use a framework like Preact or React. Don't try to do it with vanilla JS in Astro's server-side context.
  3. Database as State Machine: The broadcast_drafts table acts as a workflow state machine (pendingapprovedsent). This makes the system auditable and recoverable.
  4. Fallback is Critical: The manual broadcast form is still there. If the agent fails, I can always send newsletters the old way.

For AI Pair Programmers

  1. Be Specific About Context: When I said "the button should be disabled until the secret is entered," the AI understood immediately. Vague prompts lead to vague code.
  2. Iterate on Errors: The NoMatchingRenderer error led to a 5-minute detour, but the AI fixed it autonomously. Trust the process.
  3. Review the Plan: The AI created an implementation_plan.md artifact before writing any code. I approved it, and the execution was smooth.

What's Next?

I still need to integrate this with the StoryChief webhook receiver. Once that's done, the entire flow will be:

  1. Publish in StoryChief
  2. Agent drafts newsletter
  3. I click "Approve & Send"
  4. Subscribers get the email

Zero manual typing. Zero copy-paste. Just pure, automated bliss.


Tech Stack: Astro, Cloudflare Workers, D1, Preact, Gemini 2.0, Resend
Lines of Code: ~500
Time to Build: 2 hours (with AI assistance)
Time Saved Per Newsletter: ~15 minutes

If you're building agentic systems, I highly recommend the A2UI pattern. It's the future of human-AI collaboration.