Skip to main content

Your HR team fields the same PTO and benefits questions on repeat. You want a self-serve lookup tool answered from your real HR docs — but nobody on the team writes backend code, and you don't want to wait for an engineer to have time for it.

Not yet tested end-to-end on a live Replit account. The prompt below states the exact, corrected Chat API shape literally rather than describing it in prose, so an agent can't guess wrong — that part is verified. Whether Replit Agent actually produces a working app from it is not, since that's outside this cookbook's control.

A Replit account with Agent access
A Glean API token with the CHAT scope (kept in Replit Secrets)
1

Get your secrets ready before you start — Replit Agent will ask for them mid-build, and you want to paste them into Replit's Secrets tab, not the chat.

  • GLEAN_API_TOKEN — Admin Console → Platform → API Tokens → scope it to CHAT only.
  • GLEAN_INSTANCE — the <instance> part of your https://<instance>-be.glean.com URL.
2

Start a new Repl, open Agent, and paste this whole block — fill in your instance name first.

Build "Acme HR Assistant" — a single-page chat tool that answers employee
questions about PTO and benefits using the Glean Chat API. I don't want to
write or review implementation code; you own that. I do want to review the
running app and its use of secrets.

Stack: Node.js + Express backend, plain HTML/CSS/JS frontend (no framework
needed). The backend is the only thing that talks to Glean — the browser
never sees a Glean API token.

1. Scaffold an Express server with one route:
- GET / → serves a static index.html: a text input, a submit
button, an answer area, and a "Sources" list below the answer.
- POST /api/ask → body `{ "question": string }`, calls Glean, returns
`{ "answer": string, "citations": [{ "title": string, "url": string }] }`.

2. Install `@gleanwork/api-client` (pin the version — do not use a `^` or
`latest` range) and construct the client like this:

```ts
import { Glean } from '@gleanwork/api-client';

const glean = new Glean({
apiToken: process.env.GLEAN_API_TOKEN,
instance: process.env.GLEAN_INSTANCE, // e.g. "<your-glean-instance>"
});
```

Both `GLEAN_API_TOKEN` and `GLEAN_INSTANCE` must come from Replit
Secrets (the padlock icon in the sidebar), never hardcoded and never
sent to the browser. Stop and ask me for these two values by name
before running the app — do not invent placeholder values and move on.

3. Call the Chat API like this — the response shape is specific, follow it
exactly rather than guessing at field names:

```ts
export async function askGlean(question: string) {
const response = await glean.client.chat.create({
messages: [{ author: 'USER', fragments: [{ text: question }] }],
});

const contentMessages = (response.messages ?? []).filter(
(m) => m.messageType === 'CONTENT',
);
const fragments = contentMessages.flatMap((m) => m.fragments ?? []);

const answer = fragments.map((f) => f.text ?? '').join('');

const citations = fragments
.map((f) => f.citation?.sourceDocument)
.filter(
(doc): doc is NonNullable<typeof doc> => !!doc?.title && !!doc?.url,
);
const uniqueCitations = Array.from(
new Map(citations.map((doc) => [doc.url, doc])).values(),
);

return { answer, citations: uniqueCitations };
}
```

Notes on the response shape, since guessing at field names here is
easy to get wrong:
- The response can include earlier step-narration messages
(search/read progress) before the real answer — filter to
`messageType === 'CONTENT'` or that narration text ends up
prepended to the answer.
- Citations live per-fragment, in `fragment.citation.sourceDocument`
— not a top-level `citedDocuments` field, and not the older
`message.citations[]` field (deprecated, and not populated at all
on a live agentic response). Dedupe by `url` since the same source
is commonly cited by more than one fragment.

4. Frontend: on submit, POST the question to `/api/ask`, render `answer`
as text, and render each citation as a link using its `title` and
`url`. Show a loading state while waiting. Show the raw error message
if the request fails (this is an internal tool — don't hide errors
from me while I'm testing it).

5. Give the assistant a short system framing so it stays on-topic: it
should present itself as "Acme HR Assistant" and answer PTO/benefits
questions using only what Glean returns — don't have it add HR advice
Glean didn't cite.

6. When you're done, tell me the two things I need to test:
- Ask "What is our PTO policy?" and confirm the answer cites the PTO
policy document.
- Ask something outside HR entirely (e.g. "what's our revenue?") and
confirm the assistant doesn't fabricate an answer when Glean has
nothing relevant to cite.

Do not add authentication, a database, or user accounts — Glean already
enforces per-user permissions on the backend token's behalf for this demo,
and this is a single-tenant internal tool.

The prompt states the exact Chat API call rather than describing it in prose. Left to guess, an agent will confidently invent the wrong response shape — a top-level citedDocuments field is a common wrong guess. Pinning the real shape in the prompt itself is what makes this reliable.

3

Test with the two demo queries below, then try one off-topic question to confirm the assistant doesn't fabricate an answer when nothing's relevant to cite.

Take it further
Scope the CHAT token even further if your Glean plan supports per-collection tokens, so this tool can only ever answer from HR content.
Compare this to no-code-it-helpdesk-lovable — same Chat API call, same "browser never holds the token" constraint, different no-code tool and persona.
Once your team outgrows the no-code version, acme-answers shows the same pattern as a hand-written, version-controlled app.
Scaffold starter code

Copies a prompt your AI assistant can build from.

At a glance
SurfacesClient API
StatusShowcase
Time~45 min
Required scopes
CHAT