Skip to main content

Search filters differ across Glean instances and connected sources. Hard-coding a datasource or field from another environment makes a first integration brittle, while hand-writing HTTP hides the supported client developers should use. This quickstart uses the official TypeScript API client to discover what the signed-in user can filter, then carries that returned selection into a typed Search request that matches the reader's own instance and permissions.

The runnable scaffold uses the published Glean auth package for tenant discovery and refreshable OAuth credentials, gives that token provider to the official API client, and keeps the Search sequence explicit.

Configure the API client

Resolve the backend from work email or an explicit server URL, then give the Glean client the auth package's async token provider, a finite timeout per attempt, and bounded exponential backoff. A user-scoped SEARCH token remains an explicit fallback.

src/client.ts
import { Glean, type SDKOptions } from '@gleanwork/api-client';
import type { XGleanOptions } from '@gleanwork/api-client/hooks/x-glean-options.js';
import { createGleanTokenProvider, discoverGleanTenant } from '@gleanwork/auth';

const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);

interface GleanClientTarget {
email?: string;
serverUrl?: string;
}

async function resolveServerUrl({ email, serverUrl }: GleanClientTarget) {
const explicit = serverUrl?.trim();
if (explicit) return explicit;

const workEmail = email?.trim();
if (workEmail) return (await discoverGleanTenant(workEmail)).serverUrl;

const configured = process.env.GLEAN_SERVER_URL?.trim();
if (configured) return configured;

throw new Error(
'Pass --email or --server-url, or set GLEAN_SERVER_URL in your environment.',
);
}

export async function createGleanClient(target: GleanClientTarget) {
const serverURL = await resolveServerUrl(target);
const server = new URL(serverURL);
const loopback = LOOPBACK_HOSTS.has(server.hostname);
if (
(server.protocol !== 'https:' && !loopback) ||
server.username ||
server.password ||
server.search ||
server.hash ||
(server.pathname && server.pathname !== '/') ||
(!loopback && server.port)
) {
throw new Error('Use a complete Glean backend HTTPS origin.');
}

const staticToken = process.env.GLEAN_API_TOKEN?.trim();
const options = {
serverURL: server.origin,
apiToken:
staticToken ||
createGleanTokenProvider({
serverUrl: server.origin,
scopes: ['search'],
}),
includeExperimental: true,
timeoutMs: 30_000,
retryConfig: {
strategy: 'backoff',
backoff: {
initialInterval: 500,
maxInterval: 5_000,
exponent: 2,
maxElapsedTime: 90_000,
},
retryConnectionErrors: true,
},
} satisfies SDKOptions & XGleanOptions;

return new Glean(options);
}

Discover filters, then search

Search broadly by default, or list visible fields and request query-specific suggestions when you explicitly narrow the query to one datasource.

src/search.ts
import {
chooseDatasources,
chooseFilter,
createTerminal,
parseCliOptions,
printSearchResponse,
} from './cli.js';
import { createGleanClient } from './client.js';
import type { PlatformFilter } from '@gleanwork/api-client/models/components';
import { formatSdkError } from './errors.js';

/**
* Searches across all datasources by default. When a datasource list is
* supplied, discovers query-specific values for a single datasource and
* applies the selected filter.
*
* @remarks
* Filter discovery returns a best-effort catalog and may omit valid fields.
* Query-specific values are bounded suggestions, not guaranteed matches. The
* final Search request still enforces the caller's permissions.
*/
async function main() {
const cliOptions = parseCliOptions();
const glean = await createGleanClient({
email: cliOptions.email,
serverUrl: cliOptions.serverUrl,
});
const terminal = createTerminal();

try {
let datasources: string[] | undefined;
let filter: PlatformFilter | undefined = cliOptions.filter;

if (cliOptions.datasources || cliOptions.autoSelect || filter) {
const { result: catalog } = await glean.search.listFilters();
datasources = await chooseDatasources(
catalog.datasources,
cliOptions.datasources,
cliOptions.autoSelect,
terminal,
);

if (!filter && datasources.length === 1) {
const datasource = datasources[0];
const { result: suggestions } = await glean.search.listFilters(
datasources,
cliOptions.query,
);
const filterInfo = suggestions.datasources.find(
(candidate) => candidate.datasource === datasource,
);
if (!filterInfo) {
throw new Error(
`No filter metadata for "${datasource}" (request ${suggestions.request_id}).`,
);
}

filter = await chooseFilter(
filterInfo,
undefined,
cliOptions.autoSelect,
terminal,
);
}
}

const searchRequest = {
query: cliOptions.query,
page_size: 10,
...(datasources ? { datasources } : {}),
...(filter ? { filters: [filter] } : {}),
};
let cursor: string | undefined;
let resultOffset = 0;

for (let page = 1; page <= cliOptions.pages; page += 1) {
const searchResponse = await glean.search.query({
...searchRequest,
...(cursor ? { cursor } : {}),
});

printSearchResponse(
searchResponse,
datasources,
filter,
page,
resultOffset,
cliOptions.pages,
);
resultOffset += searchResponse.results.length;

if (!searchResponse.has_more) break;
if (!searchResponse.next_cursor) {
throw new Error('Search reported has_more without a next_cursor.');
}
if (searchResponse.next_cursor === cursor) {
throw new Error(
'Search returned the same cursor for consecutive pages.',
);
}
cursor = searchResponse.next_cursor;
}
} finally {
terminal?.close();
}
}

main().catch((error: unknown) => {
console.error(formatSdkError(error));
process.exitCode = 1;
});
Your CLIquery and filter choice
TypeScript API clienttyped requests and responses
Search Filtersvisible datasources and fields
Platform Searchpermission-aware results
Node 22.12.0 or newer
A Glean instance with content indexed
Your work email, or the complete Glean backend origin shown under Server instance (QE)
A tenant that permits this public OAuth client and search scope through DCR; an administrator-provisioned OAuth client or user-scoped SEARCH token is the fallback
Experimental Platform APIs enabled through the SDK's includeExperimental constructor option, which the scaffold sets automatically
1

Scaffold the project

Copies the runnable TypeScript Search CLI and fixture tests into a new directory. OAuth login and secure token storage come from the pinned @gleanwork/auth package.

npx -y tiged@2.12.8 --mode=git gleanwork/glean-cookbook/recipes/search-with-discovered-filters search-with-discovered-filters
2

Install dependencies

cd search-with-discovered-filters && npm install
3

Run the fixture tests

Runs the Vitest fixture suite without credentials or network access, covering catalog discovery, query-backed suggestions, retries, typed errors, field-filter propagation, and experimental headers.

npm test
4

Sign in with OAuth

Discovers your Glean backend from work email, completes Authorization Code with PKCE for search and offline_access, and stores refreshable credentials outside the project. Use --server-url for an explicit backend, GLEAN_OAUTH_CLIENT_ID for an administrator-provisioned public client, or GLEAN_API_TOKEN as a user-scoped fallback.

npm run login -- --email "<work-email>"
5

Verify against your instance

Searches your topic across all datasources and validates the Search response shape and pagination state. Add --pages 2 to fetch a second page, --datasources to narrow the query, or --auto-select to exercise the discovered datasource and suggested-filter path.

npm run verify -- --email "<work-email>" --query "<search-query>"
6

Discover filters and search

Runs the non-interactive CLI path, selecting the first returned datasource and suggested field value, then prints results, warnings, pagination state, and request IDs. Add --pages 2 to fetch the next result page. Omit --auto-select when running interactively to choose them yourself.

npm start -- --email "<work-email>" --query "<search-query>" --auto-select --pages 2

A field omitted from Search Filters may still be valid, and suggested values are bounded and non-exhaustive. Reusing a returned value does not guarantee matching results.

DCR may be disabled, restricted to approved applications or redirect URI patterns, or unable to grant search. Use an administrator-provisioned public OAuth client when available; a user-scoped SEARCH token is the tutorial fallback.

Both endpoints may change. The scaffold opts in explicitly and should be evaluated before production adoption.

Take it further
  • Use --pages to follow next_cursor through a bounded number of Search requests without treating the cursor as data or constructing it yourself.
  • Persist the catalog briefly for UI rendering, but refresh it and continue to treat it as best effort rather than an exhaustive schema.
  • Build a faceted search interface that requests query-specific values after the user chooses one datasource.
  • Add time_range alongside discovered filters for bounded recency searches.

Search for a topic you know exists in your Glean instance

Searches across all datasources by default and returns a valid permission-aware Search response without assuming a fixed result count. Explicit --datasources or --auto-select enables datasource and suggested-filter discovery.

View source

Runs the recipe through the Glean cookbook plugin.

Auth

Run the authenticate step on this page. It discovers your tenant from work email and signs you in with OAuth, using the shipped login command. If OAuth is unavailable, create a scoped Glean-issued token in Token Management (search).

At a glance
CapabilitiesSearch
SurfacesPlatform API
StatusQuickstart
Time~15 min
Required scopes
SEARCH