A support issue arrives with a URL or a short description, but the first response still requires someone to reconstruct the evidence, identify the next diagnostic, and write a safe customer update.
The runnable CLI resolves a refreshable Glean credential, builds an explicit USER prompt, calls the Platform Agents run endpoint, and prints only the Agent's structured response.
Build an evidence-first triage prompt
Keep the issue input explicit and require the Agent to separate observed evidence from hypotheses before producing a customer-safe response.
#!/usr/bin/env node
import 'dotenv/config';
import { spawn } from 'node:child_process';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createGleanTokenProvider } from '@gleanwork/auth';
export function requiredEnv(name, env = process.env) {
const value = env[name]?.trim();
if (!value) throw new Error(`Missing required environment variable: ${name}`);
return value;
}
export function parseAgentAnswer(response) {
const answer = (response.messages ?? [])
.filter((message) => message.role === 'GLEAN_AI')
.flatMap((message) => message.content ?? [])
.map((block) => block.text ?? '')
.join('\n')
.trim();
return { answer };
}
export function buildTriagePrompt(issue) {
return [
'Triage this one support issue using only information available to you through Glean.',
'Return four concise sections: Summary, Evidence, Next diagnostic step, and Customer-safe response.',
'Separate observed evidence from hypotheses. If the indexed evidence is insufficient, say so instead of inventing a cause.',
'If the issue is an Intercom URL, try the URL and its conversation ID; if the live page is only an authenticated application shell, use the indexed ticket record and disclose that limitation.',
'',
`Support issue:\n${issue.trim()}`,
].join('\n');
}
const transientRequestAttempts = 2;
const transientRetryDelayMs = 1500;
function fetchErrorDetail(error) {
const cause = error?.cause;
const causeDetail = cause?.code ?? cause?.message;
return causeDetail ? `${error.message} (${causeDetail})` : error.message;
}
async function requestWithCurl(endpoint, apiToken, payload) {
const directory = await mkdtemp(join(tmpdir(), 'glean-triage-'));
const bodyPath = join(directory, 'request.json');
const config = [
`url = "${endpoint}"`,
'request = POST',
`header = "Authorization: Bearer ${apiToken}"`,
'header = "Content-Type: application/json"',
`data-binary = "@${bodyPath}"`,
'silent',
'show-error',
'write-out = "\\n__HTTP_STATUS__:%{http_code}"',
].join('\n');
try {
await writeFile(bodyPath, JSON.stringify(payload), { mode: 0o600 });
const { stdout, stderr, status } = await new Promise((resolve, reject) => {
const childEnv = { ...process.env };
delete childEnv.GLEAN_API_TOKEN;
const child = spawn('curl', ['--config', '-'], {
env: childEnv,
stdio: ['pipe', 'pipe', 'pipe'],
});
const stdoutChunks = [];
const stderrChunks = [];
const timeout = setTimeout(() => child.kill('SIGTERM'), 180_000);
child.stdout.on('data', (chunk) => stdoutChunks.push(chunk));
child.stderr.on('data', (chunk) => stderrChunks.push(chunk));
child.on('error', reject);
child.on('close', (code) => {
clearTimeout(timeout);
resolve({
stdout: Buffer.concat(stdoutChunks).toString(),
stderr: Buffer.concat(stderrChunks).toString(),
status: code,
});
});
child.stdin.end(config);
});
if (status !== 0)
throw new Error(`curl failed: ${stderr.trim() || `exit ${status}`}`);
const marker = '\n__HTTP_STATUS__:';
const markerIndex = stdout.lastIndexOf(marker);
if (markerIndex < 0) throw new Error('curl returned no HTTP status');
return {
status: Number(stdout.slice(markerIndex + marker.length).trim()),
ok:
Number(stdout.slice(markerIndex + marker.length).trim()) >= 200 &&
Number(stdout.slice(markerIndex + marker.length).trim()) < 300,
text: stdout.slice(0, markerIndex),
};
} finally {
await rm(directory, { recursive: true, force: true });
}
}
export async function triageSupportIssue(issue, env = process.env) {
const agentId = requiredEnv('GLEAN_AGENT_ID', env);
const serverURL = requiredEnv('GLEAN_SERVER_URL', env);
if (!issue?.trim())
throw new Error('Provide a support issue URL or description.');
const apiToken = await createGleanTokenProvider({
serverUrl: serverURL,
scopes: ['agents'],
})();
const prompt = buildTriagePrompt(issue);
const endpoint = `${serverURL.replace(/\/+$/u, '')}/api/agents/${encodeURIComponent(agentId)}/runs`;
const payload = {
messages: [{ role: 'USER', content: [{ text: prompt, type: 'text' }] }],
stream: false,
};
let response;
let lastFetchError;
for (let attempt = 1; attempt <= transientRequestAttempts; attempt += 1) {
try {
response = await fetch(endpoint, {
method: 'POST',
headers: {
authorization: `Bearer ${apiToken}`,
'content-type': 'application/json',
},
body: JSON.stringify(payload),
});
break;
} catch (error) {
lastFetchError = error;
if (attempt < transientRequestAttempts)
await new Promise((resolve) =>
setTimeout(resolve, transientRetryDelayMs),
);
}
}
if (!response) {
if (lastFetchError?.cause?.code === 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY') {
response = await requestWithCurl(endpoint, apiToken, payload);
} else {
throw new Error(
`Platform Agents API request failed after ${transientRequestAttempts} attempts: ${fetchErrorDetail(lastFetchError)}`,
);
}
}
if (!response) {
throw new Error(
`Platform Agents API request failed after ${transientRequestAttempts} attempts: ${fetchErrorDetail(lastFetchError)}`,
);
}
const responseText =
typeof response.text === 'function' ? await response.text() : response.text;
let responseBody;
try {
responseBody = JSON.parse(responseText);
} catch {
responseBody = undefined;
}
if (!response.ok) {
const detail = responseBody?.message || responseBody?.error || responseText;
throw new Error(
`Platform Agents API returned HTTP ${response.status}: ${String(detail).slice(0, 500)}`,
);
}
const parsed = parseAgentAnswer(responseBody ?? {});
if (!parsed.answer) {
throw new Error(
'The Agent returned no answer text. Confirm it is conversational and published.',
);
}
return parsed;
}
if (process.argv[1]?.endsWith('/triage.mjs')) {
const issue =
process.argv.slice(2).join(' ') || process.env.GLEAN_SUPPORT_ISSUE;
try {
const { answer } = await triageSupportIssue(issue);
console.log(answer);
} catch (error) {
console.error(`Support triage failed: ${error.message}`);
process.exit(1);
}
}
Keep the fixture check credential-free
Exercise response parsing against a recorded Agent response so the recipe can verify its output contract before making a live request.
#!/usr/bin/env node
import assert from 'node:assert/strict';
import { parseAgentAnswer } from './triage.mjs';
const response = {
messages: [
{ role: 'USER', content: [{ text: 'ignored' }] },
{
role: 'GLEAN_AI',
content: [
{ type: 'text', text: 'Summary\nThe support issue is a stale answer.' },
{
type: 'text',
text: '\nEvidence\nA recent document update is indexed.',
},
],
},
],
};
const parsed = parseAgentAnswer(response);
assert.match(parsed.answer, /Summary/u);
assert.match(parsed.answer, /Evidence/u);
assert.doesNotMatch(parsed.answer, /ignored/u);
console.log(
'fixture verification passed: Platform Agent response parsing is stable',
);
Scaffold the recipe
npx -y tiged@2.12.8 --mode=git gleanwork/glean-cookbook/recipes/triage-support-issue triage-support-issue
Install dependencies
Installs dotenv. The recipe uses Node's built-in fetch against the Platform Agents API, not A2A or the legacy REST API.
cd triage-support-issue && npm install
Sign in to Glean
Use @gleanwork/auth for tenant discovery and refreshable OAuth credentials stored outside the project. If OAuth is unavailable, provide a user-scoped token that can run the selected Agent.
npm run login -- --email "<work-email>"
Set the Agent and issue
Set GLEAN_AGENT_ID to a published conversational support-triage Agent. Then pass one ticket URL or issue description to the triage command. A form-triggered Agent is outside this quickstart because it requires a different input contract.
Run the triage
The command makes one Platform Agent run and prints the structured triage. It does not make any external write or customer-facing update.
npm run triage -- "<support-issue>"
Verify the response
Run the no-credential fixture check first. For a live check, set GLEAN_SUPPORT_ISSUE and run npm run verify; confirm the Agent separates evidence from hypotheses.
npm run verify:fixture
The prompt asks the Agent to separate observed Glean evidence from hypotheses and to disclose when evidence is insufficient.
The beginner recipe only prints the Agent response; it does not update a ticket, send a message, or close an issue.
The recipe invokes a published conversational Agent. It does not teach Agent Builder, tool governance, or form-triggered input schemas.
- Add a streaming response so a support engineer can see the Agent's progress.
- Pass structured issue fields after you have a form-triggered Agent and its input schema is verified.
- Pair this recipe with a Trigger that creates a triage request when a support-relevant event arrives.
Triage this support issue: the customer reports that search results are stale after a document update.
The Agent returns non-empty Summary, Evidence, Next diagnostic step, and Customer-safe response sections, and clearly distinguishes evidence from hypotheses.
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 (AGENTS).