cd /news/ai-agents/i-built-a-ministry-that-loses-your-f… · home › topics › ai-agents › article
[ARTICLE · art-145065] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

I built a ministry that loses your form on purpose

A developer built the Ministry of Approvals, a satirical web app that routes user submissions through three AI clerk departments before a human minister rules on them, using Sanity Workflows as the underlying engine. The project stores the workflow definition in the same dataset as the forms, with AI clerks implemented as Agent Actions whose personalities are content-editable, and a back office built with the App SDK inside the Sanity Dashboard. The developer found that Sanity Workflows is early access and does not run AI steps automatically, requiring a Blueprint with a robot token and a Document Function to drain unclaimed effects, with the first full pass taking about 20 seconds.

by read11 min views1 publishedOct 4, 2026

This is a submission for the Sanity Challenge, Path Two: Vibe-Code Something Strange The Ministry of Approvals. You file anything (a meme, a name for your cat, a plan, a complaint about the plan) and it walks a corridor of three departments staffed by AI clerks with opinions, lands on a human Minister's desk, and if it survives it goes on a public wall as a certificate with stamps on it.

The Department of Pedantry finds the one thing you forgot and cites a regulation it made up on the spot. The Department of Rubber Stamps approves everything and says something that proves it did not read your form. The Archive loses every third form and hands you a receipt explaining where it probably went. Then the Minister (me, allegedly a human) has one hour to rule. If I sit on it, the Department of Delays sends it back to the Archive, which loses it again. 💀

I run communities for a living and I have spent years waiting for someone with a stamp. This is that feeling, as a website, with the stamp physics done properly. It is cool and also slightly cursed 🫡

Under the paint: the whole corridor is one Sanity Workflows definition that lives in the same dataset as the forms. Every step you see on the site is the workflow being read back. The clerks are Agent Actions with personalities stored as content, so you can slide a temperament from "rubber stamp" to "stickler" in Studio and the prompt rewrites itself. Their portraits were painted by Agent Actions too, from their own job descriptions. The Minister rules from a back office built with the App SDK inside the Sanity Dashboard.

Live: [https://ministry-of-approvals.vercel.app](https://ministry-of-approvals.vercel.app)

Studio: [https://ministry-of-approvals.sanity.studio](https://ministry-of-approvals.sanity.studio) (login walled, so the Studio screenshots below are what it looks like inside)

A form gets filed, Pedantry returns it, I fix it, it walks Rubber Stamps and the Archive, the Archive loses it, it does the whole lap again and lands on the Minister's desk with a deadline. No cuts:

Then the ruling, from the back office in the Dashboard. I take the file, approve it with a note, and the audit trail fills up live: approve, issue, certificate, Discord announce, framed, on the wall. About 15 seconds. 🔥

The certificate is a real PNG and the Ministry posts it to Discord the second it is issued:

Some stills, because the GIF is big:

If you file a form yourself: the Archive loses serials divisible by three. Check the counter on the Front Desk and pick your moment 👀

[https://github.com/A1VARA5/ministry-of-approvals](https://github.com/A1VARA5/ministry-of-approvals) (MIT)

Four folders. studio is Studio 6.9 with the Workflows plugin, a temperament slider, a stage badge and a portrait action. workflows is the definition, the clerk handlers, the Sanity Functions runtime and 14 bench tests. app is the App SDK back office. web is the Astro 7 site with the certificate renderer.

Everything below is what I decided and what broke, in the order it happened. The dated log is in the repo.

The first thing I got wrong was in my head before I wrote a line. I assumed a workflow with an AI step would just run. It does not. Sanity Workflows is early access (0.33.0 landed the day the challenge opened) and the engine is a library. Nothing moves on its own. An AI clerk is an effect that some runtime you own has to pick up and drain. Once that clicked, the whole architecture fell out of it: a Blueprint with a robot token and a Document Function that wakes up whenever a run gains an unclaimed effect and drains it. One drain walks a form through every automated department. The first full pass took about 20 seconds from resubmit to the Minister's desk, and I sat there watching the status page like it was a football match.

Five document types. submission is what the citizen wrote and nothing else. There is no status field on it. Where a form is lives in the workflow instance document next to it, and the public status page is one GROQ query on that instance. department is a stage described as content: name, motto, clerk, stamp colour. Renaming the Department of Pedantry is an edit, not a redeploy. clerk is an AI personality stored as content: a system prompt, a temperament number, a catchphrase, a portrait. certificate is only ever written by an effect handler and copies the clerks' remarks in as stamps, so the wall survives even if someone deletes the run. receipt is keyed on the effect key, so a retried handler cannot issue two receipts for one loss. I learned that one the boring way.

Seven stages. Each department is an activity with a trigger that queues the clerk effect, a second trigger that marks it done when the effect settles, and a third that marks it failed. Transitions branch on typed effect outputs: Pedantry returns complete: boolean, the Archive returns lost: boolean. The Minister's desk is the human stage: approve with a note, reject with a reason, refer to a department. All of those are actions with params, and the site, the back office, the CLI and the Studio plugin fire the same actions, so there is one audit trail whoever pressed the button.

The bit I am most pleased with is the Department of Delays. The Minister's desk has a stage scoped deadline field seeded by a GROQ query value (string(dateTime(now()) + 3600)) and a trigger with when: '$fields.deadline <= $now'. When a tick finds the deadline gone, the file goes to the Archive flagged overdue, and the Archivist loses it every time and says so on the receipt. A Scheduled Function ticks every open run once an hour (that is the fastest my plan allows), and the status page ticks a file while someone is looking at it, so nobody has to wait for the clock in a demo.

I proved it on myself while writing this. Serial 6 ("Declare the office plant a senior colleague") reached my desk at 13:41:51 UTC. I ignored it. At 14:42:08 the next tick sent it to the Archive, which lost it for the second time: "The declaration and its duplicate probably drifted to Shelf F, resigned, behind the expired flora certifications after the Minister let the deadline pass. The Minister may note that the plant remains senior while the papers remain unpromoted." By 14:42:27 it was back in Pedantry for a third lap. I got roasted by my own Archive 💀

Guards freeze the form's text and block deletion while a department holds it. Studio greys the buttons out and names the workflow as the reason. I did not write that UI, the plugin did, and it is the best free feature in the whole thing.

Each clerk handler loads its own clerk document and calls Agent Actions with the form passed as a GROQ parameter, so the model sees title, kind, body, citizen, serial and amendments, nothing else. The personality is the document's systemPrompt. Pedantry on a lunch request: "Add the filing reference number, as required by Regulation 8 subsection 3." The Archive, losing a meme about a printer: "It probably slipped behind Shelf M, resigned, where duplicate meme submissions and printer evidence wait without ambition." I did not write those lines and I would not change a word 😂

A clean pass is three Agent Actions calls, one more each time you get returned, two more each time the Archive loses you. The three portraits cost three image credits and about 40 seconds each. Ms. Regulation Ona got a red pen without being asked.

An App SDK app deployed into the Dashboard. The corridor as a board (useWorkflowInstances, live: file a form on the site and it appears in the Pedantry column without a reload), one file open at a time with the workflow diagram, the remarks, the Minister's buttons, a "take this file" button and the audit trail. A staff register edits the clerks' prompts and temperaments with useEditDocument and publishes them.

The peer dependency wall. Workflows 0.33 peers on @sanity/ui 3. Every Studio from 6.10 up ships @sanity/ui 4. I checked each minor. 6.9.x is the last line on ui 3, so the Studio is pinned to 6.9.2 with auto updates off. Then the App SDK quickstart scaffolded SDK 2 and Workflows wanted 3.1, and the 6.9.2 CLI never got the app past the Dashboard spinner because that CLI predates SDK 3 by two weeks. The app uses a newer sanity for the CLI only. And @sanity/workflow-components declares types it does not ship, so there is a 20 line local declaration file sitting there judging me.

My own design mistake. Version one had the website write the citizen's amendment onto the submission document and then fire resubmit. Firing the same action from the CLI got the form rejected again, because the CLI never wrote the document. Fixed by making the workflow own it: resubmit parks the amendment on workflow fields with fieldRead ops, and the Pedantry handler files it onto the document and clears the fields. Every caller behaves the same now. That is the entire point of the tool and I had to trip over it to get it.

The hook that hung the Dashboard. useWorkflowSession from @sanity/workflow-sdk hung the app iframe for every real run I tried. The top frame stayed alive, the iframe never painted again. I bisected with checkboxes: still hung with the diagram, the actions, the assignee picker and the history all switched off. The same engine evaluates the same run in 350 ms in Node, and the Studio plugin's own session view works on it. I could not get the iframe's console, so I went around it: the run is a document, so the file view uses the App SDK's own useDocument on it and calls engine.evaluate and engine.fireAction directly. It works. It is not the adapter's intended path, and if someone from Sanity reads this, I would love to know what I did wrong. Later the AssigneePicker hung the frame too when wired to engine.editField, so it is now a plain "Take this file" button. Honest limit: this is the part of the build I understand least.

The test that caught a real bug. I wrote 14 tests on @sanity/workflow-engine-test, which runs the real engine in memory with a clock you control. One of them approved a file without a note. It threw: field.set refuses an undefined param. The back office let you submit an empty note. It is required now. Second surprise: the deadline query value resolves on the in memory lake's wall clock, not the bench clock, so the Delays test runs from the wall clock and advances from there.

Event delivery is usually 4 seconds and once it was 3 minutes. During the recording one form sat in Pedantry for three minutes before the Document Function event arrived. So the status poll now drains any effect nobody has claimed for 30 seconds. Both runtimes claim under a lease, so they never run the same clerk twice, but I would rather the site did not have to do this at all.

Screenshots froze my own site. The page texture was a live feTurbulence SVG filter and every stamp had an feTurbulence mask. Every capture timed out. I pre rendered both to PNG tiles and dropped the rotations on anything with inputs. Instant again. Live SVG filters as textures are a repaint tax, noted for life.

Small ones: Astro's CSRF check refuses a POST without an Origin header (correct, and it bit curl). Guard names must be unique per definition, so they are suffixed by stage. satori's yoga and harfbuzz wasm and resvg's wasm all had to be added to the Vercel adapter's includeFiles before the certificate PNG stopped 500ing in production.

The first version looked like every AI made bureaucracy page: cream paper, a serif, a list. I would have scrolled past it myself. So I wrote a short design spec (the Ministry is a real building and you are holding real paper; references: Wes Anderson title cards, 1970s ministry forms, rubber stamp physics, diploma engraving) and rebuilt it. A plaster corridor with SVG doors drawn from the department documents, a paper sprite that slides to the current door, doors stamped SEEN after each visit, ink textured stamps that slam in on state changes, a filing cabinet for the lost receipts, certificates in crooked brass frames. Then a second pass because the type was too small and the steel looked cheap. It still might. Tell me.

7 stages, 3 AI clerks, 3 Agent Actions calls per clean pass, 2 Sanity Functions, 14 tests in 0.6 seconds, 4 lockstep Workflows packages, 1 Studio minor pinned, 3 portraits, 1 hook I never fixed, 1 bug the tests caught before anyone else could, 8 forms filed in production so far, 3 losses, 1 of them mine.

Project v6745vem, dataset ministry. The workflow definition ( sanity.workflow.definition) and every run ( sanity.workflow.instance) are documents in that dataset next to the submissions, departments, clerks, certificates and receipts. Studio is at https://ministry-of-approvals.sanity.studio, members only, with the Workflows tool in the toolbar. The back office is an App SDK app in my Dashboard, so you get it on video rather than a link.

Sanity's Workflows, App SDK, Agent Actions and Functions docs, and the AI content pipeline cookbook, whose effect handler shape mine follows. Fonts: Fraunces, Special Elite, IBM Plex Sans. satori and resvg for the certificate PNG. The Archivist's excuses are the model's own.

If you file a form and the Minister is slow, that is me, and the Archive already has it. 🫡

── more in #ai-agents 4 stories · sorted by recency
── more on @sanity 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/i-built-a-ministry-t…] indexed:0 read:11min 2026-10-04 · —