This is a submission for the Sanity Challenge, Path One: Ship an Agent That Queries Real Content.
CampusLoop Atlas: I Built an Agent That Knows What Is Actually Happening on Campus
Ask the campus. Get an answer grounded in real, structured campus content.
Most "AI campus assistants" are really just chat UIs sitting on top of a search box.
I wanted to build something harder.
CampusLoop Atlas is an agent that answers questions about clubs, events, people, schedules, eligibility, locations, and opportunities by querying a structured Sanity content system through Sanity Context.
The important part is not that it can answer:
"What clubs exist?"
A keyword search can do that.
The interesting questions are things like:
"I'm a second-year CSE student, I like web development, I can only attend events after 5 PM on Friday, and I want something where beginners are welcome. What should I join, and why?"
That answer requires the agent to connect multiple pieces of structured content.
It has to reason over:
Student profile
│
├──────────────┐
│ │
▼ ▼
interests availability
│ │
└──────┬───────┘
▼
clubs
│
┌─────┼─────┐
▼ ▼ ▼
events people eligibility
│
▼
locations
That's the part I wanted Sanity to power.
What I Built
CampusLoop Atlas turns a structured campus content graph into a conversational agent.
The content model is intentionally richer than a collection of blog posts.
A typical record looks conceptually like:
Club
├── name
├── slug
├── description
├── categories[]
├── skills[]
├── audience[]
├── beginnerFriendly
├── members[]
├── events[]
└── socials
Event
├── title
├── startAt
├── endAt
├── venue
├── organizer
├── clubs[]
├── eligibility[]
├── skills[]
├── registrationUrl
└── status
Person
├── name
├── role
├── clubs[]
└── areas[]
Venue
├── name
├── building
└── campusArea
The agent can then answer questions that depend on relationships between documents, not just matching words.
Example
User:
"Which web-development opportunities can a first-year student attend this weekend?"
The agent needs to discover:
- events occurring this weekend
- their organizers
- the organizer's associated clubs
- the skills/category attached to those events
- eligibility requirements
- registration state
- exact timing and venue
That's precisely the kind of task where structured content becomes useful.
The Problem With Normal Search
Suppose the dataset contains:
Web Development Club
HackQuest
Frontend Workshop
ACM Meetup
React Bootcamp
A search engine can find these words.
But the real question might be:
"Which beginner-friendly event this Saturday is run by a club that actively teaches frontend development, is on campus, and still has registration open?"
The answer is not a document.
It is a join across documents.
┌─────────────┐
│ Events │
└──────┬──────┘
│
│ organizer
▼
┌─────────────┐
│ Clubs │
└──────┬──────┘
│
┌────────┼─────────┐
▼ ▼ ▼
skills audience members
│ │
└────┬───┘
▼
Candidate events
│
┌─────┴─────┐
▼ ▼
eligibility schedule
│ │
└─────┬─────┘
▼
Answer
Sanity stores the structure. The agent reasons over it.
Why Sanity?
Sanity's Content Lake stores content as structured JSON documents with schemas that I can design around the domain.
For this project, that means I can model the campus as actual entities and relationships rather than forcing everything into flat text.
Sanity Context then exposes that content to an agent through a hosted, read-only MCP server. In GROQ mode, Context provides tools for understanding the schema and querying documents; Knowledge Base mode provides access to pre-built indexed entries. citeturn545668search1turn545668search0
For this challenge I use Knowledge Base + Sanity Context MCP so the agent can retrieve the curated campus knowledge that I selected instead of relying on a generic web search.
The challenge explicitly calls for an agent backed by a Sanity Context MCP endpoint and a Knowledge Base, and says the strongest Path One submissions are ones that only work because the content is structured. citeturn545668view0
That is exactly the constraint I designed around.
How I Used Sanity
1. I modeled the campus as connected content
Instead of:
"ACM organizes HackQuest in the Main Auditorium..."
I store:
{
"_type": "event",
"title": "HackQuest",
"organizer": {
"_ref": "club-acm"
},
"venue": {
"_ref": "venue-main-auditorium"
},
"skills": ["web-development", "ai"],
"eligibility": ["students"],
"status": "registration-open"
}
That small difference changes what the agent can infer.
The agent doesn't have to guess whether two pieces of text refer to the same entity.
The relationship is data.
2. I pointed Sanity Context at the content I actually care about
The Knowledge Base contains the campus sources used by the agent:
CampusLoop content
├── Clubs
├── Events
├── People
├── Venues
├── Eligibility rules
├── Categories
└── FAQs / announcements
Knowledge Bases are pre-built indexes over selected material and can combine sources so an agent can retrieve from one indexed knowledge layer. Sanity currently documents Knowledge Bases as a beta feature. citeturn545668search3turn545668search2
For this project, that means the agent sees a curated campus knowledge graph rather than an unstructured pile of pages.
3. I used the Context tools as an agent, not as a hidden search endpoint
Sanity Context's GROQ-mode tools include:
initial_context
schema_explorer
groq_query
array_field_reader
The schema-aware tools matter because the agent first needs to understand what kind of content exists and how it is shaped before it can ask useful questions of the data. citeturn545668search0
In Knowledge Base mode, the agent works with indexed entries instead of directly querying the live dataset. citeturn545668search0turn545668search3
The important workflow is:
User question
│
▼
Agent interprets intent
│
▼
Sanity Context
│
├── discover relevant content
├── follow structured relationships
├── retrieve source-backed facts
│
▼
Agent synthesizes answer
│
▼
Answer + evidence
A Question That Breaks Keyword Search
Here is the type of question Atlas is designed for:
"I am a first-year student interested in AI and frontend development. I don't want competitive coding events, I can only attend after 6 PM, and I want something that doesn't require prior club membership. What can I attend this week?"
Notice how many constraints are hidden inside one sentence.
The agent needs to combine:
| Constraint | Structured field |
|---|---|
| First-year | eligibility |
| AI | skills[] |
| Frontend |
skills[] / categories[]
|
| Avoid competitive coding | categories[] |
| After 6 PM | startAt |
| No membership required | eligibility[] |
| This week | event date |
A plain text search engine may retrieve relevant pages.
But it isn't naturally performing this structured filtering and relationship traversal.
That's why the content model is part of the product.
The Agent's Answer
Instead of dumping search results, Atlas is designed to return something closer to:
I found 3 opportunities that match your constraints.
1. Frontend Workshop
Friday • 6:30 PM
Web Development Club
Beginner-friendly
No club membership required
Main Lecture Hall
Why it matches:
✓ frontend
✓ after 6 PM
✓ beginner-friendly
✓ open to students
2. AI Build Night
Saturday • 7:00 PM
AI Club
...
3. Open Source Sprint
Sunday • 6:30 PM
Developer Community
...
I excluded 4 other events because they:
• started before 6 PM
• required prior membership
• were categorized as competitive programming
The final response is generated by the agent.
But the facts come from structured content.
🔍 The "Why?" Button
One feature I care about a lot is showing why the agent reached its conclusion.
For every recommendation, the UI can expose:
Matched because:
✓ skill = frontend
✓ audience = first-year
✓ beginnerFriendly = true
✓ startAt > 18:00
✓ registrationStatus = open
Source:
→ Frontend Workshop
→ Web Development Club
→ Main Lecture Hall
This makes the result inspectable.
Sanity Context is read-only and the connection is scoped by its configured sources and filters, which is useful for keeping the agent inside the content boundary I explicitly gave it. citeturn545668search10
🧠 Handling Conflicting Information
Another reason I chose Sanity Context instead of simply scraping pages at runtime is provenance.
The challenge specifically highlights that Knowledge Base entries remain linked to their source and that contradictory source claims can be surfaced together. citeturn545668view0
That matters on a campus.
Imagine:
Club page:
"Workshop starts at 5 PM"
Official event page:
"Workshop starts at 6 PM"
A generic chatbot might confidently choose one.
Atlas can treat those as conflicting source-backed claims and surface the discrepancy for resolution instead of silently inventing certainty.
That is a much safer interaction model for real-world content.
🛠️ Architecture
The production architecture is intentionally small:
┌─────────────────────┐
│ CampusLoop Atlas │
│ Web App │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ AI Agent │
│ │
│ intent + planning │
│ answer synthesis │
└──────────┬──────────┘
│
│ MCP
▼
┌─────────────────────┐
│ Sanity Context │
│ │
│ Knowledge Base │
└──────────┬──────────┘
│
indexed content
│
▼
┌─────────────────────┐
│ Sanity Content │
│ Lake │
└─────────────────────┘
The frontend is only the interface.
The interesting system is:
structured content
+
retrieval
+
agent reasoning
=
grounded campus answers
💻 The Data Model
The schema is deliberately relationship-heavy.
A simplified version:
defineType({
name: "event",
type: "document",
fields: [
defineField({
name: "title",
type: "string",
validation: Rule => Rule.required()
}),
defineField({
name: "startAt",
type: "datetime",
validation: Rule => Rule.required()
}),
defineField({
name: "organizer",
type: "reference",
to: [{ type: "club" }]
}),
defineField({
name: "venue",
type: "reference",
to: [{ type: "venue" }]
}),
defineField({
name: "skills",
type: "array",
of: [{ type: "string" }]
}),
defineField({
name: "eligibility",
type: "array",
of: [{ type: "string" }]
}),
defineField({
name: "beginnerFriendly",
type: "boolean"
})
]
})
The exact production schema is available in the repository linked below.
🔗 Why References Matter
Consider these two representations.
Flat content
HackQuest is hosted by ACM in Main Auditorium.
ACM is a technical club.
Main Auditorium is on Central Campus.
Structured content
HackQuest
├── organizer → ACM
└── venue → Main Auditorium
ACM
└── category → Technical
Main Auditorium
└── campusArea → Central Campus
Now a question like:
"Which technical events are happening in Central Campus?"
can be answered from relationships rather than semantic coincidence.
That is the difference between searching text and querying a content model.
🤖 Agent Loop
The agent roughly follows this pattern:
1. Understand the question
2. Identify constraints
3. Retrieve relevant entities
4. Traverse relationships
5. Filter candidates
6. Resolve conflicts
7. Generate an answer
8. Cite the supporting content
Pseudo-code:
def answer(question):
intent = parse_question(question)
candidates = sanity_context.retrieve(
entities=intent.entities,
constraints=intent.constraints
)
filtered = apply_constraints(
candidates,
intent.constraints
)
conflicts = detect_conflicts(filtered)
return synthesize(
question=question,
results=filtered,
conflicts=conflicts,
sources=get_sources(filtered)
)
The key point is that Sanity is the source of truth for the campus knowledge used by the agent.
🚦 Guardrails
I don't want a campus assistant that confidently fabricates event information.
So Atlas follows a few rules:
NO SOURCE
↓
Do not present the claim as campus fact.
CONFLICTING SOURCES
↓
Surface the conflict.
MISSING CONSTRAINT
↓
Ask a follow-up question.
STALE / PAST EVENT
↓
Label it clearly instead of presenting it as upcoming.
NO MATCH
↓
Say no matching opportunity was found.
This is especially important because Sanity Context itself is a read-only content interface; it exposes the content configured for the agent rather than letting the agent silently mutate the dataset. citeturn545668search1turn545668search10
🧪 How I Tested It
I created test questions in increasing levels of difficulty.
Level 1 — Direct lookup
"What is the next HackQuest event?"
Level 2 — Filtering
"Which events are happening after 6 PM?"
Level 3 — Multi-constraint
"Which AI events after 6 PM are beginner-friendly?"
Level 4 — Relationship reasoning
"Which beginner-friendly events are hosted by clubs
that teach web development?"
Level 5 — Real-world ambiguity
"Which event should I attend if I'm a first-year student,
want to learn frontend development, cannot attend before 6 PM,
and don't want competitive programming?"
Level 6 — Conflict handling
"Why does one source say 5 PM and another say 6 PM?"
The higher-level tests are the important ones.
A benchmark that only measures Level 1 is basically measuring search.
📈 What I Measure
I evaluate more than whether an answer "sounds right."
| Metric | What I check |
|---|---|
| Grounding | Are important claims supported by Sanity content? |
| Constraint accuracy | Did the answer respect every user constraint? |
| Relationship accuracy | Did it follow the right references? |
| Freshness | Did it distinguish upcoming from past content? |
| Conflict awareness | Did it surface contradictions? |
| Abstention | Did it avoid inventing an answer when data was missing? |
| Source coverage | Can I trace recommendations back to content? |
A useful agent should be correct because it found the right content, not merely because the language model produced a plausible sentence.
🔥 The coolest use case
The feature I ultimately want CampusLoop Atlas to become is a personal campus navigator.
Imagine opening the app at the beginning of a semester and asking:
"I have 6 hours free this week. I want to meet developers, learn React, attend one AI event, avoid anything before 5 PM, and I'd prefer events that are free."
Atlas could construct a schedule from structured campus content.
That is not a search result.
It is a plan built from a content graph.
And that is exactly why Sanity is useful here.
🎬 Demo
Live demo: [ADD YOUR DEPLOYED AGENT URL]
Recommended demo flow:
1. Ask a simple question.
2. Ask a multi-constraint question.
3. Open the "Why this answer?" evidence.
4. Ask a question whose answer depends on two or more linked documents.
5. Demonstrate a conflict or missing-data case.
6. Show the underlying Sanity content that produced the answer.
The sixth step is important.
I don't just want to show that the agent talks.
I want to show where the answer came from.
💻 Code
Repository: [ADD YOUR PUBLIC GITHUB REPOSITORY]
The repository contains:
/apps
/web
/agent
/sanity
/schemas
/seed
/lib
/context
/retrieval
/evaluation
/README.md
The README includes setup instructions, environment variables, the Sanity schema, and the agent integration.
🧩 How I Used Sanity Context
The key integration is the Sanity Context MCP endpoint.
The agent is configured to access the Sanity knowledge surface rather than being given a giant prompt containing campus data.
Conceptually:
Agent
│
│ MCP
▼
Sanity Context
│
├── initial context
├── schema / content understanding
├── retrieval
└── source-linked knowledge
│
▼
Sanity Knowledge Base
Sanity documents the Context MCP as a hosted, structured, read-only interface for agents. GROQ mode exposes schema and query tools, while Knowledge Base mode exposes indexed entries. citeturn545668search0turn545668search1
For the challenge, I use the Knowledge Base path because I want the agent to work from a curated campus corpus.
🗂️ Sanity Project Details
Sanity Project ID: [ADD YOUR SANITY PROJECT ID]
Public dataset / project link: [ADD PUBLIC DATASET URL, IF USED]
The Sanity workspace contains the structured content models used by CampusLoop Atlas:
club
event
person
venue
category
announcement
The important part of the model is not the number of document types.
It is the relationships between them.
🧾 Agent Session
Agent Session: [ADD PUBLIC DEV AGENT SESSION URL]
The session should show:
prompt
↓
agent planning
↓
Sanity Context calls
↓
retrieved content
↓
reasoning
↓
final grounded answer
Agent Sessions on DEV are unlisted by default, so the session must be made public for judges to access it. The challenge also recommends curating the useful portions and checking the transcript for secrets before publishing. citeturn545668view0
🧠 What I Learned
The biggest lesson was surprisingly simple:
Retrieval quality is partly a schema-design problem.
I started from the agent.
I ended up thinking much more about the data.
Questions such as:
"What events should I attend?"
became schema questions:
What is an event?
Who organizes it?
Who can attend?
What skills does it teach?
Where is it?
When does it happen?
What club does it belong to?
What else is related to it?
Once those relationships existed as structured data, the agent had something much stronger than a pile of documents.
It had a model of the campus.
⚔️ Why this is more than RAG
A lot of AI applications can be described as:
chunk text
↓
embed
↓
vector search
↓
LLM
That pattern is powerful.
But it doesn't automatically understand:
event → organizer → club → skill
event → venue → campus area
event → eligibility → student year
event → time → availability
CampusLoop Atlas is intentionally built around structured relationships plus retrieval.
The content model is part of the agent's intelligence.
🌐 What I Want to Build Next
The next version could add:
calendar-aware planning
+
personal preferences
+
real-time event state
+
notifications
+
agent-generated weekly plans
For example:
"Build me a campus week."
The response could become:
MON
6:00 PM — React Workshop
WED
7:00 PM — AI Build Night
SAT
5:30 PM — Open Source Meetup
Each recommendation would remain connected to its underlying Sanity content.
🏁 Final Takeaway
I didn't build CampusLoop Atlas because campus information needs another chatbot.
I built it because campus information is structured.
Clubs have members.
Events have organizers.
Events have venues.
Venues belong to areas.
Events have dates.
Events have eligibility.
Clubs have skills.
And students have preferences.
Those relationships are exactly what makes the problem interesting.
Sanity gives me a place to model those relationships.
Sanity Context gives the agent a controlled way to access them.
The agent turns those facts into an answer.
STRUCTURED CONTENT
↓
SANITY CONTEXT
↓
AGENT RETRIEVAL
↓
RELATIONSHIP REASONING
↓
GROUNDED ANSWER
The goal isn't to make an agent that knows everything.
It's to make an agent that knows where its answers came from.
🔗 Links
- Live Demo: https://campusloop.space/
- Repository: https://github.com/SH20RAJ/campusloop
- Sanity Project: ``
- Agent Session: N/A
- CampusLoop: https://campusloop.space/
Top comments (0)