Skip to content

Use Cached Query Templates

โฑ 20 minutes intermediate
๐Ÿ“œCoreGraph

Every standard GraphQL request sends the full query text to Graph. For complex queries, this means hundreds of bytes of query syntax on every API call. Saved query templates (also called persisted queries) let you register queries once and reference them by ID, reducing payload size, improving CDN cache hit rates, and preventing arbitrary queries from untrusted clients.

Write the query using variables for any dynamic values. Templates must be parameterized โ€” hardcoded filter values defeat the purpose.

Template-ready query
graphql
query GetArticleListing(
  $locale: Locales = en
  $category: String
  $limit: Int = 10
  $cursor: String
) {
  ArticlePage(
    locale: $locale
    where: {
      _metadata: { status: { eq: "Published" } }
      Category: { eq: $category }
    }
    orderBy: { _metadata: { published: DESC } }
    limit: $limit
    cursor: $cursor
  ) {
    items {
      Headline
      Author
      Summary
      Category
      _metadata {
        published
        url { default }
      }
    }
    total
    cursor
  }
}

Save the query template through the Graph admin API. Graph returns a unique hash that clients use to reference the template.

Register a saved query
bash
curl -X PUT \
  https://cg.optimizely.com/api/content/v3/saved-queries \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Basic {base64(AppKey:Secret)}' \
  -d '{
    "name": "GetArticleListing",
    "query": "query GetArticleListing($locale: Locales = en, $category: String, $limit: Int = 10, $cursor: String) { ArticlePage(locale: $locale, where: { _metadata: { status: { eq: \"Published\" } }, Category: { eq: $category } }, orderBy: { _metadata: { published: DESC } }, limit: $limit, cursor: $cursor) { items { Headline Author Summary Category _metadata { published url { default } } } total cursor } }"
  }'

# Response:
# {
#   "hash": "abc123def456789",
#   "name": "GetArticleListing"
# }

Replace the full query text with the persisted query hash in your API calls.

Execute a saved query
javascript
const SAVED_QUERY_HASH = 'abc123def456789';

async function getArticles(category, page) {
  const response = await fetch(GRAPH_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${SINGLE_KEY}`,
    },
    body: JSON.stringify({
      extensions: {
        persistedQuery: {
          version: 1,
          sha256Hash: SAVED_QUERY_HASH,
        },
      },
      variables: {
        category: category,
        limit: 10,
        cursor: page?.cursor || null,
      },
    }),
  });

  return response.json();
}
bash
curl -X POST \
  'https://cg.optimizely.com/content/v2' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {SingleKey}' \
  -d '{
    "extensions": {
      "persistedQuery": {
        "version": 1,
        "sha256Hash": "abc123def456789"
      }
    },
    "variables": {
      "category": "Technology",
      "limit": 10
    }
  }'

Treat saved query templates as part of your codebase. Store them alongside your frontend code and register them during deployment.

Template management script
javascript
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';

const QUERIES_DIR = './src/graphql/queries';
const GRAPH_ADMIN_URL =
  'https://cg.optimizely.com/api/content/v3/saved-queries';

async function registerTemplates() {
  const files = fs.readdirSync(QUERIES_DIR)
    .filter((f) => f.endsWith('.graphql'));

  for (const file of files) {
    const query = fs.readFileSync(
      path.join(QUERIES_DIR, file), 'utf-8'
    );
    const name = path.basename(file, '.graphql');

    const response = await fetch(GRAPH_ADMIN_URL, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Basic ${AUTH_TOKEN}`,
      },
      body: JSON.stringify({ name, query }),
    });

    const result = await response.json();
    console.log(`Registered ${name}: ${result.hash}`);
  }
}

registerTemplates();

Query the admin API to see all registered templates and remove outdated ones.

List saved queries
bash
# List all saved queries
curl -s \
  https://cg.optimizely.com/api/content/v3/saved-queries \
  -H 'Authorization: Basic {base64(AppKey:Secret)}'

# Delete a saved query by name
curl -X DELETE \
  https://cg.optimizely.com/api/content/v3/saved-queries/GetArticleListing \
  -H 'Authorization: Basic {base64(AppKey:Secret)}'

Saved queries can be invoked via HTTP GET, making them cache-friendly at the CDN layer. This is the fastest delivery pattern for public content.

GET-based template invocation
bash
# Invoke a saved query via GET for CDN caching
curl -G 'https://cg.optimizely.com/content/v2' \
  --data-urlencode 'extensions={"persistedQuery":{"version":1,"sha256Hash":"abc123def456789"}}' \
  --data-urlencode 'variables={"category":"Technology","limit":10}' \
  -H 'Authorization: Bearer {SingleKey}'

GET requests produce deterministic URLs that CDN edge nodes cache efficiently. Use this approach for high-traffic listing pages and landing pages.

IssueCauseResolution
PersistedQueryNotFound errorTemplate not registeredRegister the template via the admin API before using it
Stale results after content updateCDN cache not invalidatedWait for Graph webhooks to trigger cache purge
Variables not applyingMalformed JSON in variablesValidate the variables JSON structure matches the query parameters
Hash mismatchQuery text changed after registrationRe-register the template and update the hash in your frontend
  • Use variables, not string concatenation โ€” Always parameterize dynamic values. Concatenating values into query strings defeats caching.
  • Register templates in CI/CD โ€” Automate template registration as part of your deployment pipeline to keep templates in sync with your frontend code.
  • Version template names โ€” When making breaking changes to a template, create a new versioned name (e.g., GetArticleListing_v2) and deploy the new frontend before removing the old template.
  • Audit unused templates โ€” Periodically review registered templates and remove those no longer referenced by any deployed application.
  • Prefer GET for public content โ€” Use GET-based invocations for SingleKey-authenticated queries to maximize CDN cache hit rates.