A first Chat integration needs more than a request and a string: it must preserve the user's permissions, continue a conversation, and consume streamed events correctly. This quickstart shows the modern Platform Chat API with the official TypeScript client's createStream EventStream.
The runnable scaffold uses glean.chat.createStream, the published Glean auth package, and the official API client. Every turn iterates a typed EventStream.
Configure the Platform API client
Resolve the backend from work email or an explicit server URL, then construct Glean with a refreshable OAuth token provider or GLEAN_API_TOKEN, plus includeExperimental.
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']);
export 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: ['chat'],
}),
includeExperimental: true,
} satisfies SDKOptions & XGleanOptions;
return new Glean(options);
}
Iterate createStream events
Call glean.chat.createStream and for-await the typed EventStream. Write RESPONSE_OUTPUT_TEXT_DELTA.data.delta and keep RESPONSE_COMPLETED.data.response for citations.
import type { Glean } from '@gleanwork/api-client';
import type { PlatformChatCompletedResponse } from '@gleanwork/api-client/models/components';
export interface ChatTurn {
completed?: PlatformChatCompletedResponse;
conversationId?: string;
text: string;
}
export async function streamTurn(
client: Glean,
input: string,
conversationId?: string,
): Promise<ChatTurn> {
const stream = await client.chat.createStream({
conversation_id: conversationId,
input,
store: true,
});
let text = '';
let completed: PlatformChatCompletedResponse | undefined;
for await (const event of stream) {
switch (event.event) {
case 'RESPONSE_OUTPUT_TEXT_DELTA':
process.stdout.write(event.data.delta);
text += event.data.delta;
break;
case 'RESPONSE_COMPLETED':
completed = event.data.response;
break;
case 'RESPONSE_FAILED':
throw new Error(event.data.response.error.message);
case 'RESPONSE_CREATED':
case 'RESPONSE_PROGRESS':
case 'RESPONSE_OUTPUT_TEXT_DONE':
break;
default: {
const _exhaustive: never = event;
void _exhaustive;
}
}
}
process.stdout.write('\n');
return {
completed,
conversationId: completed?.conversation_id ?? undefined,
text,
};
}
Continue a conversation and render citations
Store the first turn, pass its conversation_id to a follow-up, and render citation sources and snippets from the completed response.
import type { PlatformChatCompletedResponse } from '@gleanwork/api-client/models/components';
import { createGleanClient, type GleanClientTarget } from './client.js';
import { streamTurn } from './stream.js';
export interface ChatOptions extends GleanClientTarget {
followUp?: string;
prompt: string;
}
function printCitations(response: PlatformChatCompletedResponse) {
const citations = response.output.flatMap((message) =>
message.content.flatMap((content) => content.annotations ?? []),
);
if (citations.length === 0) return;
console.log('\nSources:');
for (const [index, citation] of citations.entries()) {
for (const source of citation.sources) {
const title =
'title' in source && typeof source.title === 'string'
? source.title
: undefined;
const url =
'url' in source && typeof source.url === 'string'
? source.url
: undefined;
console.log(` ${index + 1}. ${title ?? url ?? source.type}`);
if (url) console.log(` ${url}`);
}
for (const snippet of citation.snippets ?? []) {
console.log(` ${snippet.text}`);
}
}
}
export async function runChat({
email,
followUp,
prompt,
serverUrl,
}: ChatOptions) {
const client = await createGleanClient({ email, serverUrl });
const firstTurn = await streamTurn(client, prompt);
if (firstTurn.completed) printCitations(firstTurn.completed);
if (!followUp) return;
if (!firstTurn.conversationId) {
throw new Error(
'The first turn did not return a conversation_id; cannot continue the conversation.',
);
}
console.log('\nFollow-up:');
const followUpTurn = await streamTurn(
client,
followUp,
firstTurn.conversationId,
);
if (followUpTurn.completed) printCitations(followUpTurn.completed);
}
npx and npm. Install Node from https://nodejs.org if needed.chat scope through DCRScaffold the project
Copies the runnable TypeScript Chat 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 gleanwork/glean-cookbook/recipes/streaming-chat-with-citations streaming-chat-with-citations
Install dependencies
cd streaming-chat-with-citations && npm install
Run the fixture tests
Runs Vitest with MSW-backed fixtures, without credentials or live network access, covering typed createStream events, conversation_id propagation, and citations.
npm test
Sign in with OAuth
Discovers your Glean backend from work email and completes Authorization Code with PKCE for Chat and offline_access. Use --server-url for an explicit backend. If DCR is restricted, set GLEAN_OAUTH_CLIENT_ID for an administrator-provisioned public client. If OAuth is not available, set GLEAN_API_TOKEN later as a user-scoped fallback.
npm run login -- --email "<work-email>"
Stream one Chat turn
Sends a question through createStream, then prints delta text, conversation_id, and grounded citation data against your own instance.
npm run verify -- --email "<work-email>" --prompt "<chat-question>"
Stream a follow-up
Starts a stored conversation, iterates createStream events, and sends a follow-up using the returned conversation_id.
npm start -- --email "<work-email>" --prompt "<chat-question>" --follow-up "<follow-up-question>"
Platform Chat is experimental and may change. The scaffold opts in explicitly through the SDK constructor.
createStream is SDK-onlyHTTP clients request SSE by setting stream to true in the JSON body. SDK callers use createStream() instead of setting stream on create().
DCR may be disabled or restricted, or may not grant Chat. Use an administrator-provisioned public client when available; a user-scoped CHAT token is the tutorial fallback.
- Render citation spans as links in a web interface using annotation
start_indexandend_index. - Persist
conversation_idin an application session and resume it on the next request. - Add cancellation with
AbortControllerwhen the Chat interaction moves into a UI.
What's our PTO policy?
Returns a non-empty permission-aware streamed response with a conversation_id and at least one citation source when the user's indexed content contains a relevant policy.
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 (chat).