Skip to content

Brand consistency agent ​

This page explains how a brand consistency agent reads your website, help center, social profiles, marketplace listings, or CMS drafts under a read-only grant, checks cross-channel consistency against a versioned glossary and claims rules, and returns a sourced diff list. After reading it, you will understand which modules this scenario needs, how the permission boundary narrows, and where terminology rules and patrol baselines each belong.

Use case ​

Marketing, content, and product teams need to check multiple pages before release or on a recurring basis for brand consistency. Brand language is spread across the website, social channels, store pages, and the help center, each maintained by a different team — terminology drift and stale claims are hard to catch manually, and differences keep accumulating after channel redesigns.

Typical triggers:

  • The brand glossary or claims wording changes, and existing language across all channels must be swept.
  • A recurring brand patrol needs consistency spot-checks on the website, social channels, and store pages.
  • A channel page is redesigned, and the changes must be verified against brand guidelines.

Engineering challenges ​

  • Channels are scattered and evolve independently: the website, social channels, store pages, and help center are maintained by different teams, terminology drifts gradually, and manual spot checks only see single-page snapshots — never the full cross-channel picture.
  • Diffs need evidence, not impressions: "this page's tone feels off" drives no rewrite; every diff must land on a concrete page, a concrete phrase, and the concrete rule it violates, or content owners have nothing to act on.
  • The consistency baseline itself keeps changing: the glossary and claims wording are continuously updated, so patrol conclusions must bind to a rule version and baselines must be archived by version — otherwise two patrol cycles cannot be compared, and no one can tell new drift from an old issue.

Module composition ​

ModuleRoleNotes
GenAuthCoreRead-only delegation, revocation, and the audit chain for CMS and channel back offices; recurring patrols run on freshly issued credentials per trigger.
Web AgentCoreControlled sessions extract text, CTAs, and visual context page by page with sources; key pages can be watched continuously with Track — change detection is driven by deterministic rules, not by model phrasing.
GUMemNot usedThe glossary and claims rules are versioned configuration — keep them in your policy store and inject them per version. Patrol baselines and diff conclusions are business state — archive them to your structured storage. This scenario has no user long-term preferences that belong in Memory.

Brand consistency agent architecture

Permission and delegation boundaries ​

The Agent holds no inherent permissions. The effective authority for each patrol task is the intersection of three sets: what the user actually holds ∩ what was explicitly delegated for this task ∩ what the enterprise has approved. Applied to this scenario:

  • The delegated scope covers only "read the selected sites, CMS collections, and channel pages" — no content editing, page publishing, or listing changes.
  • Delegation credentials are short-lived; minute-level validity is recommended for a single patrol run, and recurring watches run on freshly issued credentials per trigger.
  • The user or an administrator can revoke the grant at any time; new page-read requests fail immediately after revocation.
  • Out-of-scope attempts (for example, a workspace outside the delegated list) are rejected and recorded — the audit chain covers all attempts, not just successful actions.

Note: this sample requests doAnything product permissions. Your application and downstream services must configure and enforce business limits such as site lists, CMS collection ranges, action whitelists. Product scopes and prompt rules do not enforce those detailed limits; this sample does not configure them. See Delegate token and attenuation.

Workflow ​

Brand consistency agent workflow

  1. The user selects the sites, channels, or page lists to check, plus the patrol dimensions (terminology, tone, claims), and confirms the interactive authorization in Qoni Console.

  2. GenAuth issues a least-privilege, read-only delegation credential for this task.

  3. Your app loads the current version of the glossary, brand voice, and claims rules from the policy store and injects them into the task.

  4. Web Agent extracts text, CTAs, and visual context from each channel page, keeping source URLs and screenshots.

    Checkpoint: When a channel back office or CMS preview hits a login wall, CAPTCHA, or risk-control page, the Web Agent should escalate to a human instead of silently bypassing it.

  5. The Agent compares channel language against the rules, flagging terminology drift, tone deviations, and stale claims, each with a page source and the triggered rule ID.

  6. Your app validates the output contract — diffs missing a page source or rule ID are dropped — and archives this cycle's patrol baseline to your structured storage by policy version.

  7. For key pages that need ongoing watching, Track is configured to monitor future changes by deterministic rules.

  8. The Agent returns a diff list, rewrite suggestions, and a source list, with the policy version and an audit id.

    Checkpoint: Every inconsistency in the diff list should trace back to a concrete page source; conclusions without evidence should not enter the deliverable.

Example code ​

This example uses @qoniai/qoni 0.9.0, published on npm. The download includes the same SDK version, installed with npm ci. The demo compares the Firefox product page and Mozilla Manifesto using a synthetic brand rule. The SDK reads real public pages; business inputs in scenarios.ts are labeled public-demo.

This demo does not read or write GUMem; its task uses the supplied page list and explicit business inputs. DoAnything opens the supplied pages and produces the scenario output. Supply your own JSON with --input; use --interactive when the user must consent to delegation. For site sign-in and user responses, see Qoni SDK.

Download the complete runnable examples, or run from the documentation repository:

bash
cd examples/qoni
npm ci
npm run case -- brand-consistency-agent
# Supply your own inputs
npm run case -- brand-consistency-agent --input /path/to/input.json

Set server-side QONI_ACCESS_KEY and QONI_SECRET_KEY. QONI_USER_ID can identify your application's current GenAuth user; the local demo otherwise selects a user from the bound pool. Demonstration Memory writes use an isolated user rather than changing a business user's preferences.

This scenario's executable entry point:

ts
import { cliOptions } from '../runtime.js'
import { inputFile, runScenario } from '../run-case.js'

// Check multiple public pages against the same brand rules.
const report = await runScenario('brand-consistency-agent', cliOptions(), inputFile())
// report.items: pages, differences or observations, quotations, rule IDs, and sources.
// Validated fields: page, diff, quote, ruleId, sourceUrl.
console.log(JSON.stringify(report, null, 2))

The entry point loads the definition below by scenario ID. The code is included directly from scenarios.ts, with comments shown in the page language: the task, output fields, source field, Web Search queries (if any), whether Memory is used, and the demonstration input. The pipeline appends the input data, search sources, recalled Memory, and shared safety constraints to the task to build the final prompt; see the pipeline below for the full assembly.

ts
// Check multiple public pages against the same brand rules.
browser('brand-consistency-agent',
  'Compare product wording on the supplied pages against the approved policy. Return [{page,diff,quote,ruleId,sourceUrl}]. For every inspected page, diff must explain the observed difference or supported alignment in non-empty text; never return null. If no inconsistency is found, describe the alignment supported by quote. Capture and attach a screenshot as evidence. Do not edit or publish anything.',
  ['page','diff','quote','ruleId','sourceUrl'], 'sourceUrl', sample([firefox,manifesto], { rules:[{id:'evidence-and-voice',text:'Claims should be supported and use clear wording.'}] })),

The sample implements runScenario(), browser(), research(), and sample() as application functions. The pipeline below makes the actual SDK calls: delegation and introspection → required Memory/search → browser task or monitor → validation and saving. fields and sourceField define the application's output checks. The complete application helpers are in the package's runtime.ts.

Inspect the actual SDK pipeline
ts
// browser() uses DoAnything; research() searches first; sample() labels public-demo inputs.
const firefox = 'https://www.mozilla.org/en-US/firefox/new/'
const manifesto = 'https://www.mozilla.org/en-US/about/manifesto/'
const privacy = 'https://www.mozilla.org/en-US/privacy/firefox/'
const support = 'https://support.mozilla.org/en-US/kb/get-started-firefox-overview-main-features'
// These copy rules become prompt context; server permissions and business checks remain separate.
const policy = {
  version: 'demo-2026-09',
  approvedClaims: ['Describe only features supported by the cited page.'],
  forbiddenClaims: ['guaranteed security', '100% private', 'unverified pricing or performance'],
  voice: 'concise and warm',
}
const sample = (pages: string[], business: JsonObject = {}): JsonObject => ({
  dataset: 'public-demo', pages, policy, business,
  notice: 'Business records are synthetic demonstration inputs. Referenced websites and SDK execution are real.',
})
// fields lists required output keys; sourceField identifies URL checks; memory enables GUMem calls.
const browser = (id: string, task: string, fields: string[], sourceField: string | undefined, input: JsonObject, memory = false): Scenario => ({
  id, products: ['doAnything'], task, fields, sourceField, input, memory,
})
const research = (id: string, task: string, fields: string[], sourceField: string, queries: string[], input: JsonObject, memory = false): Scenario => ({
  id, products: ['webSearch', 'doAnything'], task, fields, sourceField, queries, input, memory,
})
ts
import { QoniScopes, type JsonObject, type RunResult } from '@qoniai/qoni'
import { readFileSync } from 'node:fs'
import { getScenario, type Scenario } from './scenarios.js'
import { appendTrace, checkInputCoverage, cleanupDemoUser, createContext, delegate, handleInteraction, inputEntryCount, isolateDemoUser, object, readWithRetry, renderScreenshot,
  save, saveArtifacts, searchHits, settled, settleRun, validateItems, withCleanup, type Context, type Options } from './runtime.js'

export async function runScenario(id: string, options: Options = {}, input?: JsonObject) {
  // Load the task definition by ID; --input replaces its business inputs.
  const scenario = getScenario(id)
  const data = input ?? scenario.input
  if (scenario.memory && data.dataset !== 'public-demo' && options.mode !== 'interactive' && !options.userId && !process.env.QONI_USER_ID) {
    throw new Error('Business Memory writes require the current QONI_USER_ID; do not select an arbitrary bound user')
  }
  const context = await createContext(id, { ...options,
    skipUserResolution: scenario.memory && data.dataset === 'public-demo' })
  return withCleanup(context, async register => {
    register('isolated demonstration user', () => cleanupDemoUser(context))
    // Isolate demo preferences; business Memory belongs to the identified current user.
    if (scenario.memory && data.dataset === 'public-demo') await isolateDemoUser(context)
    return await executeScenario(context, scenario, data)
  })
}

export async function executeScenario(context: Context, scenario: Scenario, input: JsonObject) {
  const pages = input.pages
  if (!Array.isArray(pages) || !pages.length || pages.some(page => typeof page !== 'string' || !/^https:\/\//.test(page))) {
    throw new Error('Input pages must be an array of HTTPS URLs')
  }
  if (input.requiresLogin === true && context.mode !== 'interactive') {
    throw new Error('Targets that require sign-in need --interactive and user-controlled login')
  }
  const memoryScopes = scenario.memory
    ? [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_MEMORY_WRITE, QoniScopes.GUMEM_MESSAGE_WRITE] : []
  // delegate() is an application helper around the SDK delegation methods.
  const grant = await delegate(context, scenario.id, scenario.products, memoryScopes)
  // Read the effective scopes; readWithRetry() retries only retryable read failures.
  const { data: tokenInfo } = await readWithRetry(context, 'delegation introspection',
    () => context.qoni.genauth.introspectDelegationToken({ token: grant.token }))
  const info = object(tokenInfo)
  if (info.active !== true) throw new Error('The delegation token is not active')
  const audit = { grantId: grant.grantId, auditId: grant.auditId, scopes: info.scope }
  let memory: unknown
  if (scenario.memory) {
    // A Session associates this conversation with the user; the app chooses sessionId.
    const sessionId = `${scenario.id}-${Date.now()}`
    await context.qoni.gumem.createSession({
      token: grant.token, userId: context.userId, sessionId, title: scenario.id,
    })
    const preferences = object(input.business ?? {}).confirmedPreferences
    if (Array.isArray(preferences) && preferences.length) {
      // Store confirmed preferences only; sync: true requests synchronous processing.
      await context.qoni.gumem.addMessages({ token: grant.token, userId: context.userId, sessionId, sync: true,
        messages: [{ role: 'user', content: `Confirmed demonstration preferences: ${preferences.join('; ')}` }] })
    }
    // Recall relevant preferences for the later task prompt.
    memory = (await readWithRetry(context, 'GUMem recall', () => context.qoni.gumem.recall({ token: grant.token, sessionId,
      query: 'Confirmed preferences relevant to this task', details: true }))).data
    save(context, 'memory.json', { sessionId, context: memory })
    if (Array.isArray(preferences) && preferences.length && !preferences.every(value => JSON.stringify(memory).includes(String(value)))) {
      throw new Error('Recall did not include the confirmed preferences just written by this demo')
    }
  }

  if (scenario.products.includes('track')) return runMonitor(context, scenario, input, grant.token, audit)

  let hits: ReturnType<typeof searchHits> = []
  if (scenario.queries) {
    // Web Search returns results[]; DoAnything receives these sources to inspect.
    const search = await context.qoni.webSearch.run({ token: grant.token, prompt: scenario.queries, maxResultsPerQuery: 3 })
    save(context, 'search-ref.json', { runId: search.id, audit })
    await withCleanup(context, async register => {
      register('Web Search run', () => search.cancel('Documentation demonstration cleanup'))
      const result = await settleRun(context, search)
      save(context, 'search-result.json', result)
      settled(result)
      hits = searchHits(result.output)
    })
  }

  // One-per-entry scenarios request exactly one item per input entry; others at most two.
  const requiredItems = inputEntryCount(scenario.id, input)
  // Assemble the task, inputs, search sources, and Memory as application-defined context.
  const prompt = [scenario.task, `Task inputs: ${JSON.stringify(input)}`,
    `Search sources: ${JSON.stringify(hits)}`, `Confirmed memory: ${JSON.stringify(memory ?? null)}`,
    `Actual collection time: ${new Date().toISOString()}`,
    requiredItems === undefined
      ? 'Inspect the supplied sources. Return at most two items in the requested JSON array, without prose or Markdown.'
      : `Inspect the supplied sources. Return exactly ${requiredItems} item${requiredItems === 1 ? '' : 's'} in the requested JSON array, one per input entry, without prose or Markdown.`,
    'Keep synthetic demonstration data identified as synthetic. Do not send messages, publish, pay, edit accounts or submit forms.',
    input.requiresLogin === true ? 'Request user sign-in through an interaction when required; never enter credentials yourself.' : 'Public demonstration sources only; do not sign in.',
  ].join('\n\n')
  // Start the Agent with this grant; capture receives delivered screenshots, not every step.
  const run = await context.qoni.doAnything.run({ token: grant.token, prompt, capture: { screenshots: true } })
  save(context, 'run-ref.json', { runId: run.id, session: run.sessionRef, audit })
  let result: RunResult
  const trace = (event: { type: string; data: unknown }) => {
    context.eventCounts[event.type] = (context.eventCounts[event.type] ?? 0) + 1
    if (['progress','message','done'].includes(event.type)) appendTrace(context, event)
    if (event.type === 'browserLiveUrlChanged') {
      const liveUrl = object(event.data).liveUrl
      if (typeof liveUrl === 'string') context.browserUrl = liveUrl
    }
  }
  return withCleanup(context, async register => {
    register('DoAnything run', () => run.cancel('Documentation demonstration cleanup'))
    if (context.delivery === 'events') {
      // --events streams updates; wrap interaction data in an SDK handle for user handling.
      for await (const event of run.events({ signal: AbortSignal.any([context.abort.signal, AbortSignal.timeout(context.timeoutMs)]) })) {
        trace(event)
        if (event.type === 'screenshot') renderScreenshot(context, event.image)
        if (event.type === 'interaction') await handleInteraction(context, run.interactionHandle(event.data))
      }
      result = await settleRun(context, run)
    } else {
      // Callback mode receives this run's events inside wait; helpers save images and ask the user.
      result = await settleRun(context, run, { onEvent: trace,
        onScreenshot: (image, index) => renderScreenshot(context, image, index),
        onInteraction: interaction => handleInteraction(context, interaction) })
    }
    save(context, 'result.json', result)
    settled(result)
    await saveArtifacts(context, result)
    if (scenario.id === 'landing-page-audit-agent' && context.screenshots === 0) throw new Error('The landing-page audit did not deliver the requested screenshot')
    // The app checks required fields and source URL formats; a reviewer still checks facts.
    const items = validateItems(result.output, scenario.fields, scenario.sourceField)
    // Scenarios that require one item per input entry are checked against the input.
    checkInputCoverage(scenario.id, items, input)
    const report = { scenario: scenario.id, dataset: input.dataset, passed: true, runId: run.id,
      status: result.status, items, audit, screenshots: context.screenshots, interactions: context.interactions,
      events: context.eventCounts, artifactIds: result.artifacts.map(artifact => artifact.id) }
    save(context, 'report.json', report)
    return report
  })
}

async function runMonitor(context: Context, scenario: Scenario, input: JsonObject, token: string, audit: JsonObject) {
  // Track creates a monitor with targets, extraction fields, and hourly scheduling.
  const monitor = await context.qoni.track.create({ token, prompt: scenario.task,
    targetUrls: input.pages, extractionSchema: { heading: 'string', source_url: 'string' },
    tickInstructions: `Open the target URLs and read the actual visible heading. Return a JSON object with heading and source_url. ${scenario.task}`,
    triggerDsl: { on: 'change' }, schedule: { kind: 'interval', intervalSeconds: 3600 } })
  save(context, 'monitor-ref.json', { id: monitor.id, audit })
  return withCleanup(context, async register => {
    register('Track monitor', () => monitor.delete())
    const definition = await monitor.get()
    save(context, 'monitor-definition.json', definition)
    if (object(definition.schedule).intervalSeconds !== 3600) throw new Error('Track did not persist the requested schedule interval')
    // Run one tick and inspect its extraction by runId; completed alone does not prove success.
    const tick = await monitor.runNow()
    save(context, 'tick.json', tick)
    if (tick.state !== 'completed') throw new Error(`Track execution failed: ${tick.state} / ${tick.error ?? ''}`)
    const runId = tick.runId
    if (typeof runId !== 'string') throw new Error('Track tick did not return a runId')
    const detail = await monitor.run(runId)
    save(context, 'tick-detail.json', detail)
    if (detail.state !== 'completed' || !detail.extracted || !Object.keys(object(detail.extracted)).length) {
      throw new Error('Track did not extract page data')
    }
    const extracted = object(detail.extracted)
    if (typeof extracted.heading !== 'string' || !extracted.heading.trim() ||
      typeof extracted.source_url !== 'string' || !/^https:\/\//.test(extracted.source_url)) {
      throw new Error('Track extraction is missing a heading or source URL')
    }
    const normalizeUrl = (value: string) => { const url = new URL(value); url.hash = ''; return url.href.replace(/\/$/, '') }
    if (!(input.pages as string[]).some(url => normalizeUrl(url) === normalizeUrl(String(extracted.source_url)))) {
      throw new Error('The Track source URL is not a configured target')
    }
    // Check persisted pause/resume state; withCleanup() deletes the monitor on exit.
    await monitor.pause()
    if ((await monitor.get()).status !== 'paused') throw new Error('Track did not persist the paused state')
    await monitor.resume()
    if ((await monitor.get()).status !== 'active') throw new Error('Track did not persist the active state')
    const report = { scenario: scenario.id, dataset: input.dataset, passed: true, monitorId: monitor.id,
      runId, state: detail.state, outcome: detail.outcome, extracted: detail.extracted, audit }
    save(context, 'report.json', report)
    return report
  })
}

export function inputFile(): JsonObject | undefined {
  const index = process.argv.indexOf('--input')
  return index >= 0 ? object(JSON.parse(readFileSync(process.argv[index + 1], 'utf8'))) : undefined
}

Results are written to output/brand-consistency-agent/report.json. report.items contains pages, differences or observations, quotations, rule IDs, and sources, with fields page, diff, quote, ruleId, sourceUrl. audit links the grant ID, audit ID, and effective scopes; http.json records redacted request statuses. diff must contain a non-empty explanation. If no difference is found, diff must describe the alignment, supported by quote, instead of returning null. The application parses DoAnything output and checks required fields and source URL formats. A business reviewer still assesses the content against the original sources.

Data and memory boundaries ​

This scenario touches four kinds of data; none of them needs GUMem:

  • Versioned rules: the glossary, brand voice, and claims rules — managed by version in your policy store, with every diff citing a rule version.
  • Business state: patrol baselines, diff lists, page screenshots — archived by policy version to your structured storage for cross-cycle comparison and traceability.
  • Audit records: the delegation and behavior chain formed by grantId and auditId — maintained by GenAuth.
  • User Memory (optional): this scenario neither recalls nor writes back by default; if a genuine cross-task user preference emerges later, evaluate GUMem then.

Failure handling ​

SituationRecommended handling
Channel back-office login state expiresSuspend the task, notify the user to sign in again, and resume from the checkpoint.
Page structure changes break extractionTreat it as a failure and replay the session recording; never emit diffs without evidence.
Request for a workspace outside the delegated scopeReject and record it; the attempted access remains visible in the audit chain.
A diff lacks a page source or rule IDApp-side validation drops the entry and the diff list notes how many were dropped.

Production notes ​

Do not overwrite published content automatically. Content owners should approve bulk changes. The Agent only produces diff lists and suggestions and never edits live content on any channel; diff lists should carry the policy version, and old baselines are not retroactively re-judged after the glossary changes. Patrol frequency against external channel pages should be capped to avoid load on those sites.

Next steps ​