{
  "schema_version": 2,
  "id": "develop/ai/context-engine/context-gateway/manage-context-surfaces",
  "title": "Manage Context Surfaces",
  "url": "https://redis.io/docs/latest/develop/ai/context-engine/context-gateway/manage-context-surfaces/",
  "summary": "Create a Context Surface to expose Redis records as read-only tools, then update its data model, refresh its tools, or delete it.",
  "tags": [
    "docs",
    "develop",
    "ai"
  ],
  "last_updated": "2026-11-02T17:45:57-08:00",
  "page_type": "content",
  "content_hash": "b6f6f97de382e4c3d1537b3e29c2885d0f7d84faaf55e17cbddb083476aab301",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "Make records stored in Redis available to agents through read-only tools.\n\nA Context Surface is an Upstream that generates tools from a data model describing your records, searchable fields, and relationships. Agents call these tools through the Gateway's Model Context Protocol (MCP) endpoint.\n\nCreating a Surface does not import records. Load your records into the source database first.\n\n[code example]"
    },
    {
      "id": "before-you-begin",
      "title": "Before you begin",
      "role": "content",
      "text": "The examples use a self-managed deployment. You need:\n\n- A Gateway, its administration URL, and an admin token. See [Manage Gateways and Upstreams](https://redis.io/docs/latest/develop/ai/context-engine/context-gateway/manage-gateways#before-you-begin).\n- A registered source database and its ID.\n- Records that match your data model, loaded into that database.\n- The Iris command-line interface (CLI), installed from the Python `redis-iris` package. See [SDK and CLI setup](https://redis.io/docs/latest/develop/ai/context-engine/sdk-and-cli).\n- `jq`, plus cURL for the HTTP examples.\n\nSet the connection values:\n\n[code example]"
    },
    {
      "id": "choose-a-source-database",
      "title": "Choose a source database",
      "role": "content",
      "text": "Your administrator registers the source database and gives the Control Plane and Data Plane each a Redis user for it. You supply the registry ID when creating a Surface, without an endpoint or credentials. See [Context Surface databases](https://redis.io/docs/latest/operate/iris/context-gateway/self-managed/configuration#context-surface-databases).\n\nCheck these access boundaries before choosing a database:\n\n| Boundary | What it means for your application |\n| --- | --- |\n| Gateway access | Every agent with access to a Gateway can read every record of every Surface on it. |\n| Access tags | Context Surfaces do not narrow results by access tags. |\n| Shared source database | Gateways whose Surfaces share a database can read each other's records. Use separate databases when records must stay separate. |\n| Gateway storage | The source database must be separate from the Gateway database used for cache and compression. Never use one Redis database for both. |\n\nThe examples supply `contextSurface.databaseId` explicitly. Database selection follows these rules:\n\n| Value | Result |\n| --- | --- |\n| Explicit database ID | Uses that database, overriding any default. |\n| Omitted on self-managed with a default | Uses the configured default Surface database. |\n| Omitted on self-managed without a default | Returns `400` for `contextSurface.databaseId`. |\n| Empty string or `null` | Returns `400` for `contextSurface.databaseId`. |\n| Omitted on Redis Cloud | Returns an error. Redis Cloud requires an explicit ID. |\n\nThe response includes the resolved ID in `contextSurface.databaseBinding.databaseId` and the database name in `contextSurface.databaseBinding.name`. The name is absent if its lookup fails. See [Default databases](https://redis.io/docs/latest/operate/iris/context-gateway/self-managed/configuration#default-databases)."
    },
    {
      "id": "describe-your-records",
      "title": "Describe your records",
      "role": "content",
      "text": "A data model tells the Gateway which records to expose and how agents can query them.\n\n| Model element | Purpose |\n| --- | --- |\n| Entity | A type of record, such as a provider or location. |\n| Redis key template | Identifies the keys that store an entity's records. |\n| Key component | A field used to construct the record's key for lookup by ID. |\n| TAG index | Makes a field available for filtering, such as providers by specialty. |\n| TEXT index | Makes a field available for text search, such as locations by name. |\n| Relationship | Links an entity to another through a field, such as a provider's home location. |\n\nThe clinic model has two entities: `Location` and `Provider`. Providers have a specialty and a home location. Locations have a name and city."
    },
    {
      "id": "write-a-data-model",
      "title": "Write the data model",
      "role": "content",
      "text": "Save this model as `clinic-model.json`:\n\n[code example]"
    },
    {
      "id": "check-model-limits",
      "title": "Check model limits",
      "role": "content",
      "text": "| Limit | What to do |\n| --- | --- |\n| At most 1 mebibyte (MiB) of JSON | Keep the model within this size. |\n| One Redis index per field | To search the same value with both TAG and TEXT, store it in two fields and give each field one index. |\n| No `acl_field_mappings` | Omit this unsupported field. The Control Plane rejects models that include it. |"
    },
    {
      "id": "prepare-text-values",
      "title": "Prepare text values",
      "role": "content",
      "text": "Normalize line breaks to spaces in TEXT-indexed values before loading records. Redis TEXT tokenization does not reliably turn every line-break form into a word boundary. Include CRLF, LF, CR, vertical tab, form feed, U+0085, U+2028, and U+2029 in your normalization.\n\nRewrite existing records with normalized values. Refreshing the catalog or rebuilding an index alone does not change stored text.\n\nTest representative queries against your indexed data. Do not assume punctuation-containing phrases have literal phrase-search semantics."
    },
    {
      "id": "load-the-records",
      "title": "Load the records",
      "role": "content",
      "text": "The [self-managed API examples](https://redis.io/docs/latest/operate/iris/context-gateway/self-managed/api-examples) show how to load the clinic records used by the quickstart. If you also created the Surface there, make sure the connection values in [Before you begin](#before-you-begin) identify that same Gateway. From the directory containing the setup response files, load its Surface ID:\n\n[code example]\n\nThen skip to [Verify the generated tools](#verify-the-generated-tools)."
    },
    {
      "id": "create-a-context-surface",
      "title": "Create a Context Surface",
      "role": "content",
      "text": "Create an Upstream of kind `CONTEXT_SURFACE` with the database ID and data model. The `scheduling` namespace identifies its tools in the Gateway catalog.\n\n**CLI:**\n\n[code example]\n\n**HTTP:**\n\n[code example]"
    },
    {
      "id": "check-the-creation-result",
      "title": "Check the creation result",
      "role": "content",
      "text": "Registration, tool publication, and indexing are separate outcomes. The Control Plane generates and publishes tools during the create request. If generation fails, registration still succeeds with HTTP `201`, and `initialCatalogFailure` explains the problem:\n\n| `initialCatalogFailure.status` | Meaning | What to do |\n| --- | --- | --- |\n| `422` | The Surface database refused the login. | Ask your administrator to fix the registry login, then refresh. |\n| `424` | The Surface database was unreachable, answered an error, or refused a command. | Ask your administrator to check the database and its Redis users, then refresh. |\n| `408` | Tool generation timed out. | Refresh. |\n| `423` | The Gateway or its tenant was suspended. | Refresh once the suspension ends. |\n\nA `400` response identifies invalid members in `errors[]`. A body member uses a JSON Pointer in `pointer`, such as `/contextSurface/dataModel`, and a human-readable `detail`. The response does not echo rejected credentials or database addresses. Nothing was written.\n\nTools can be published while background indexing is still running. Search results can be incomplete until indexing finishes."
    },
    {
      "id": "verify-the-generated-tools",
      "title": "Verify the generated tools",
      "role": "content",
      "text": "<a id=\"list-the-generated-tools\"></a>\n\nList the Surface's tool catalog:\n\n**CLI:**\n\n[code example]\n\n**HTTP:**\n\n[code example]\n\n\n\nEvery generated tool is read-only. Agents see tools under the Surface's namespace, for example `scheduling__filter_provider`.\n\nUse MCP discovery to inspect each tool's input schema before calling it, as shown in the [quickstart](https://redis.io/docs/latest/develop/ai/context-engine/context-gateway/quickstart).\n\n| Agent task | Model support |\n| --- | --- |\n| Filter providers by specialty or location | TAG indexes on `Provider.specialty` and `Provider.location_id`. |\n| Search location names | TEXT index on `Location.name`. |\n| Filter locations by city | TAG index on `Location.city`. |\n| Look up a record by ID | Redis key template and key component. |\n\nThe Provider model has no TEXT-indexed field, so it supports filtering rather than text search. A lookup for an ID that does not exist succeeds and returns `null`."
    },
    {
      "id": "allow-indexing-to-finish",
      "title": "Allow indexing to finish",
      "role": "content",
      "text": "Creating a Surface, updating its model, or refreshing it starts background indexing. A published tool catalog does not mean indexing is complete. Wait for indexing to finish before checking result completeness. If results remain incomplete, ask your administrator to inspect the source database's indexes."
    },
    {
      "id": "change-the-data-model-or-refresh",
      "title": "Update the data model",
      "role": "content",
      "text": "Edit `clinic-model.json`, then submit the complete replacement model. The CLI reads the current revision for the update. With HTTP, read it yourself and send it as `expectedRevision`.\n\n**CLI:**\n\n[code example]\n\n**HTTP:**\n\n[code example]\n\n\n\nThe model and generated tools commit together. Subsequent discovery returns the new tools, although indexing can still be underway.\n\nA stale revision returns `412`. Read the resource again and reconcile your change before retrying."
    },
    {
      "id": "refresh-the-tools",
      "title": "Refresh the tools",
      "role": "content",
      "text": "Refresh a Surface to generate tools from its stored model and recreate missing indexes.\n\n| Change or problem | Action |\n| --- | --- |\n| New entities, fields, indexes, or relationships | Update the data model. |\n| Initial tool generation failed | Fix the reported problem, then refresh. |\n| A database index is missing | Refresh to recreate it. |\n| Stored text contains line breaks | Rewrite the values. A refresh alone does not normalize them. |\n\n**CLI:**\n\n[code example]\n\n**HTTP:**\n\n[code example]\n\n\n\nA tool call during a model update or refresh can fail with `upstream_error` while its index is replaced. Retry the read-only call after the operation finishes.\n\n[Verify the catalog](#verify-the-generated-tools) and allow indexing to finish before checking results."
    },
    {
      "id": "delete-a-context-surface",
      "title": "Delete a Context Surface",
      "role": "content",
      "text": "Deleting a Surface removes its registration and drops its indexes from the source database. The CLI reads the current revision and asks for confirmation. With HTTP, read the revision before deleting:\n\n**CLI:**\n\n[code example]\n\n**HTTP:**\n\n[code example]"
    },
    {
      "id": "if-index-cleanup-fails",
      "title": "If index cleanup fails",
      "role": "content",
      "text": "If deletion repeatedly returns `503` because indexes cannot be dropped, remove the registration with the index-cleanup override. This leaves the indexes in the database. Ask your administrator to remove them afterward.\n\n**CLI:**\n\n[code example]\n\n**HTTP:**\n\n[code example]"
    },
    {
      "id": "next-steps",
      "title": "Next steps",
      "role": "content",
      "text": "- [Call a tool through the Gateway](https://redis.io/docs/latest/develop/ai/context-engine/context-gateway/quickstart).\n- [Manage Gateways and Upstreams](https://redis.io/docs/latest/develop/ai/context-engine/context-gateway/manage-gateways).\n- [Connect an agent](https://redis.io/docs/latest/develop/ai/context-engine/context-gateway/agent-integration).\n- [Review limits and troubleshooting](https://redis.io/docs/latest/develop/ai/context-engine/context-gateway/limits)."
    }
  ],
  "examples": [
    {
      "id": "overview-ex0",
      "language": "mermaid {background=\"transparent\"}",
      "code": "---\nconfig:\n  look: neo\n---\nflowchart LR\n    Records[(\"Records in Redis\")] --> Surface[\"Context Surface\"]\n    Model[\"Data model\"] -->|Defines entities, fields, and relationships| Surface\n    Surface --> Tools[\"Generated read-only tools\"]\n    Tools --> Gateway[\"Gateway MCP endpoint\"]\n    Gateway --> Agent[\"Agent\"]",
      "section_id": "overview"
    },
    {
      "id": "before-you-begin-ex0",
      "language": "bash",
      "code": "export IRIS_CONTEXT_GATEWAY_SERVICE_URL='<administration-url>'\nexport IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN='<admin-token>'\nexport IRIS_CONTEXT_GATEWAY_ID='<gateway-id>'\nexport SURFACE_DATABASE_ID='<surface-database-id>'\nexport SURFACE_GATEWAY_API=\"$IRIS_CONTEXT_GATEWAY_SERVICE_URL/iris/v1/context-gateway/gateways/$IRIS_CONTEXT_GATEWAY_ID\"",
      "section_id": "before-you-begin"
    },
    {
      "id": "write-a-data-model-ex0",
      "language": "json",
      "code": "{\n  \"title\": \"Healthcare scheduling\",\n  \"description\": \"Clinics and the providers who work at them\",\n  \"entities\": [\n    {\n      \"name\": \"Location\",\n      \"description\": \"A medical facility where providers see patients\",\n      \"redis_key_template\": \"location:{id}\",\n      \"fields\": [\n        {\"name\": \"id\", \"type\": \"str\", \"description\": \"Location ID\", \"is_key_component\": true, \"redis_indices\": []},\n        {\"name\": \"name\", \"type\": \"str\", \"description\": \"Facility name\", \"redis_indices\": [{\"type\": \"text\", \"weight\": 1.0}]},\n        {\"name\": \"city\", \"type\": \"str\", \"description\": \"City\", \"redis_indices\": [{\"type\": \"tag\"}]}\n      ],\n      \"relationships\": []\n    },\n    {\n      \"name\": \"Provider\",\n      \"description\": \"A clinician who sees patients at one location\",\n      \"redis_key_template\": \"provider:{id}\",\n      \"fields\": [\n        {\"name\": \"id\", \"type\": \"str\", \"description\": \"Provider ID\", \"is_key_component\": true, \"redis_indices\": []},\n        {\"name\": \"specialty\", \"type\": \"str\", \"description\": \"Specialty\", \"redis_indices\": [{\"type\": \"tag\"}]},\n        {\"name\": \"location_id\", \"type\": \"str\", \"description\": \"Home location\", \"redis_indices\": [{\"type\": \"tag\"}]}\n      ],\n      \"relationships\": [\n        {\"name\": \"works_at\", \"target\": \"Location\", \"description\": \"The provider's home location\", \"source_field\": \"location_id\"}\n      ]\n    }\n  ],\n  \"entity_count\": 2\n}",
      "section_id": "write-a-data-model"
    },
    {
      "id": "load-the-records-ex0",
      "language": "bash",
      "code": "export SURFACE_ID=\"$(jq -er '.upstreamId' \"surface-$IRIS_CONTEXT_GATEWAY_ID.json\")\"",
      "section_id": "load-the-records"
    },
    {
      "id": "create-a-context-surface-ex0",
      "language": "bash",
      "code": "iris --json context-gateway upstream add \\\n  --kind context-surface --name 'Healthcare scheduling' --namespace scheduling \\\n  --surface-database-id \"$SURFACE_DATABASE_ID\" --data-model clinic-model.json > surface.json\n\nexport SURFACE_ID=\"$(jq -er '.upstreamId' surface.json)\"\njq '{upstreamId, revision, initialCatalogFailure}' surface.json",
      "section_id": "create-a-context-surface"
    },
    {
      "id": "create-a-context-surface-ex1",
      "language": "bash",
      "code": "jq -n --arg database \"$SURFACE_DATABASE_ID\" --slurpfile model clinic-model.json \\\n  '{name: \"Healthcare scheduling\", namespace: \"scheduling\", kind: \"CONTEXT_SURFACE\",\n    contextSurface: {databaseId: $database, dataModel: $model[0]}}' > surface-create.json\n\ncurl --fail-with-body --silent --show-error \\\n  -X POST \"$SURFACE_GATEWAY_API/upstreams\" \\\n  -H \"Authorization: Bearer $IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  --data-binary @surface-create.json > surface.json\nexport SURFACE_ID=\"$(jq -er '.upstreamId' surface.json)\"\njq '{upstreamId, revision, initialCatalogFailure}' surface.json",
      "section_id": "create-a-context-surface"
    },
    {
      "id": "verify-the-generated-tools-ex0",
      "language": "bash",
      "code": "iris context-gateway upstream tools \"$SURFACE_ID\"",
      "section_id": "verify-the-generated-tools"
    },
    {
      "id": "verify-the-generated-tools-ex1",
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  \"$SURFACE_GATEWAY_API/upstreams/$SURFACE_ID/tools\" \\\n  -H \"Authorization: Bearer $IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN\" | jq .",
      "section_id": "verify-the-generated-tools"
    },
    {
      "id": "change-the-data-model-or-refresh-ex0",
      "language": "bash",
      "code": "iris context-gateway upstream update \"$SURFACE_ID\" --data-model clinic-model.json",
      "section_id": "change-the-data-model-or-refresh"
    },
    {
      "id": "change-the-data-model-or-refresh-ex1",
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  \"$SURFACE_GATEWAY_API/upstreams/$SURFACE_ID\" \\\n  -H \"Authorization: Bearer $IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN\" > surface.json\n\njq -n --arg revision \"$(jq -er '.revision' surface.json)\" \\\n  --slurpfile model clinic-model.json \\\n  '{expectedRevision: $revision, contextSurface: {dataModel: $model[0]}}' > surface-update.json\n\ncurl --fail-with-body --silent --show-error \\\n  -X PATCH \"$SURFACE_GATEWAY_API/upstreams/$SURFACE_ID\" \\\n  -H \"Authorization: Bearer $IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  --data-binary @surface-update.json > surface.json",
      "section_id": "change-the-data-model-or-refresh"
    },
    {
      "id": "refresh-the-tools-ex0",
      "language": "bash",
      "code": "iris context-gateway upstream refresh \"$SURFACE_ID\"",
      "section_id": "refresh-the-tools"
    },
    {
      "id": "refresh-the-tools-ex1",
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  -X POST \"$SURFACE_GATEWAY_API/upstreams/$SURFACE_ID/refresh\" \\\n  -H \"Authorization: Bearer $IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN\" \\\n  > surface-refresh-response.json",
      "section_id": "refresh-the-tools"
    },
    {
      "id": "delete-a-context-surface-ex0",
      "language": "bash",
      "code": "iris context-gateway upstream delete \"$SURFACE_ID\"",
      "section_id": "delete-a-context-surface"
    },
    {
      "id": "delete-a-context-surface-ex1",
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  \"$SURFACE_GATEWAY_API/upstreams/$SURFACE_ID\" \\\n  -H \"Authorization: Bearer $IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN\" > surface.json\n\ncurl --fail-with-body --silent --show-error \\\n  -X DELETE \"$SURFACE_GATEWAY_API/upstreams/$SURFACE_ID?expectedRevision=$(jq -er '.revision | @uri' surface.json)\" \\\n  -H \"Authorization: Bearer $IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN\"",
      "section_id": "delete-a-context-surface"
    },
    {
      "id": "if-index-cleanup-fails-ex0",
      "language": "bash",
      "code": "iris context-gateway upstream delete \"$SURFACE_ID\" --ignore-index-sweep-failure",
      "section_id": "if-index-cleanup-fails"
    },
    {
      "id": "if-index-cleanup-fails-ex1",
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  -X DELETE \"$SURFACE_GATEWAY_API/upstreams/$SURFACE_ID?expectedRevision=$(jq -er '.revision | @uri' surface.json)&ignoreIndexSweepFailure=true\" \\\n  -H \"Authorization: Bearer $IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN\"",
      "section_id": "if-index-cleanup-fails"
    }
  ]
}
