{"slug": "nebula-agent-an-ai-that-surfaces-contradictions-in-structured-content", "title": "Nebula Agent: An AI That Surfaces Contradictions in Structured Content", "summary": "A developer built Nebula Agent, a command-line AI agent that queries a Sanity Content Lake with self-written GROQ queries and uses Gemini 3.5 Flash Lite to answer questions about a fictional board game while surfacing contradictions between sources. In live transcripts, the agent flagged conflicting starting energy-token counts (5 in the rulebook versus 8 in the errata) and win conditions (10 stars versus 12), citing source URLs for each claim. The project was submitted to the Sanity Challenge, Path One.", "body_md": "*This is a submission for the [Sanity Challenge, Path One: Ship an Agent That Queries Real Content](https://dev.to/challenges/sanity-2026-09-16)*\n\nA **Sanity-powered AI agent** that answers questions about a fictional board game called **Nebula** and, crucially, **surfaces contradictions between sources**.\n\nThe agent doesn't just search for keywords. It writes its own **GROQ queries(Graph Relational Object Queries)** against a Sanity Content Lake in real time, reads the structured articles it retrieves, and when two sources disagree and presents **both claims side by side with their source URLs**.\n\n**The core question:**\n\n\"According to the Nebula rulebook and errata, how many energy tokens do players start with? Are there any contradictions?\"\n\n**The agent's answer:**\n\nThe sources provide conflicting information:\n\nA keyword search would have returned both articles. It would not have known which was authoritative, nor that the errata corrects the rulebook. My agent does this **because the content was structured**.\n\nSince the agent runs in the terminal, here's a live transcript from two consecutive runs.\n\n**Question 1_Energy tokens:**\n\n```\ntext\n👤 User: According to the Nebula rulebook and errata, how many energy tokens\n   do players start with? Are there any contradictions?\n\n🤖 Agent using tool: query_documents\n   Args: {\"query\":\"*[_type match \\\"*rule*\\\" || _type match \\\"*errata*\\\" || lower(title) match \\\"*rule*\\\" || lower(title) match \\\"*errata*\\\"]\"}\n   Result preview: [{\"_type\":\"article\",\"body\":\"To win Nebula, you must collect 10 stars...\"}]\n\n🤖 Agent: According to the Nebula rulebook and errata, there is a contradiction\nregarding how many energy tokens players start with:\n\n* Nebula Core Rulebook - Setup: States that each player starts with 5 energy\n  tokens. Source: https://example.com/nebula-rulebook\n\n* Nebula Official Errata v2.1: States that the core rulebook is incorrect and\n  that the correct starting amount is 8 energy tokens.\n  Source: https://example.com/nebula-errata\n```\n\nQuestion 2_Win condition:\n\n```\n👤 User: What is the win condition for Nebula?\n\n🤖 Agent using tool: query_documents\n   Args: {\"query\":\"*[_type == \\\"article\\\" && (title match \\\"*Nebula*\\\" || body match \\\"*Nebula*\\\")]{title, body, source}\"}\n   Result preview: [{\"body\":\"Many players struggle with energy management...\"}]\n\n🤖 Agent: The sources provide conflicting information regarding the win\ncondition for Nebula:\n\n* According to the Nebula Core Rulebook and the strategy guide, you must\n  collect 10 stars to win (Rulebook, Strategy Guide).\n\n* According to the Nebula Official Errata v2.1, the win condition was updated\n  to collecting 12 stars (Errata).\n```\n\nA command-line agent that uses Gemini to answer questions from article content stored in Sanity.\n\nThe agent checks its Sanity connection, sends a built-in question about Nebula's win condition to Gemini, and answers using Sanity query results. It can query documents with GROQ or list the dataset's document types and fields. Its instructions require source URLs for claims and ask it to show contradictory sources side by side.\n\nThe command prints the question, connection status, tool name, a result preview, and the final answer. Query arguments are not printed. Errors are written to stderr.\n\n`gemini-3.5-flash-lite`\n`uyvc8si1`, dataset `production`\nRun these commands from the `agent` directory:\n\n```\nnpm install\n```\n\nCreate an `.env` file in the `agent` directory with both keys:\n\n```\nGEMINI_API_KEY=your-gemini-api-key\nSANITY_API_TOKEN=your-sanity-api-token\n```\n\nThe Sanity token must have…\n\n**Stack:**\n\nNode.js\n\nGemini 3.5 Flash Lite (free tier)\n\n[@sanity](https://dev.to/sanity)/client for GROQ queries\n\n@google/genai for function calling\n\n** Structured Content Model**\n\ni defined a single document type in sanity studio using typescript:\n\n``` js\nexport const article = defineType({\n  name: 'article',\n  title: 'Article',\n  type: 'document',\n  fields: [\n    defineField({ name: 'title',  title: 'Title',  type: 'string' }),\n    defineField({ name: 'slug',   title: 'Slug',   type: 'slug', options: { source: 'title' } }),\n    defineField({ name: 'body',   title: 'Body',   type: 'text' }),\n    defineField({ name: 'source', title: 'Source', type: 'url' }),\n  ],\n})\n```\n\nI populated it with three documents that set up a deliberate contradiction:\n\nThe title and the source for each of the documents are:\n\nNebula Core Rulebook; The Setup - [https://example.com/nebula-rulebook](https://example.com/nebula-rulebook)\n\nNebula Official Errata - [https://example.com/nebula-errata](https://example.com/nebula-errata)\n\nHow to Win at Nebula - [https://example.com/nebula-strategy](https://example.com/nebula-strategy)\n\n(The source URLs are placeholder examples. In a real deployment, this field would point to the actual publisher page.)\n\nThe agent uses function calling with Gemini. It has one primary tool and it was coded out with javascript:\n\n```\n{\n  name: \"query_documents\",\n  description: \"Run a GROQ query against the Sanity dataset. Returns JSON.\",\n  parameters: {\n    type: \"object\",\n    properties: {\n      query: { type: \"string\", description: \"A GROQ query string.\" }\n    },\n    required: [\"query\"]\n  }\n}\n```\n\nWhen the User asks a question, the LLM:\n\nWrites a GROQ query itself: an example- *[_type == \"article\"]{title, body, source}\n\nMy code executes it via [@sanity](https://dev.to/sanity)/client against project uyvc8sil, dataset production\n\nThe JSON result is fed back into the LLM's Context\n\nThe LLM reads the content and answers with citations\n\nThe system prompt tells the model: \n\n\"When two sources contradict each other, show both claims side by side with their sources. Cite the source URL for every claim. Never invent information.\"\n\nBecause the content is structured, the agent can do the following:\n\nCompare specific fields across documents; it reads body from the rulebook and body from the errata and notices the numeric values differ.\n\nAttribute each claim to its source URL; the citations aren't guessed, they're data.\n\nDistinguish types of documents; the errata is authoritative over the rulebook because its title says so.\n\nA plain-text search would have surfaced both documents but had no way to reason about which was newer, more authoritative, or even that they contradicted each other.\n\nSchema-informed prompting; The LLM initially guessed type names like card and document, returning empty results. I injected a schema description into the system prompt so it knows to query *[_type == \"article\"]. This is a small detail that helps to dramatically improved reliability.\n\nSanity as the source of truth(where the articles are confirmed from); No content is hardcoded in the agent. Every fact the agent reports comes from a live GROQ query against the Content Lake. If I update an article in the Studio, the next question gets the updated answer.\n\nuseCdn(false); I disabled the CDN for the agent so responses are always fresh from the Content Lake and always 100% Real Time. This matters for an agent where \"the latest errata\" is the whole point.\n\nProject ID: uyvc8sil\n\nDataset: production\n\nDocument type: article (fields: title, slug, body, source)\n\nI learned a lot because this is my first time of hearing about sanity. I learned how to made the case for structured content viscerally clear. So this provide me an opportunity that an LLM can reason about provenance when we have fields like title, body, and source defined in a schema. This led me to understand the difference between these statements \"here are two documents\" and \"these two sources disagree, and here's the URL for each claim\".\n\nThe hardest part wasn't the LLM logic, it was teaching the agent the schema. Once I stopped trying to make the model guess type names and instead told it was in the content Lake, everything clicked.\n\nThanks to Sanity and DEV for the challenge. It pushed me to build something I'd been wanting to try: an agent that treats structured content as the foundation, not an afterthought.", "url": "https://wpnews.pro/news/nebula-agent-an-ai-that-surfaces-contradictions-in-structured-content", "canonical_source": "https://dev.to/vicarioy/nebula-agent-an-ai-that-surfaces-contradictions-in-structured-content-3760", "published_at": "2026-09-27 10:27:07+00:00", "updated_at": "2026-09-27 11:01:19.182783+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "structured-data", "large-language-models", "developer-tools"], "entities": ["Sanity", "Gemini 3.5 Flash Lite", "Google", "Nebula", "Node.js", "GROQ"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/nebula-agent-an-ai-that-surfaces-contradictions-in-structured-content", "markdown": "https://wpnews.pro/news/nebula-agent-an-ai-that-surfaces-contradictions-in-structured-content.md", "text": "https://wpnews.pro/news/nebula-agent-an-ai-that-surfaces-contradictions-in-structured-content.txt", "jsonld": "https://wpnews.pro/news/nebula-agent-an-ai-that-surfaces-contradictions-in-structured-content.jsonld"}}