I vibe-coded a career app on 1,016 O*NET jobs: two Sanity Workflows, a patched plugin, and a mascot named Mot
DEV Community

I vibe-coded a career app on 1,016 O*NET jobs: two Sanity Workflows, a patched plugin, and a mascot named Mot

This is a submission for the Sanity Challenge, Path Two: Vibe-Code Something Strange What I Built Mot is a career exploration app for job seekers, built on ONET 31.0, the U.S. Department of Labor's database of about 1,000 occupations. Mot (a cross between Meta's Muse and OpenAI's Dots, made casual so it feels like a friend) has three tools: - Career Explorer: chat about careers, bookmark occupations, and compare them. - Mock Interview Coach: practice interviews graded against ONET's own examples of each skill level. - Interest Quiz: a RIASEC interest quiz matched against every occupation, plus a sidebar of other career quizzes from around the web. My Path One post covers the agents. This post covers the other half: the Sanity side I prompted into existence, and the people who aren't job seekers. - Career counselors write interview coaching guides in Studio. A guide only reaches the AI coach after another counselor reviews it in a Workflow. - Content managers vet career quizzes that anyone with Studio access can submit. A quiz goes through a second Workflow with gated review tasks, and approving it publishes it straight to the site. - I get a weekly Sanity Function that classifies every chat transcript, so I can see what people ask. Everything except the Astro front end and the Node agent service lives in Sanity: more than 1,500 ONET documents, the coaching guides, the quiz submissions, both workflow definitions, a Context Knowledge Base, two Context MCP endpoints, and the Function. Demo Live app: https://career-exploration-two.vercel.app/ (a Cloudflare Turnstile human check runs before the agents start) Code - Workspace (Sanity Function, Blueprint, docs): https://github.com/sea2709/career-exploration - Sanity Studio (schemas, workflows, custom action, importer, setup scripts): https://github.com/sea2709/career-exploration-studio - Agent service (Node, AI SDK, Gemini): https://github.com/sea2709/career-exploration-agent - Web app (Astro 7 + React 19, Tailwind 4): https://github.com/sea2709/career-exploration-web My Build Process Tooling. I built all of it in Cursor's agent mode, across about 47 agent chats and roughly 80 commits in four repos, from September 19 to October 2. I wrote very little code by hand. My job was deciding what to build, writing the prompts, testing in the browser, and saying "no, not like that." Three things made the agent much more reliable, and I'd set them up earlier next time: - An AGENTS.md in every repo, plus one at the workspace root listing the cross-repo contracts. For example: "the quiz tool shapes inagent/src/quiz-agent.ts match the types at the top ofweb/src/components/InterestQuiz.tsx ", and "QUIZ_FOCUSES in the Studio schema matchesFOCUS_LABELS in the web component." Once these were written down, the agent stopped changing one side of a contract and forgetting the other. - Sanity's agent skills ( create-agent-with-sanity-context ,shape-your-agent ,dial-your-context ), vendored into the agent repo. My actual prompt was "Use the create-agent-with-sanity-context skill to help me build an agent in this project." - Cursor rules for my own bad habits: every new branch goes in a git worktree inside its app folder, and the agent never starts a dev server on main /master . I added the second one after the agent started a second Astro server on port 4322 next to mine. Here's how it went, in order, with the prompts that mattered. 1. Modeling ONET in Sanity (day 1) I want to create schema for Onet data at https://www.onetcenter.org/dictionary/31.0/mysql/ in Sanity studio ONET ships as dozens of relational tables. The first decision was the important one: only reference tables and occupations become documents. Per-occupation rows (tasks, job titles, ratings, related occupations, interest scores) are embedded as arrays on onetOccupation , and element-level rows go on onetContentModelElement . The rest of the project works because of that choice. An agent can fetch one document and get the whole job, and GROQ can join a rating to its scale and its Level Scale Anchors with a single -> . The importer (pnpm import:onet ) downloads the tab-separated files, parses them, and upserts them in phases, with --dry-run , --limit , --phases , and --occupations flags so I could import 50 occupations and look at them in Studio before importing all 1,016. I later asked "is there any data I missed importing?" and then "I want all data to get imported", which added the detailed work activities, task ratings, and interest data in later phases. The Studio structure puts the ONET types under one ONET section (Occupations, then Reference data), and the occupation document has tabs (Overview, Tasks, Job Titles, Software Skills, Ratings, Related, Interests, Survey Metadata), so a 1,000-row ratings array doesn't bury the description. 2. From "what should I build?" to three agents I want to build an agent consume data from Onet, any ideas about agent I should build? That gave me the Career Explorer. Then: So right now, we have a Career Explorer tool, I want to add a new tool Mock interview coach and interest quiz, I think each tool will present in a separate page At first the agent lived inside the Astro app. After a day I asked, "I want to separate agent into another workspace, it needs to stand out from web/", and it became its own Node service. The web app proxies to it through server routes, behind a Turnstile human check and a shared bearer token. 3. Finding a real job for a Knowledge Base In Sanity, when creating a MCP endpoint, I need to point out the Content source, which comes with 2 options knowledge base and Dataset. What are the differences between two? do you have any ideas that I can use Knowledge Base in our app? The answer that stuck: career counselors write interview coaching guides, and a Knowledge Base turns them into the coach's playbook. ONET itself stays on a dataset-backed endpoint because answers need exact codes and ratings. One honest detail: the challenge page says beta Knowledge Bases index up to 150 documents, so I had the agent write that limit into AGENTS.md . Later, the Knowledge Base API reported a source limit of 5,000 for my organization. Now the docs tell future agents to check sourceUsage instead of trusting either number. 4. Workflows, round one: coaching guides Can you come up ideas for the app where leverage Workflows in Sanity? The idea I picked: Review coaching guides before they reach the Interview Coach. Right now, publishing a coachingGuide sends it straight into the Context Knowledge Base, so one bad guide immediately… I used @sanity-labs/sanity-plugin-workflows . The workflow is defined in code (coachingWorkflow.ts ) and a setup script writes it to the dataset as a workflow.definition document. After that, Studio is the source of truth. It has two roles (Author and Reviewing counselor), tasks bound to those roles, and completion gating, so a guide can't leave Counselor review until "check that the Job Zones match the advice" and "check the guide agrees with the rating scale" are done. What I got wrong: the first version had a fourth stage, Coach test, backed by a separate staging Knowledge Base that included drafts, so you could try a guide in the coach before publishing it. It looked careful on paper. In practice it used up a Knowledge Base slot. I also learned that a Knowledge Base refresh only files issues and doesn't rewrite entries, so publishing a guide doesn't change what the coach reads until someone applies those issues. That made the staging copy pointless. I removed the stage, moved the "run three mock answers in the coach" task to Approved as a post-publish reminder, and deleted the staging Knowledge Base. What surprised me: when I published an overlapping guide, the refresh did flag it, but the dashboard's Issues page didn't show it. I only found it through the API. So I had the agent change pnpm kb:coaching to print every open issue (kind, severity, entry path, finding, suggested fix) right in the terminal. Two setup gotchas, now in the Studio README: workflow tasks live in the comments addon dataset, so task creation silently does nothing until someone has added at least one comment in that Studio. And role-bound tasks only appear once the role has an assignee. 5. Workflows, round two: user-submitted career quizzes I'd added a hard-coded list of "other career quizzes" to the quiz page. Then: I want to have a new content type for quiz, so people can submit quiz, and submitted quizes will go through a review process The careerQuiz type has a name, provider, URL (with a duplicate-URL check), description, focus, cost, list order, and submitter notes. The workflow is Draft → Ready to Review → In Review → Approved, with a Retired off-ramp for dead links or quizzes that start charging money. In Review gates on three Content Manager tasks: check that the provider is trustworthy (read the privacy policy, reject quizzes that sell data or push paid coaching before showing results), check the listing, and take the quiz end to end. A fourth task, "Try the quiz on a phone", is optional, because many job seekers only have a phone. Then I hit a design problem. A workflow transition only changes the document's status . So a quiz could say "Approved" and still not be on the site. My prompts: When the quiz is moved to Approved. It should be published I think I don't need the step from Approved -> Published The result is a custom document action (publishOnApproveAction.ts ). It wraps the plugin's action so that at In Review, "Move to Approved" becomes Approve and publish. The plugin still runs its task gating and confirm dialog. When the transition succeeds, the action publishes the draft with client.action({actionType: 'sanity.action.document.publish'}) . If publishing fails, the quiz stays at Approved, a toast says "Approved, but not published", and the regular Publish button is there to retry. In this Studio, "Approved" means "live on the site." 6. The bug the m

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.