Skip to main content

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.

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']);

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.

src/stream.ts
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.

src/chat.ts
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);
}
Your CLIprompt and follow-up
TypeScript API clientcreateStream
Platform Chatpermission-aware response
EventStreamtyped createStream events
Node.js 22.12.0 or newer. The steps use npx and npm. Install Node from https://nodejs.org if needed.
A Glean instance with content indexed
Your work email, or the complete Glean backend HTTPS origin
A tenant that permits the public OAuth client and chat scope through DCR
1

Scaffold 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
2

Install dependencies

cd streaming-chat-with-citations && npm install
3

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
4

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>"
5

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>"
6

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.

HTTP 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.

Take it further
  • Render citation spans as links in a web interface using annotation start_index and end_index.
  • Persist conversation_id in an application session and resume it on the next request.
  • Add cancellation with AbortController when 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.

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 (chat).

At a glance
CapabilitiesChat
SurfacesPlatform API
StatusQuickstart
Time~20 min
Required scopes
CHAT