DEV Community

Cover image for The fries are gluten-free. The fryer isn't. An allergy agent that only works because the menu is a graph
Malbolged
Malbolged Subscriber

Posted on AI-assisted

The fries are gluten-free. The fryer isn't. An allergy agent that only works because the menu is a graph

Sanity Challenge Path One Submission

This is a submission for the Sanity Challenge, Path One: Ship an Agent That Queries Real Content

What I Built

Olive & Ember Allergy Concierge: an AI agent for a (fictional) Mediterranean grill in Dublin that tells guests with food allergies what they can actually eat, why, and what the kitchen can change to make a dish safe.

Ask a normal menu chatbot "are the fries gluten-free?" and it reads "Skin-on fries, sea salt" and says yes. It's wrong. The fries share Fryer 1 with battered cod, calamari and chicken goujons. A coeliac guest needs the swap to the gluten-free fries from the dedicated Fryer 2. That fact isn't in any description. It lives in a relationship: component → prepared on → shared equipment ← other components → ingredients → allergens.

The menu has plenty of these traps, all modelled rather than written down:

  • The Caesar is gluten-free only once the sourdough croutons (a removable component) are left off.
  • The flame-grilled vegetables look nut-free, but the romesco has almonds and hazelnuts.
  • The charred cauliflower looks vegan, but there's honey in the harissa dressing.
  • The slow-roast lamb hides fish and gluten inside the Worcestershire in its glaze (a sub-recipe inside a sub-recipe).
  • The vegan chocolate pot has no milk in the recipe, but the supplier's label says may contain milk.
  • The rice pilaf is made with chicken stock, so it's not vegetarian.

Guests tick their allergies (the 14 EU allergens) and diet, then ask in plain language. The agent answers with dish cards, and each card's verdict (safe, safe with changes, caution or unsafe) is computed from the content graph, not by the AI.

Demo

Live: https://olive-ember-allergen-agent.vercel.app

One-click demos (each link preselects the allergies and asks the question):

Each answer shows a trace of what the agent actually looked up (Queried menu graph → Read kitchen guidance → Verified N dishes), then groups the dishes into Safe as served, Safe with a change (with the exact "Ask the kitchen to…" fix) and Not for you today.

Desktop Full conversation
Mobile Full conversation

Answers take 30–90 seconds on the free Gemini tier.

Code

Olive & Ember · Allergen-safe menu agent

An AI allergy concierge for a (fictional) Mediterranean grill in Dublin. Guests pick their allergies and diet, ask questions in plain language, and get answers grounded in the restaurant's structured menu graph in Sanity plus a Sanity Context Knowledge Base of kitchen procedures and UK Food Standards Agency guidance.

Live demo: https://olive-ember-allergen-agent.vercel.app Public dataset: project a2iy46w2, dataset production (public read, e.g. all dishes)

Built for the DEV Sanity Challenge, Path One. Not medical or dietary advice.

Why structure matters here

"Is this dish safe for me?" can't be answered from menu prose. The fries have three clean ingredients, yet they are not gluten-free: they share Fryer 1 with battered cod and calamari. The Caesar is gluten-free only once the croutons are left off. The "vegan-looking" cauliflower has honey in its dressing. Those answers only exist as relationships:

dish ─▶ components[]
…

How I Used Sanity

1. The menu is a graph, not a document

Six schema types in Sanity Studio: dish, component (sub-recipe), ingredient, allergen, equipment and guidanceArticle.

  • A dish is assembled from components. Each has a removable flag, and the dish lists supported substitutions (e.g. brioche bun → gluten-free bun, +€1.50).
  • A component has ingredients and at most one level of sub-components. That limit is enforced by a custom Studio validation, so every allergen roll-up is a fixed-depth GROQ projection.
  • An ingredient references the allergens it contains, the allergens its supplier says it may contain, and its animalOrigin, which drives the vegan and vegetarian checks.
  • A component is preparedOn equipment. Shared equipment passes the allergens of everything cooked on it to everything else cooked on it.

The seed has 14 allergens, 87 ingredients, 51 components, 8 stations and 25 dishes.

2. Two Sanity Context MCP endpoints, on purpose

A Context endpoint with a dataset source serves GROQ tools and ignores Knowledge Base sources. So the agent connects to two endpoints:

Endpoint Mode Source Used for
menu GROQ dataset a2iy46w2.production, filtered to menu types What is in this dish?
guidance Knowledge Base Knowledge Base "Olive & Ember allergen guidance" How does the kitchen handle this? What does "may contain" mean? What happens if I react?

The Knowledge Base is built from two sources:

  • 11 guidanceArticle documents from the same dataset (a dataset source): the shared-fryer policy, the allergy-order workflow, coeliac vs. gluten intolerance, how the kitchen defines vegan and vegetarian, the anaphylaxis procedure, and more.
  • The UK Food Standards Agency's Allergen guidance for food businesses (a website source).

The build turned these into 10 cited entries. It also caught a real conflict: my article said "112 in the EU" while the entry said "999 or 112". Resolving that is what made me pin the restaurant to Dublin, where both numbers work.

Tools the agent uses:

  • groq_query on the menu endpoint, walking dish → components → subComponents → ingredients → allergens in one query, and following shared equipment with *[_type == "component" && references(^._id)].
  • knowledge_base_read on the guidance endpoint, reading entries such as cross_contact and emergency_response/anaphylaxis_procedure.
  • Both endpoints' /initial-context payloads are fetched once and inlined into the system prompt, so the initial_context tool is dropped.

Here's the kind of query the agent writes on its own:

*[_type == "dish" && slug.current == $slug][0]{
  name,
  "equipment": components[].component->preparedOn[]->{
    name, shared,
    "sharedWith": *[_type == "component" && references(^._id)]{
      name,
      "allergens": array::unique(
        coalesce(ingredients[]->allergens[]->code, [])
        + coalesce(subComponents[]->ingredients[]->allergens[]->code, [])
      )
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

3. The LLM finds and explains; the graph decides

Safety is too important to leave to a language model's reading of a JSON blob. The same graph feeds a small pure function (evaluateDish). It rolls up contains, may contain and cross-contact, checks the diet against animalOrigin, and then searches the dish's removable components and substitutions for the smallest set of changes that makes it safe.

That function:

  • renders every dish card, and
  • is exposed to the agent as a verify_dishes tool that it must call before stating a verdict.

In testing, before I added verify_dishes, the agent told a peanut-allergic guest to avoid the falafel. The graph knew the peanut risk came only from the tahini sauce, which can be left off. With the tool, the agent's text and the card now agree: safe with changes, no tahini.

4. Editors stay in control

The GROQ filter and domain instructions for each endpoint live in the Context app, not in code. The kitchen's procedures live as ordinary Sanity documents that feed the Knowledge Base. When a chef changes a recipe or moves a dish to another fryer in the Studio, the next answer reflects it. There's no re-embedding and no prompt edit.

Sanity Project Details

  • Project ID: a2iy46w2
  • Dataset: production (public)
  • Browse the data:
  • Context endpoints: menu (GROQ mode) and guidance (Knowledge Base mode) in organization o9gofjii1.

Agent Session

The whole project was built in one session with Claude Code: challenge research, schema design, seed data, both Context endpoints and the Knowledge Base, the agent, the deployment and this post.

Building an allergen-safe menu agent on Sanity Context claude-opus-5-5
Agent
11 messages not shown
Agent
Agent
4 messages not shown
Agent
18 messages not shown
You

<task-notification>
<task-id>ac78a8f7d51fccdff</task-id>
<tool-use-id>toolu_016vJSdtnAdcgxSCfBn6DNkJ</tool-use-id>
<output-file>[REDACTED]\AppData\Local\Temp\claude\C--Users-user-Desktop-sanity-challenge\23523501-313e-42ee-84b3-7aed61500188\tasks\ac78a8f7d51fccdff.output</output-file>
<status>completed</status>
<summary>Agent "Seed data + guidance articles" finished</summary>
<note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>The seed data is written and the offline validator passes. seed.ts hasn't run yet; it needs the Sanity project and a write token first.

Files (all under studio/scripts/):
- data/: helpers.ts, allergens.ts, ingredients.ts, equipment.ts, components.ts, dishes.ts, guidance.ts, index.ts (exports allDocuments in the order they must be seeded)
- seed.ts: idempotent createOrReplace, written in chunks of 100, with a clear error if env vars are missing
- validate-data.ts: runs offline

Counts: 196 documents in total.

Type Count
Allergens 14
Ingredients 87
Equipment 8 (Fryer 1 shared, Fryer 2 dedicated, grill, plancha, bread oven, GF toaster, pastry bench, cold prep)
Components 51
Dishes 25, across all 5 sections
Guidance articles 11, each 200–600 words with a fictional-demo disclaimer

I went over your targets on components (~30–35) and dishes (20–24). The extra halloumi salad puts the orphaned za'atar, capers and dressing to use.

Teaching cases:
- Honey: charred-cauliflower is vegan only if the harissa dressing comes off.
- Hidden nuts: flame-grilled-vegetables has romesco (tree nuts, plus gluten and sulphites).
- Croutons: ember-caesar is gluten-free only with the croutons removed.
- Supplier may-contain: vegan-chocolate-pot may contain milk and tree nuts (dark chocolate) and gluten (oat crumble).
- Unavailable: garlic-prawns.
- Shared fryer: plain fries have clean ingredients but pick up gluten, fish, molluscs, milk and eggs in Fryer 1.
- More hidden sources:
- falafel has a wheat binder.
- rice-pilaf uses chicken stock, so it isn't vegetarian.
- slow-roast-lamb-shoulder has fish and gluten from Worcestershire.
- The gluten-free flatbread swap contains lupin.

Validation: all references resolve, keys and slugs are unique, nesting stays at one level, and every substitution matches a component in its dish. No orphans remain. The data modules type-check under tsc --strict.

One deviation from your directive: IDs use hyphens (allergen-gluten), not dots. A dotted ID sits under a path in Sanity and is hidden from unauthenticated reads, which would break the public dataset.

Schema: no changes.</result>
<usage><subagent_tokens>176739</subagent_tokens><tool_uses>19</tool_uses><duration_ms>496009</duration_ms></usage>
</task-notification>

31 messages not shown
Agent
Agent
64 messages not shown
Agent

The source and filter are right. The Instructions box is still empty; fill it in before creating the endpoint.

Your steps

  1. Paste this into Instructions: This dataset is the allergen graph of Olive & Ember, a (fictional) Mediterranean grill. A dish is assembled from components[] (each {component->, removable}); a component has ingredients[]-> and at most one level of subComponents[]->; an ingredient has allergens[]-> and mayContain[]-> (supplier precautionary labelling) and animalOrigin (none|dairy|egg|honey|meat|fish|shellfish). Allergens are the 14 EU Annex II allergens; always filter on allergen.code (gluten, crustaceans, eggs, fish, peanuts, soybeans, milk, tree-nuts, celery, mustard, sesame, sulphites, lupin, molluscs). A dish's allergens = allergens of its components' ingredients PLUS the ingredients of their subComponents. Never judge from names or descriptions. Cross-contact: a component's preparedOn[]-> equipment with shared == true exposes it to the allergens of every other component prepared on that equipment: *[_type == "component" && references($equipmentId)]. dish.substitutions[] ({replaces->, with->, surcharge}) and removable components are the only modifications the kitchen supports. Only recommend dishes with available == true.
  2. Optional: click Preview to check that the filter returns documents.
  3. Click Create endpoint.
  4. Copy the endpoint URL into .env as SANITY_CONTEXT_MENU_MCP_URL=, then say "menu done".

Where things stand

  • Knowledge Base: building, or ready to build
  • menu endpoint: form filled except for Instructions
  • Next: I test the menu endpoint as soon as its URL is in .env
1 message not shown
Agent
14 messages not shown
Agent
52 messages not shown
Agent
Agent
Agent
7 messages not shown
Agent
3 messages not shown
Agent

The full agent now works on both endpoints, and its answers match the dish cards. Tests pass and the production build compiles.

What works now

  1. Menu endpoint (GROQ): finds candidate dishes by walking the dish → component → ingredient → allergen graph, including cross-contact from shared equipment.
  2. Guidance endpoint (Knowledge Base): reads the cross-contact, order-workflow and anaphylaxis entries. It says "999 or 112" and describes the 5-step allergy ticket process.
  3. New verify_dishes tool: the agent now checks its final verdicts with the same deterministic function that draws the dish cards. For example, falafel is now correctly "safe without the tahini sauce" instead of "avoid".
  4. Speed: about 32 seconds per combined answer.
  5. Checks: 5 of 5 tests pass, next build passes.

Your steps

  1. Resolve the last issue: the Knowledge Base still shows Issues 1. Choose "What the entry says" (999 or 112) → Resolve issue, then click Check for changes so it picks up the Dublin location text.
  2. Try the app: http://localhost:3100. Pick some allergies and ask a question.
  3. Log in to Vercel for the public demo: ! npx vercel login.
  4. Create a GitHub repo for the code link, since git is read-only for me. When you're ready, I'll give you the exact commands.

What I do next

  1. Deploy to Vercel with the env vars set as secrets there.
  2. Draft the DEV post from the Path 1 template in .pr/, or submission/POST.md if you prefer. It will include the Sanity project ID, the public dataset URL and the "only works because it's structured" story.
  3. Take screenshots and record demo chats for the post.

Where things stand

  • Deadline: Sat Oct 4, 11:59 PM PDT (5 days left)
  • Waiting on: issue resolution, vercel login, GitHub repo
20 messages not shown
Agent
6 messages not shown
Agent
7 messages not shown
Agent
2 messages not shown
Agent

The app is live at https://olive-ember-allergen-agent.vercel.app, and a full chat works there. The DEV post is drafted. What's left for you: push the code, make the repo public, and publish the post.

Done

  1. Deploy: the Vercel project is linked, 8 production env vars are set (the seed write token is left out), and the deploy is live and public.
  2. Live checks: the home page, the dish verdict endpoint and a full chat all work, using both endpoints plus verify_dishes.
  3. README.md: covers the architecture, why the structure matters, setup and tests.
  4. submission/dev-post.md: follows the exact Path One template. The title is "The fries are gluten-free. The fryer isn't." Both public query links in it work. submission/ is gitignored, so the draft stays local.
  5. Secret scan: clean. .env, .env.local and .vercel are all ignored.

Your steps

  1. Make the repo public. malbolged/olive-ember-allergen-agent is private, and judges can't open it.
  2. Push the code from [REDACTED]\Desktop\sanity_challenge: bash git init -b main git add . git status # check: no .env, .env.local, .vercel, node_modules, submission/ git commit -m "Olive & Ember allergen-safe menu agent (Sanity Challenge, Path One)" git remote add origin https://github.com/malbolged/olive-ember-allergen-agent.git git push -u origin main
  3. Take 2–3 screenshots of the live app, e.g. the coeliac + fries answer with a dish card, and add them under Demo.
  4. Agent session: upload this Claude Code transcript at https://dev.to/agent_sessions/new, trim it, click Make Public, and embed it. It has no full secrets; only 4-character key prefixes appear.
  5. Publish: paste submission/dev-post.md into DEV and set published: true. The #sanitychallenge tag is already in the front matter.

Things to know

  • Latency: answers take about 30–90 seconds on the free Gemini tier, depending on fallbacks. Enabling Gemini billing would make them faster and more consistent for judges.
  • Vercel account: the deploy went to the tester-user account. If that isn't the account you want, tell me before you publish.

Where things stand

  • Deadline: Sat Oct 4, 11:59 PM PDT (5 days left)
  • Done: data, Studio, both Context endpoints, the Knowledge Base, the agent, the UI, the live deploy, the README, the post draft
  • Waiting on you: repo public, push, screenshots, agent session, publish
42 messages not shown
Agent
12 messages not shown
You

change the font and colors, they look too vibecoded

126 messages not shown
Agent

I checked first. The code isn't pushed yet: the folder isn't a git repo, and GitHub now returns "Not Found" for malbolged/olive-ember-allergen-agent. Earlier I could see it as an empty private repo, so it may have been renamed or deleted, or its access changed.

Remaining steps

  1. Confirm the repo exists. Open https://github.com/malbolged/olive-ember-allergen-agent. If it's gone, create it again, empty, and set it to Public. If it exists but is private, go to Settings → Change visibility → Public. Judges need to be able to open it.
  2. Push the code from [REDACTED]\Desktop\sanity_challenge: bash git init -b main git add . git status # check: no .env, .env.local, .vercel, node_modules, submission/ git commit -m "Olive & Ember allergen-safe menu agent (Sanity Challenge, Path One)" git remote add origin https://github.com/malbolged/olive-ember-allergen-agent.git git push -u origin main
  3. Upload the agent session. Go to https://dev.to/agent_sessions/new, upload this Claude Code transcript, cut it down to the best parts (schema design, the two-endpoint discovery, the falafel verify_dishes fix, the 112/999 Knowledge Base issue, the redesign), then click Make Public and copy the embed.
  4. Write the DEV post. Go to https://dev.to/new and paste submission/dev-post.md. Then:
    • Under Demo, upload design/exports/prod-welcome.png, desk-conv-full.png and mob-conv-full.png, and remove the TODO comment.
    • Under Agent Session, paste the embed from step 3 and remove the TODO comment.
    • Optionally, use prod-welcome.png as the cover image.
    • Check that the tags are devchallenge, sanitychallenge, sanity, ai.
  5. Publish. Set published: true, or click Publish, before Sat Oct 4, 11:59 PM PDT.
  6. Optional, for faster answers: enable billing on the Gemini key (AI Studio → Billing). Answers currently take 30–90 seconds on the free tier.

Already done (no action needed)

  • Sanity project a2iy46w2 with a seeded public dataset, the Studio deployed, the schema deployed
  • Both Context endpoints (menu and guidance) and the Knowledge Base built
  • The app live at https://olive-ember-allergen-agent.vercel.app with the redesign
  • README.md and the DEV post draft written

Where things stand

  • Deadline: Sat Oct 4, 11:59 PM PDT (3 days left)
  • Next: confirm the repo exists and is public, then push. Tell me when it's pushed and I'll check it from here.
3 messages not shown
Agent
12 messages not shown
Agent
Add allergen-safe menu agent

Use it in the commit step:
bash
git commit -m "Add allergen-safe menu agent"

8 messages not shown
Agent

The push worked. GitHub has commit 1afa6f8 with all 64 files, the repo is public, and nothing excluded made it up.

Checked on GitHub

  • Top level: app/, studio/, README.md, .env.example, .gitignore, package.json, pnpm-lock.yaml, pnpm-workspace.yaml
  • No secrets or local-only folders: no .env, .vercel, design/ or submission/
  • Branch: master, not main. That's fine: the repo link and the {% github %} embed in the post both use the default branch.

Remaining steps

  1. Upload the agent session. Go to https://dev.to/agent_sessions/new and upload this Claude Code transcript. Keep the best parts: the schema design, the two-endpoint discovery, the falafel verify_dishes fix, the 112/999 Knowledge Base issue, and the redesign. Click Make Public and copy the embed.
  2. Write the post. Go to https://dev.to/new and paste submission/dev-post.md. Then:
    • Under Demo, upload design/exports/prod-welcome.png, desk-conv-full.png and mob-conv-full.png, and remove the TODO comment.
    • Under Agent Session, paste the embed and remove the TODO comment.
    • Optionally, use prod-welcome.png as the cover image.
  3. Publish before Sat Oct 4, 11:59 PM PDT, and send me the post link. I'll check that the links, the repo embed and the demo URLs all work.
  4. Afterwards: delete design/ (or ask me to).

Where things stand

  • Done: the app is live, the repo is public and pushed, the Sanity project and Context endpoints are set up, the post is drafted
  • Waiting on you: agent session, publish the post
  • Deadline: Sat Oct 4, 11:59 PM PDT (3 days left)

Top comments (0)