Skip to main content

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.

scripts/triage.mjs
#!/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.

scripts/verify-fixture.mjs
#!/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',
);
Support issueOne ticket URL or issue description
Glean Platform AgentRuns with the caller's permissions
Triage outputEvidence, next diagnostic, and safe response
Node 22.12 or newer
A published conversational Agent configured to triage support issues using the user's permitted Glean content
The Agent ID from Agent Builder or the Agents Platform API
A work email for tenant discovery and OAuth sign-in, or a token able to run the selected Agent
1

Scaffold the recipe

npx -y tiged@2.12.8 --mode=git gleanwork/glean-cookbook/recipes/triage-support-issue triage-support-issue
2

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
3

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

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.

5

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

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.

Take it further
  • 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.

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

At a glance
CapabilitiesAgents
SurfacesPlatform API
StatusQuickstart
Time~15 min
Required scopes
AGENTS