You shipped a system prompt to Redis, your agent's tone shifted overnight, and nobody can tell you what changed or who changed it. There's no diff, no review, no rollback. `redis-cli GET agent:prompt:v3` returns a 4KB string with zero history behind it, and the one person who edited it last Tuesday is on vacation. Prompt config is product behavior now, but you're managing it like a cache entry.
This is the gap Sanity Context fills. Sanity Context is Sanity's agent-facing product. Its primary surface today is Context MCP, a hosted, read-only MCP endpoint that exposes schema reads, GROQ queries, reference traversal, and optional semantic search across a Sanity dataset. Knowledge Bases is the second surface, for unstructured sources like PDFs and support docs. The point that matters here: the content your agent reads is versioned, reviewable, and editable by humans, with a real audit trail behind every change.
Redis is the right home for ephemeral per-user chat history. It is the wrong home for the instructions, brand voice, and approved answers that define how your agent behaves. This article draws that line, shows where each kind of state actually belongs, and walks through wiring durable, versioned agent config into your loop without turning your prompt into an untracked production secret.
Two kinds of state, and why developers keep conflating them
There are two completely different things hiding under the word "state" in an agent app, and the bugs start when you store both the same way.
The first kind is ephemeral session state: the running conversation, scratchpad reasoning, the last tool result, per-user counters. This is high-write, short-lived, and nobody needs to review it. It belongs in Redis or Upstash. A `SET conversation:{userId} ... EX 3600` is exactly right. You want it fast, you want it to expire, and you never want a human in the loop.
The second kind is editorial state: the system prompt, the routing instructions, the brand voice rules, the canned answers your support agent is allowed to give, the escalation policy. This stuff is low-write, long-lived, and high-stakes. When it changes, behavior changes for every user at once. It needs review before it ships, an author attached to every edit, and a way to roll back when the new prompt starts hallucinating refund policies that don't exist.
The failure mode is treating the second kind like the first. A `redis-cli SET agent:systemPrompt "..."` has no diff, no approval gate, and no history. When QA reports the agent went off-brand, you have a value and a vibe, not a changelog. You can bolt versioning onto Redis with key suffixes like `agent:prompt:v1`, `agent:prompt:v2`, but you've now reinvented a worse content management system inside a cache, with no review UI and no concept of who approved what. The instinct is right; the storage choice is wrong.
What "editable agent state" actually means at runtime
Editable agent state is the config your agent loads at the start of a turn, that a non-engineer changed since the last deploy. The two requirements that follow from "a non-engineer changed it" are the ones Redis can't give you: a review step before the change goes live, and a record of what the change was.
Concretely, this is the shape of state you want to externalize: the base system prompt, per-intent instruction blocks, few-shot examples, the list of approved responses for sensitive topics, and feature flags that gate tools the agent can call. In a typical loop you fetch these at turn start and assemble the final prompt. The naive version inlines them as TypeScript constants, which means every wording tweak is a code deploy and a PR review by someone who can't actually judge the copy.
The fix is to load them from somewhere editable, then cache the result. Here's the boundary drawn cleanly: ephemeral history stays in Redis, durable config comes from a content source. Note that the two reads serve different masters. The Redis read is per-user and you want it hot. The config read is global, changes rarely, and you cache it for a few minutes so a content edit propagates without a redeploy but doesn't hammer the source on every turn.
The state boundary in one turn
Per-user history from Redis, durable config from a versioned content source.
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
async function buildTurnContext(userId: string) {
// Ephemeral: per-user, hot, expires. This is Redis's job.
const history = await redis.lrange(`chat:${userId}`, 0, 20);
// Editorial: global, versioned, human-edited.
// Loaded from a content source, cached, NOT inlined as a const.
const config = await getAgentConfig(); // see next section
return {
history,
systemPrompt: config.systemPrompt,
approvedResponses: config.approvedResponses,
enabledTools: config.enabledTools,
};
}Versioning prompts by hand is a project you don't want to own
Once you accept that prompt config needs versioning, the temptation is to build it yourself. You add a `prompts` table in Postgres with a `version` column, an `is_active` flag, an `updated_by` field, and an audit table. Then someone asks for a preview environment so they can test the new prompt before it goes live, so you add a `status` enum. Then they want two prompt changes to ship together as one release, so you add a `release_id`. Then they want to see exactly what the diff was, so you store full snapshots and write a diff viewer.
You have now spent three sprints rebuilding the parts of a content management system that have existed for a decade. And you still don't have a decent editing UI, so the actual prompt authors, your support lead, your brand person, are pasting JSON into an admin panel and praying about escaping.
This is the moment to stop building and reach for a system that already models edit history, draft versus published state, staged releases, and a human editing surface as first-class primitives. Sanity gives you Content Releases for the "ship these two changes together" workflow, document history for the diff, and Roles and Permissions for who can approve a prompt change. The prompt becomes a document. The diff is built in. The author is recorded. The rollback is one click in the Studio, not a hand-written `UPDATE prompts SET is_active = true WHERE version = ...` you run nervously against production.
The hidden cost of DIY prompt versioning
Modeling agent config as content
If the prompt is a document, you model it like one. Define a schema for an agent configuration: a system prompt field, an array of intent-specific instruction blocks, a list of approved responses each with a topic and body, and references to which tools are enabled. Because it's a real schema, every field is typed, validated, and editable in the Studio with the right input widget, a rich text editor for the prompt body, a reference picker for tools, not a freeform JSON blob.
The payoff is that editing is safe and reviewable without engineering involvement. Your brand lead opens the document, edits the voice guidelines in a proper editor, and saves a draft. Nothing changes in production yet. The draft goes through review. When it's approved, it publishes, and your agent picks it up on the next config cache refresh. Every step has an author and a timestamp attached.
A second benefit shows up the moment you have more than one agent or environment. Approved responses and instruction blocks become referenceable documents you can share across agents, so a single "refund policy" answer is authored once and reused by the support agent, the email agent, and the chat widget. Change it in one place and every consumer gets the update on its next refresh. That's the difference between content modeled as a shared foundation and config copy-pasted into five Redis keys that quietly drift apart.
An agent config schema in Sanity Studio
import { defineType, defineField } from "sanity";
export const agentConfig = defineType({
name: "agentConfig",
title: "Agent configuration",
type: "document",
fields: [
defineField({
name: "key",
title: "Agent key",
type: "string",
validation: (r) => r.required(),
}),
defineField({
name: "systemPrompt",
title: "System prompt",
type: "text",
rows: 12,
validation: (r) => r.required(),
}),
defineField({
name: "approvedResponses",
title: "Approved responses",
type: "array",
of: [{ type: "reference", to: [{ type: "approvedResponse" }] }],
}),
defineField({
name: "enabledTools",
title: "Enabled tools",
type: "array",
of: [{ type: "string" }],
}),
],
});Reading config into the loop: MCP first, GROQ second
There are two ways to get this config to your agent, and which one you pick depends on how much control you want over the query.
The fastest way in is Context MCP. It's a hosted, read-only MCP endpoint you attach to your agent loop as an MCP server, and your agent gets schema-aware tools to read the dataset without you writing any query code. Because it's read-only, the agent can fetch config but can't mutate it through MCP, which is exactly the guarantee you want here: the runtime reads instructions, humans write them. Writes go through Agent Actions, not MCP.
The second path, for when you want full control over exactly what's fetched, is a thin tool or a direct client call running a typed GROQ query. This is the right choice for the config-load path specifically, because you know precisely which document you want and you want to cache the result. You fetch the active `agentConfig` by key, resolve the referenced approved responses in the same query, and cache the result for a few minutes. A content edit then propagates within your cache TTL with no redeploy. Note what's NOT happening here: no embeddings, no semantic search. This is structured retrieval, a keyed document lookup with reference resolution, which is the heavy majority of real agent reads. Semantic search is a deeper layer you reach for over unstructured corpora, not for loading a config document you can address by key.
Read-only by design
Fetch active config with a typed GROQ query
A keyed document read with reference resolution, cached for 60 seconds. Structured retrieval, no embeddings.
import { createClient } from "next-sanity";
const client = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: "production",
apiVersion: "2024-01-01",
useCdn: true,
});
const CONFIG_QUERY = `*[_type == "agentConfig" && key == $key][0]{
systemPrompt,
enabledTools,
"approvedResponses": approvedResponses[]->{ topic, body }
}`;
let cache: { value: unknown; at: number } | null = null;
export async function getAgentConfig() {
const fresh = !cache || Date.now() - cache.at > 60_000;
if (fresh) {
const value = await client.fetch(CONFIG_QUERY, { key: "support-agent" });
cache = { value, at: Date.now() };
}
return cache!.value as any;
}Staged rollout: ship a prompt change like you'd ship code
The last piece is deployment discipline. A prompt change is a behavior change, so it should go out the way good teams ship behavior changes: stage it, preview it, release it, and be able to roll it back.
With config modeled as content, you get this without building a deployment system. Edit the system prompt in a draft. The draft is invisible to production because your runtime query reads published documents. Preview the draft against a staging agent that's pointed at draft content. When it behaves, group it with any related changes into a Content Release so the prompt update, the new approved response, and the tool flag all flip together at one timestamp rather than dribbling out inconsistently. Publish the release. Your agents pick up the change on their next config cache refresh, no redeploy, no `redis-cli SET` run by hand against production.
If the new prompt misbehaves, you don't reverse-engineer what the old string was. Document history has every prior version with its author and timestamp; you restore the previous one and you're back. This is the whole argument for why prompt config belongs in a Content Operating System rather than a cache. Sanity is the AI Content Operating System, the intelligent backend for teams running content operations where editorial state, agent instructions included, needs to be governed, reviewed, and reversible. Redis keeps your sessions hot. Sanity Context keeps the instructions that define your agent's behavior versioned, auditable, and safe to change. Use each for the job it's actually good at.