This is a submission for the Sanity Challenge, Path Two: Vibe-Code Something Strange
What I Built
California Black Stories is an editorial app for preparing fact-card posts for a California Black history Facebook page. It takes a topic, drafts one fact, checks it against source excerpts, writes a short caption, and renders a downloadable card. The post then waits for a person to review it.
I run the California Black Stories Facebook page because I’m interested in the history of my home state. Much of the content currently on the page is AI-generated. My goal is to grow the page and qualify for Facebook monetization, and I wanted a repeatable way to prepare posts while keeping the source and my review decision attached to each one.
The core feature is a four-stage Sanity workflow:
generating → inReview → approved → published
↓
generating
(reject with feedback)
I can approve a card or reject it with a note. A rejection updates the existing post using that feedback and sends it back to review. I can then approve it or request another revision. The generator cannot approve its own work.
The Next.js post library shows the card image, caption, citation, source link, and workflow status. Source notes render as formatted Markdown, with longer notes available in an expandable section beside the image. It includes drafts, supports search and status filters, and lets me copy a caption or download the original PNG.
Demo
- Post library: fact-card-workflow.nikema.dev
- Workflow walkthrough: https://youtu.be/Sa18wrSdgQQ
The claim audit shows supported, unsupported, and contradicted findings, with linked sources and options to recheck or record a reviewer override.
The post waits for human approval, with an option to reject it and request a revision.
How to test: In the post library, search for a topic, filter by workflow status, open a post, copy its caption, and download its image. The library includes works in progress, so these are editorial drafts rather than a feed of approved historical claims.
Generation, approval, rejection, and publication take place in Studio and require an authorized Sanity account. The linked walkthrough demonstrates that authenticated workflow, including generation, rejection with feedback, replacement generation, approval, and publication. Judges can use the live post library directly; no Studio test account is supplied.
After approving a draft in Studio, I click Publish. Sanity publishes the document, and a background Function completes the same workflow and updates its status. No separate “mark published” click is needed. This demo publishes to Sanity; it does not post to Facebook.
Code
Fact Card Workflow
One Git repository contains both apps and the automatic workflow runtime:
-
studio/: Sanity Studio, post schema, and editorial workflow. -
web/: Next.js app. -
functions/: automatic workflow status synchronization and rejected-card regeneration. -
shared/: generation and template rendering used by the web app and Functions. -
sanity.blueprint.ts: Sanity Function and service-token deployment.
Commit and push from this repository root. The app folders are ordinary folders
not separate repositories or submodules. Their earlier Git histories are preserved
in the ignored .git-backups/ directory.
Install and run
Run npm install at the root, npm install --prefix studio, and
npm install --prefix web. Run npm run studio:dev and npm run web:dev in
separate terminals.
Automatic status synchronization
The deployed fact-card-status-sync Sanity Function processes status effects
when a workflow queues new work. It updates the draft post's status to match its
current workflow stage. It does not approve cards…
The repository contains the Studio, Next.js frontend, shared generation and rendering code, and the Sanity Function runtime. The README explains how to run them and configure the integrations.
The verification and rendering approach builds on my earlier CBS Post Generator. I adapted it into an editorial process with structured content, stored assets, and review history.
My Build Process
I built this with the Codex desktop app. I started with a specific schema and workflow request, then changed the app through small prompts as I tested the result.
Start with the process and the schema
My initial request specified one post document with a topic, fact text, a nested citation and source URL, a Facebook caption, a template ID, an uploaded image, and a status. It also specified the four workflow stages and a rejection path back to generation.
Keeping these as separate fields matters. The source remains available to the reviewer even though it is not printed on the card. The image is a stored Sanity asset, and the template ID records how it was rendered. The workflow is stored alongside the content rather than being an informal agreement about what a status label means.
The definition lives in studio/workflows/postWorkflow.ts, and the schema uses Sanity's defineType and defineField APIs. I removed an earlier generic factCard schema once the new post model powered the app.
A deployed definition is not a running pipeline
The first workflow deployment warned that no generated runtime was present. That was a useful distinction: deploying the workflow described the process, but something still had to execute the queued effects.
I initially had a manual drain command. I asked, “can we handle it automatically instead,” and the app gained a Sanity Function that responds to newly queued effects. It synchronizes the post's status and performs replacement generation after rejection. Normal transitions no longer depend on a terminal command or an open browser.
Move generation into the app
Next I asked, “I want to generate the posts within the app.” We added a custom Generate post tool in Studio, backed by a server endpoint in the web app.
When choosing topics became difficult, I asked to bring in the topic-generation feature from CBS Post Generator. Studio now suggests five specific claim ideas from broad categories. I can choose one and send that exact idea to source checking. The suggestions are explicitly unverified. I restored the original generator’s idea prompt so suggestions retain its range of historical stories. A source-checking retry now keeps the same claim while requesting more sources and longer source text; it does not turn an unsupported claim into a post.
OpenAI produces a candidate claim when I enter my own topic. Verify with Exa retrieves source excerpts and runs the original generator’s audit prompt. The tool shows each finding as supported, unsupported, or contradicted, alongside the linked Exa sources and excerpt previews. I can expand the excerpts to read their formatted text.
If part of a claim isn’t supported or conflicts with the sources, the app suggests a correction while keeping the supported details. Apply suggested correction uses the findings already available, without another search or audit. If I edit that suggestion, the edited wording needs verification. Possible contradictions with earlier approved or published posts appear as review warnings.
Once I’m satisfied with the selected fact, Create card & submit for review renders the image and saves a draft with its citation, source URL, and caption ending in a question. Verification and card creation are separate steps, giving me time to inspect the evidence first.
That screening step can still miss historical nuance. The human review gate is part of the design, not an optional cleanup step.
When the audit gets it wrong
The claim audit can make mistakes too. In one example, it marked a claim about the first Los Angeles CORE chapter as contradicted because a source mentioned a later chapter. A later chapter doesn’t contradict the existence of the first one.
I added two ways to handle this. I can recheck an individual finding against the same saved source excerpts, with an explanation of why I disagree. Or I can keep the original claim by recording a reviewer override with a written explanation and a supporting source.
The app preserves the original findings and records the recheck or override in the audit history. An override allows the card to be created and submitted for review; the post still needs approval before publication.
This helped me see that an AI-assisted editorial workflow needs room to question the AI’s conclusions as well as the content it generates.
Refine the actual card, not just the prompt
I noticed that the card template printed the source text at the bottom and asked to remove it. We kept the citation in Sanity and on the detail page, while simplifying the image to the fact and the California Black Stories branding. New citations contain source titles and URLs rather than entire Exa excerpts; the fuller excerpts remain available during fact-checking. Studio also provides a formatted citation preview.
The images are template-rendered, not AI-generated artwork. Satori lays out the text, and WebAssembly Resvg produces a 1080 × 1080 PNG. A native renderer caused deployment trouble, so we moved to the WebAssembly version and shared that renderer between initial generation and replacements.
Make rejection useful
A reject button alone leaves me with another task. I asked how to generate a replacement automatically, and we connected rejection feedback to the generation process.
The background job uses the revision note to decide what needs changing. Caption-only feedback preserves the fact, source, and image. Image-only feedback rerenders the existing fact. Factual or source changes go through verification. I also fixed feedback such as “keep the fact and source unchanged” so it does not accidentally trigger a factual rewrite.
Replacement saves tolerate background status updates while still protecting edits a reviewer makes during generation. An actual generation failure leaves an error and a retry action. Existing workflow instances retain their definition snapshots and review history.
The current Workflows UI can briefly show “An automated step didn’t finish” while the background job is still running. In the demonstrated flow, that warning clears when the revised post returns to review. I acknowledge the temporary warning in the walkthrough rather than presenting it as a failed regeneration.
Finish the handoff
The frontend came from another practical request: “can we build a frontend that shows the post data and allows downloading the post image.” I chose to include drafts so it works as a working library. The implementation uses server-side GROQ reads with a Viewer token; the token stays out of browser code.
Finally, I wanted Studio's Publish button to complete the workflow automatically. The button checks the current workflow's approval, and a background Function advances the approved workflow after a published document exists.
The latest local checks passed 56 tests, Studio and web builds, and TypeScript checks. Tests exercise approval restrictions, rejection and retries, caption-only revisions, concurrent status updates, formatted source rendering, card rendering, and publication synchronization. A production-build endpoint check also confirms that accepting a suggested correction reuses its saved evidence without another provider call. I also checked search, filtering, caption copying, mobile layouts, and an actual 1080 × 1080 image download in the browser. The deployed library also loaded successfully, and search, status filtering, caption copying, and image download worked there.
What I would improve next
The web library is a server-rendered interface with refresh-based updates; it is not a custom real-time App SDK app. Workflows are the Sanity feature I explored deeply for this submission.
Interrupted effect jobs currently have a manual recovery path rather than a scheduled sweep. Content Lake write permissions also remain the boundary for direct API edits; the workflow and Studio button coordinate the intended editorial process.
The demo is deliberately limited to fact cards. Production uses Buffer to publish to Facebook and Instagram, but there is no Buffer integration or reels workflow in this entry.
My biggest learning was that a content workflow needs more than a status dropdown. Sanity Workflows let me model who can move a post forward, what happens after rejection, and where human approval belongs. I also learned that deploying a workflow definition does not automatically run its effects; that needs a runtime.
Customizing Studio showed me that I could put the generation tool inside the same place where I review the content. I didn’t need to keep switching between a generator and a CMS. Prompting helped me build the pieces, but testing the actual generation, rejection, approval, and publication loop is what made the app useful.
Sanity Project Details
-
Project ID:
ta2gi825 -
Dataset:
production -
Document type:
post -
Workflow:
post-workflow, deployment tagproduction -
Runtime: Sanity Function
fact-card-status-sync
Agent Session
I used Codex throughout the build. The repository includes selected prompt notes explaining the requests and course corrections behind the app.


Top comments (0)