I Built An A2UI System
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:
- Copy the post title
- Paste it into my admin dashboard
- Write a newsletter summary
- Copy the summary
- 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
- 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.
- Preact Integration: When the initial render failed with a
NoMatchingRenderererror, the AI diagnosed it instantly and rannpx astro add preact -yto fix it.
- Security Gating: The AI proactively added
!adminSecretto the button'sdisabledprop to prevent unauthorized approvals.
What I Had to Guide
- Event Listener Pattern: The AI initially passed
adminSecretas 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.
- 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.
- Workspace Boundaries: The AI couldn't access my separate
storychief-webhook-receiver-astro3project, so it provided instructions instead of directly modifying it.
Key Takeaways
For Developers Building Agentic Workflows
- Declarative > Imperative: Instead of having the agent generate HTML strings, use a JSON schema. It's safer, easier to validate, and more flexible.
- 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.
- Database as State Machine: The
broadcast_draftstable acts as a workflow state machine (pending→approved→sent). This makes the system auditable and recoverable.
- 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
- 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.
- Iterate on Errors: The
NoMatchingRenderererror led to a 5-minute detour, but the AI fixed it autonomously. Trust the process.
- Review the Plan: The AI created an
implementation_plan.mdartifact 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:
- Publish in StoryChief
- Agent drafts newsletter
- I click "Approve & Send"
- 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.