All writing

Strivelabs engineering · Generative UI

Building live reports inside Strive: the story of Strive Studio

How a chat that could already share insights grew a studio for live reports. The backstory, the naming call that let both live side by side, the bet on letting the agent write HTML, and everything that broke once people started using it.

Published
Stack
React 19 · TypeScript · sandboxed iframes
Read
~10 minutes

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.

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.

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.

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 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 doesconst 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 has its own post.

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.

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.
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.

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

  1. 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

  1. 26 Apr 2026

    HTML in a sandbox

    First HTML report rendered in a sandboxed iframe, talking to the host over a postMessage bridge

  2. 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

  3. Mid May 2026

    Report Studio

    A dedicated chat with the report builder skill, design-system context, then Tailwind-based theme tokens

  4. Late May 2026

    First users

    Rolled out behind a flag to a selected set of users

One chat · the report builder as a skill

  1. 11 Jun 2026

    Folded into the main chat

    Studio page and its agent retired; the report opens as a canvas slot beside the thread

  2. Early Jul 2026

    Hardening

    White-label-safe report links, live tool calls with a read-only guard, rename and delete

  3. Late Jul 2026

    PDF export

    Self-snapshotting reports, page-by-page export, waiting for data before snapshotting

  4. 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

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.

Back to the shelf