--- name: glean-sdk-patterns description: 'Apply production-ready Glean API patterns with typed clients, batch indexing, pagination, and error handling. Trigger: "glean SDK patterns", "glean best practices", "glean API client". ' allowed-tools: Read, Write, Edit version: 1.0.0 license: MIT author: Jeremy Longshore tags: - saas - enterprise-search - glean compatibility: Designed for Claude Code --- # Glean SDK Patterns ## Overview Production-ready patterns for the Glean enterprise search platform. Glean uses POST-based REST endpoints for both search and indexing. Search queries go to the Client API while document ingestion uses the Indexing API. A structured client centralizes token management, enforces batch pagination for bulk indexing, and provides typed responses for search results. ## Singleton Client ```typescript let _client: GleanClient | null = null; export function getClient(): GleanClient { if (!_client) { const domain = process.env.GLEAN_DOMAIN, key = process.env.GLEAN_API_KEY; if (!domain || !key) throw new Error('GLEAN_DOMAIN and GLEAN_API_KEY must be set'); _client = new GleanClient(domain, key); } return _client; } class GleanClient { private base: string; private h: Record; constructor(domain: string, key: string) { this.base = `https://${domain}/api`; this.h = { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' }; } async search(query: string, opts: { pageSize?: number; datasource?: string } = {}) { const r = await fetch(`${this.base}/client/v1/search`, { method: 'POST', headers: { ...this.h, 'X-Glean-Auth-Type': 'BEARER' }, body: JSON.stringify({ query, pageSize: opts.pageSize ?? 20, requestOptions: opts.datasource ? { datasourceFilter: opts.datasource } : undefined }) }); if (!r.ok) throw new GleanError(r.status, await r.text()); return r.json() as Promise; } async indexDocuments(datasource: string, docs: GleanDocument[]): Promise { const r = await fetch(`${this.base}/index/v1/indexdocuments`, { method: 'POST', headers: this.h, body: JSON.stringify({ datasource, documents: docs }) }); if (!r.ok) throw new GleanError(r.status, await r.text()); } async bulkIndex(ds: string, docs: GleanDocument[], batch = 100): Promise { for (let i = 0; i < docs.length; i += batch) await this.indexDocuments(ds, docs.slice(i, i + batch)); } } ``` ## Error Wrapper ```typescript export class GleanError extends Error { constructor(public status: number, message: string) { super(message); this.name = 'GleanError'; } } export async function safeCall(operation: string, fn: () => Promise): Promise { try { return await fn(); } catch (err: any) { if (err instanceof GleanError && err.status === 429) { await new Promise(r => setTimeout(r, 3000)); return fn(); } if (err instanceof GleanError && err.status === 401) throw new GleanError(401, 'Invalid GLEAN_API_KEY'); throw new GleanError(err.status ?? 0, `${operation} failed: ${err.message}`); } } ``` ## Request Builder ```typescript class GleanSearchBuilder { private body: Record = {}; query(q: string) { this.body.query = q; return this; } datasource(ds: string) { this.body.requestOptions = { datasourceFilter: ds }; return this; } pageSize(n: number) { this.body.pageSize = Math.min(n, 100); return this; } cursor(token: string) { this.body.cursor = token; return this; } facets(fields: string[]) { this.body.facetFilters = fields; return this; } build() { return this.body; } } // Usage: new GleanSearchBuilder().query('onboarding docs').datasource('confluence').pageSize(10).build(); ``` ## Response Types ```typescript interface GleanDocument { id: string; title: string; url: string; body: { mimeType: string; textContent: string }; author?: { email: string }; updatedAt?: string; } interface GleanSearchResponse { results: Array<{ document: GleanDocument; snippets: string[]; score: number }>; totalResults: number; cursor?: string; } interface GleanDatasource { name: string; displayName: string; documentCount: number; lastCrawledAt: string; } ``` ## Testing Utilities ```typescript export function mockDocument(o: Partial = {}): GleanDocument { return { id: 'doc-001', title: 'Onboarding Guide', url: 'https://wiki.example.com/onboarding', body: { mimeType: 'text/plain', textContent: 'Welcome to the team...' }, author: { email: 'hr@example.com' }, updatedAt: '2025-03-01T00:00:00Z', ...o }; } export function mockSearchResponse(n = 3): GleanSearchResponse { return { results: Array.from({ length: n }, (_, i) => ({ document: mockDocument({ id: `doc-${i}` }), snippets: ['...match...'], score: 0.95 - i * 0.1 })), totalResults: n }; } ``` ## Error Handling | Pattern | When to Use | Example | |---------|-------------|---------| | `safeCall` wrapper | All search and index calls | Structured error with operation context | | Retry on 429 | Bulk indexing pipelines | 3s delay before retry | | Batch pagination | Indexing > 100 documents | `bulkIndex` with batch tracking | | Auth validation | Client init | Fail fast on missing `GLEAN_API_KEY` | ## Resources - [Glean Developer Portal](https://developers.glean.com/) ## Next Steps Apply patterns in `glean-core-workflow-a`.