Strive Studio is where you build marketing reports in Strive. Describe what you want in chat, and you get back a live dashboard wired to your own ad, analytics and CRM data, right beside the conversation.

Think Claude artifacts, but the numbers never go stale.

A little bit of backstory

We already had a stable generative UI chat, Strive AI, built from the ground up. It could already put together a good first version of insights, straight from a conversation.

But we soon hit real bottlenecks, and the need for a far more flexible builder came much sooner than we had planned.

It started as a simple, slightly wild idea from our CTO:

What if we just let the agent write the code, instead of struggling with schemas and teaching it to make sense of them?

v1: insights, curated from chat

The first version lived inside the chat. You ran an analysis, the insights came back in pieces across the thread, and you kept the pieces that mattered.

v1: the blocks you pick from a chat, compiled into an insight doc you can re-run on a schedule.

Under the hood, an insight doc is just a set of blocks, handpicked by you or suggested by the agent, compiled into a structured page with AI summaries alongside the metrics.

It still works today, as /share-insights:

/share-insights today. Pick what mattered in a chat, and it compiles into an insight doc you can send or download.

Where v1 ran out

An insight doc is a snapshot. Reports asked for a lot more: large volumes of data, and numbers that stay live after you share them.

  • The registry didn’t scale to every layout. Owning it helped in many ways, but each new layout or variant meant one more UI tool, and more context for the agent to carry.
  • Some reports can’t be written as JSON. Multi-dimensional, multi-page reports need schemas that no model follows reliably.
  • Strict patterns drained the story out. Every time we boxed the UI into a fixed pattern, reports lost their appeal. A report should tell its story in one quick scan.

Two ways to share, one product

This was one of the more interesting problems for me as a design engineer. We were about to have two ways to make something shareable, and people needed to pick the right one without thinking about it.

We couldn’t simply drop v1. Insight docs are snapshots with AI summaries you can read in a minute, and schedulable templates that bring the same outline back every week. They’re powerful, and a few customers were already happy with them. Reports needed a proper playground.

So the first job was the words.

Each named for what it is. Insights for a moment worth keeping; reports for numbers that keep moving.
  • Insights became insight blocks, behind a skill of their own, /share-insights, which guides you to curate what’s worth keeping from a chat.
  • Reports got a studio. I wanted it to feel like a hand-off from chat into a dedicated playground. It isn’t a separate environment, though: the slot becomes the studio.
  • Reports got a home. A Reports page lists them, each one linked to its chat. Open a report there and jump into the studio to change it, or go the other way.

I’ll admit there’s still some overlap. Building at a startup is always cost against impact, so we let people use both, and we’ll unify them once we’ve seen how they do.

Why we let the agent write HTML

For reports, we stopped enforcing schemas altogether. The agent gets taste and brand context instead, and you decide what goes in the report and how it’s laid out.

Our CTO had been saying it from the start: leave the agent with what it already knows, and what’s already all over the internet. We’d made the same call before, choosing charting libraries, graph tools and diagram formats.

  • Models are extremely good at HTML.
  • No framework overhead. No bundling, nothing to compile. Minimal, and a good fit for marketing dashboards.
  • It’s a standard people know. If humans start editing alongside the agent one day, there’s very little to learn.

We’re not against frameworks. This one just works for us.

Controlled output and agent-written UI, side by side. The registry keeps the chat consistent; the agent gets the surfaces a schema would only get in the way of.

For the frontend, the job changed completely. Instead of rendering our components, we now had to host someone else’s page, safely, and still make it feel like part of the product.

The one-liner

The agent writes one HTML file. We host it in a sandbox, lend it live data through a narrow bridge, and give it enough design context to look like it belongs.

One HTML file in the middle. Chat writes it, a sandbox runs it, the host feeds it.

Hosting a page we didn’t write

Reports render in <iframe sandbox="allow-scripts">, deliberately without allow-same-origin. That one missing token does most of the work: the report runs its own scripts and charts, but it can’t read our cookies, storage or DOM, and it can’t call our API.

So when it needs data, it has to ask.

The report asks, the host answers. Credentials, the session and every check stay on the host side of the frame.
Three checks before acting on a message
window.addEventListener("message", (event) => {
  if (event.source !== frame.contentWindow) return;  // 1 · this frame
  if (event.origin !== "null") return;               // 2 · a sandboxed origin
  if (!isReportRequest(event.data)) return;          // 3 · a known shape
  handle(event.data);                                // anything else: ignored
});
  • The host picks the credential, never the iframe.
  • Every app action from a report is read-only, on both client and server. Reports run unattended whenever someone opens them, so they should never write.
  • A small SDK wraps it all. report.dataset.query(sql), report.app.execute(…), report.export(). It’s a plain script, not a bundle, so a fix reaches every report without regenerating any HTML.

The report can ask for anything. It can’t hold anything.

What broke at scale

Agent-written HTML was an instant hit. Then came the scale problems, and they taught us the most.

Design context: from a stylesheet to a little taste

At first I wrote the whole CSS context myself: 650 lines of classes matching our design system, with the skill trained on top. It became unmaintainable very quickly, and honestly it went against the whole point of moving away from fixed schemas.

What the agent really needed was a small amount of taste and a few guidelines; it can work out the rest. Tailwind is a standard models already know well, so now the agent writes Tailwind classes straight into the HTML, instead of picking from a set we defined.

The shell, the tokens and the taste each live where they are hardest to get wrong: in the client, in code, and in the skill.
The theme, roughly
tailwind.config = {
  theme: { extend: {
    colors: {
      brand: { DEFAULT: "#F97316", dark: "#EA580C", soft: "#FFF7ED" },
    },
    fontFamily: { sans: ["Inter", "system-ui", "sans-serif"] },
  } },
};
 
// charts read the same token the markup does
const brand = getComputedStyle(document.documentElement)
  .getPropertyValue("--brand");

The taste lives in the skill: restraint over decoration, brand orange in four places and no more, numbers that read like numbers, and one layout archetype per report.

Four archetypes in the skill. The agent picks the shape before it writes a line, which is most of what keeps a report from looking generated.

And a few emphasis recipes, for the most common tell of a generated dashboard.

“A row of four identical cards is the number one AI-dashboard tell,” straight from the skill. So it ships recipes for emphasis instead.

Huge volumes of data

Loading everything on page load didn’t scale. On big accounts the browser stalled, and heavy pages broke outright.

Live should never mean heavy.

Less AI slop

Reports shouldn’t look vibe-coded. They should carry the customer’s brand, and be easy to customise.

So during onboarding we extract a brand’s design-system context and keep it in our knowledge base, where it can be edited. Several workflows plug into it, and reports are one of them. That part deserves its own post, coming soon.

Multi-page exports

PDF export worked fine, until reports became interactive and multi-page. The host can’t read a sandboxed page, so only the report can snapshot itself, and then each page has to be drawn on its own.

Only the report can see its own page, so export is a request, not a screenshot. The host keeps the limits and the action.
  • Page by page. Each page loads separately in Playwright, gets snapshotted, and the snapshots are stitched into one PDF.
  • A strict 5 MB limit. It all runs on a Lambda, and we learned the hard way that it needs a firm cap.
  • It waits for the data before snapshotting, and the host fixes the action; the iframe only supplies HTML.

From a studio of its own to a skill

The first Report Studio was a separate chat, on purpose. It kept the report rules out of the main chat’s prompt, and let us experiment without touching the chat everyone uses daily. Here’s how we sketched it.

Report Studio on a whiteboard, before we built it. Chat routes into the studio, code while it builds, a preview once it’s ready, and a history of report chats only.

The trade-off we were happy to live with for a while: a separate agent meant a separate history. We always knew it was meant to be a skill, at least from the user’s side, so once it was stable we folded it in. One chat, one history.

Build apart, then fold in. The standalone studio was a step we took on purpose, and it was always meant to become a skill.
  • The report became a canvas slot, in the same floating panel and navigator rail the chat already had. No new chrome.
  • The page drives it, not the agent. It opens as soon as a thread has a report, even while history is still loading.
  • The chat stayed generic. No report imports, just a canvas slot and an awaited pre-send step. That’s what made folding it in cheap.
Four doors, one room. Whichever way you come in, the thread and the report are bound before the agent reads a thing.

The small things that make it feel native

A latch, not a loading state: once the canvas has shown a report, it never goes back to the splash.
  • No flicker on edits. A staged splash on first load only; after that, the page swaps in quietly.
  • Honest empty states. “Report not found” only after the query resolves, and a failed tool turn keeps the previous version.
  • A workspace, not a new chat. “Let’s get cooking!”, starter prompts that fill the composer instead of sending, titles taken from your prompt.
  • “Ask about this” inside a report opens a chat that already knows which report it came from.

What I’d keep

  1. Don’t throw away what works. Insights stayed. Clear names made room for both.
  2. Let the model use what it already knows. HTML and Tailwind beat any vocabulary we could invent.
  3. Treat agent HTML as untrusted. Opaque origin, host-held credentials, read-only actions, three checks on every message.
  4. Live should never mean heavy. Load what’s on screen, and fetch the rest when it’s asked for.
  5. Build apart, then fold in. The standalone studio was how we learned fast without breaking the main chat.

Six months on one timeline

v1 · insights curated from chat
Feb to Mar 2026
Insight docs
Blocks picked from a chat, compiled into a shareable, schedulable insight document. Still in the product as /share-insights
Agent-written HTML · a studio of its own
26 Apr 2026
HTML in a sandbox
First HTML report rendered in a sandboxed iframe, talking to the host over a postMessage bridge
Late Apr 2026
The SDK and the way back to chat
Report SDK split out of the first report, app actions from reports, “ask about this” handing off to chat
Mid May 2026
Report Studio
A dedicated chat with the report builder skill, design-system context, then Tailwind-based theme tokens
Late May 2026
First users
Rolled out behind a flag to a selected set of users
One chat · the report builder as a skill
11 Jun 2026
Folded into the main chat
Studio page and its agent retired; the report opens as a canvas slot beside the thread
Early Jul 2026
Hardening
White-label-safe report links, live tool calls with a read-only guard, rename and delete
Late Jul 2026
PDF export
Self-snapshotting reports, page-by-page export, waiting for data before snapshotting
Aug to Sep 2026
The pattern spreads
Brand context from onboarding, and chat artifacts generalising “the agent writes HTML, a sandbox runs it”
From insights curated out of chat, to live HTML reports in a studio of their own, then into the main chat as a skill.

What’s next

  • Public sharable links.
  • Streaming HTML into the canvas while the agent edits.
  • Revision history and diffs.
  • Editing together on one report.
  • One way to share. Bringing insights and reports closer, once we’ve learned enough from both.

The report canvas also became our first page-driven slot, and the same shape now powers briefing panels, routine previews, task details and chat artifacts. Reports were the first place we let the agent write the interface itself. They won’t be the last.

Thanks

Strive Studio is a team effort, from the data and sandboxes underneath to the agent, the skill and the checks that keep every number honest. Thanks to the whole team at Strivelabs for building it together.