

An AI shopping assistant that actually checks inventory

Ken Jones
Community Experience Engineer at Sanity

Jarod Reyes
Head of Developer Experience & Community at Sanity

Josh Hartman
Senior Solution Architect at Sanity
Published:


Email service providers (ESP) are great at sending email. But they quietly become a content management system (CMS) through the side door while nobody is looking.
Here’s a scenario to illustrate our point. Say, someone needs to send a product-promotion email. Someone in Marketing opens the email service provider (ESP), pastes product specs from Notion, grabs prose from a Google Doc, and then pastes disclaimer language from a brand guide nobody has opened in six months. They then hit send. This one product email now lives in four different places, drifting independently.
Then somebody on the team adds AI to the mix. The team buys an AI generation tool, points it at a campaign goal, and gets back an email draft that sounds like slop. The VIP segment and the lapsed-customer segment get the same copy. Marketing rewrites both by hand. AI may have sped some things up, but it didn’t solve the complicated part.
You can do better than that. The four problems below all go away when AI generation lives inside your content, with a promotion created for each audience and every prompt assembled from documents with a single source of truth.
Some of the most cutting-edge companies and dev teams are using Sanity for this exact use case. In this guide we’ll show you exactly how it’s done and give you the code to get up and running with a prototype you can show the team today.
The brief lives in Notion. The copy lives in Google Docs. The product details live in Sanity. The tone lives in a brand guide. The actual campaign lives in the ESP. Five sources of truth for one email.
Every approval cycle bounces between three tools and a Slack thread, and at the end of it somebody still types {firstname} into a subject line.
The fix: one structured campaign brief, referenced by everything downstream, including the AI.
Default LLM generation writes the same email for everybody, because the prompt knows nothing about your business or your customers. The LLM fills the gap by making things up. Coupon codes that do not exist. Product names that are not yours. Personalization tokens like {{vip_tier_2026}} that no customer record has ever carried.
But that context already exists, in your CMS. Your brand voice is a document with tone traits and prohibited words. Your campaign brief is a landing page with a primary message. Pulling those into a prompt at generation time is the difference between a bland draft and a draft you would actually send.
The fix: assemble the prompt from the documents that already describe your audience, your brand, and your campaign. Generate once per segment.
Now we’re getting to the power of building marketing workflows on top of structured content. Once you’ve sent the email the metrics start flowing in but they all end up in the ESP. Maybe you download a CSV and then build a chart in sheets?
The data exists. It is just on the wrong side of the wall, separated from the brief that produced it, the segment it targeted, and the tone traits that might explain why one variant outperformed another. Worse, none of it loops back to the AI. Next quarter's generation makes the same mistakes last quarter's generation made, because nothing taught it otherwise.
When engagement data writes back into the same document model that produced the email, the loop closes. The next time you generate copy for the lapsed segment, the agent can read "the last three subject lines that hit 30%+ open rates for this audience" as prompt context. Past performance becomes better prompts in the future which becomes better material.
The fix: engagement webhooks that write performance data back onto the structured promotion document, where the next generation can read it.
There is a reason "AI for email" usually disappoints. The model is only as smart as the context you feed it, and most SAAS setups feed it almost nothing.
Think about what a good editor knows when they sit down to write a campaign for the VIP segment:
A generic AI prompt gets none of that. A structured prompt, assembled at call time from context the editorial team owns, gets all of it. That is the difference between a draft you throw away and a draft worth refining.
This is also why AI inside a CMS beats AI sitting next to one. The structured inputs are right there. The structured outputs land where the rest of the pipeline expects them. The model sees the same content the humans see. Your structured data does the heavy lifting and AI makes it conversational.
Pull every problem above apart and the same architecture falls out.
The brief is a typed document, not a Notion page. The campaign carries goal, primary message, tone traits, target segments, and launch window as fields, not paragraphs. AI reads them. Downstream artifacts reference them. When the brief changes, every variant inherits the change.
Copy generation is per-segment and reads from real content. Brand voice is a document. Each segment is a document with editorial enrichment fields. The prompt is assembled at call time from those documents, and the AI runs once per target segment, not once per campaign.
Emails reference content, but don’t copy it. This is the quiet superpower. Your email links to the product document, not to a snapshot of the product. If a price changes Thursday morning, after the draft is approved the email still gets sent out with the new price. Nobody has to remember to update the email. Same goes for product images, promotional copy, disclaimers, and brand assets. Anything you would otherwise be tempted to inline. Reference it once, render it at send time. Your "stale email" failure mode quietly disappears.
The ESP is a delivery target, dumb on purpose. Approval workflow lives in your content system. So does the rendered email payload. The function that ships it to the ESP is the smallest possible adapter.
That is the principle. The starter described below is one implementation of it.
The email-marketing starter is one shape of the answer. Klaviyo is the reference ESP. The same shape works for SendGrid, Mailchimp, Customer.io, or Resend.
Two editable document types do the work:
campaign: The campaign holds the brief: primary message, value proposition, emotional goal, tone traits, target segments, and urgency stage. promotion: The promotion holds the segment-specific artifact, one per target segment. Inside each promotion is a field called emailSlots, an ordered array of composable blocks (emailHeader, emailSection, emailCTA, emailDivider, emailFooter) that gets rendered into HTML at send time.Three reference documents feed the AI:
brandVoice: Brand voice is a singleton with tone traits, prohibited words, and writing style rules. urgencyStage: Urgency stage tunes the campaign's pressure level. segment: Segments are two-layered. The top half is read-only and synced from Klaviyo (externalId, name, type, description, memberCount, isActive). The bottom half is editable (affinityDescription, typicalCopyTone, engagementTier). A separate klaviyoImport document tracks sync status (idle, requested, importing, imported, error), the last-imported timestamp, and counts. Useful for "did the sync run" diagnostics. Other ESPs slot into the same shape. Replace klaviyoImport with the provider equivalent, swap the synced fields on the segment document for whatever metadata that provider exposes, and the editorial enrichment layer stays put. Nothing downstream notices.
A workflow.state document tracks each promotion's lifecycle, with timestamped history entries for every transition.
A document action on the campaign loops over the campaign's referenced segments. For each one, it fetches the campaign brief, the brand voice, and the segment's enrichment, assembles a prompt, and calls Sanity Agent Actions (specifically, the generate action). The agent writes a typed promotion document. One run, one promotion per segment.
The target array lists the fields the agent is allowed to write. Output validates against your deployed schema, so generated content lands as typed Portable Text blocks, not a string blob. And instruction is assembled from real documents, so the prompt is never stale.
Here is what that produces in practice. A campaign targeting a VIP segment (typicalCopyTone: ["luxury", "exclusive", "invitation-only"], engagementTier: "vip") and a lapsed segment (typicalCopyTone: ["re-engagement", "incentive-driven"], engagementTier: "low") yields two genuinely different drafts. The VIP version opens with private-access language. The lapsed version opens with a discount and a softer "we miss you." Both follow the same brand voice rules. Both reference the same campaign brief. Neither needed a second prompt to get right.
Editors rarely accept a first draft. The refinement panel is where the actual work happens.
Open the "Refine with AI" inspector on a promotion and start talking to it. "Make the subject line more urgent." "Move the discount to the headline." "Now soften it for the lapsed audience." Each accepted suggestion patches the document live, so the email changes under the editor as they go.
The thread is keyed to the promotion ID, so the conversation survives reloads, context switches, and coming back to it the next morning. Hand the promotion off to a teammate and they see what was asked, what was tried, and what got accepted.
The agent writes the same way a human would. If the editor is working inside a content release, edits land in the release. There is no special "AI staging area." The agent uses the editor's permissions and the editor's perspective. If they cannot change the document, neither can the agent.
Code lives at studio/plugins/promotion/inspector/RefinementInspector.tsx and components/VariantRefinementPanel.tsx if you want to see how the inspector wires up to the content-agent client.
Promotions move through draft to ready-for-review to approved. A Sanity Function fires on the workflow transition, reads the document, builds HTML, and hands it to the ESP. That function is the boundary between the editorial team and the email scheduler:
The function takes a promotion ID. It produces "an email got sent." The middle is interchangeable.
A few things worth knowing about how dispatch behaves:
workflow.state document captures status (sent or error), timestamps for each transition, and the error message on failure. The function also writes the full ESP response body to the function logs when a send fails, so you do not have to guess which validation rule you tripped.pnpm bootstrap deploys the blueprint, deploys the schema, generates types, imports seed data, and prompts for an ESP API key. Studio runs at localhost:3333, frontend at localhost:3000. Hit "Generate Promotions" on the seeded campaign and watch the per-segment AI flow round-trip in under a minute.
To swap ESPs, point your AI coding agent at the repo and ask it to find every file that touches Klaviyo, then replace those calls with your ESP's SDK. The integration footprint is small (one dispatch function, one optional segment importer, one env var name), and most of the work is mechanical translation of the API shape. We are working on a Sanity agent skill that will assist with this directly. Until then, a well-prompted agent with the right context handles it cleanly.
The content model, the AI generation flow, the refinement panel, the preview, the approval workflow, the engagement data. None of that has to change.
This guide is part of AI Content Operations, a series on building AI-powered content workflows with Sanity.