# I built a ministry that loses your form on purpose

> Source: <https://dev.to/manifesta/i-built-a-ministry-that-loses-your-form-on-purpose-4o7p>
> Published: 2026-10-04 23:25:59+00:00

*This is a submission for the [Sanity Challenge, Path Two: Vibe-Code Something Strange](https://dev.to/challenges/sanity-2026-09-16)*

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](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. 🫡
