Customer support teams using Gorgias get overwhelmed when tickets require human judgment; automated deflection and intelligent escalation are needed to reduce response times.
A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
Customer support teams using Gorgias often get buried when ticket volume spikes. Automated FAQ deflection can handle the easy questions, but complex billing, account, or security issues still need a human. This recipe builds a Google Gemini-powered triage system that receives Gorgias webhook events, classifies each ticket’s intent, answers FAQ questions automatically with Gemini Flash, and escalates complex issues to a human support agent using the @reaatech/agent-handoff multi-agent handoff protocol with context compression. You’ll wire up a full pipeline: webhook → intent classifier → FAQ agent or handoff escalation → Gorgias ticket update, all observed with structured logging and metrics.
Prerequisites
Node.js 22+ and pnpm 10 installed on your machine
A Gorgias account with API access (admin settings → Integrations → API)
Familiarity with Next.js App Router (app/api/ route handlers, NextRequest/NextResponse)
Familiarity with TypeScript and basic Express-style request handling
Step 1: Create the project and install dependencies
Start from an empty directory and scaffold a Next.js project. The scaffold includes the standard config files — next.config.ts, tsconfig.json, vitest.config.ts, eslint.config.mjs — that are already locked. You’ll add the specific dependencies for this recipe.
Create .env (or append to .env.example) with the environment variables the system reads at runtime. Every process.env.X reference in the source code must have a corresponding entry.
Expected output: The .env file sits at the project root. Never commit real values — use placeholders in committed .env.example.
Step 3: Configure Next.js for the instrumentation hook
The system uses OpenTelemetry to emit observability spans. Next.js needs the experimental.instrumentationHook flag enabled so that src/instrumentation.ts (which you’ll create later) actually fires during server startup. Open next.config.ts and make sure it has this exact shape:
ts
import type { NextConfig } from "next";const nextConfig = { experimental: { instrumentationHook: true, },} as NextConfig;export default nextConfig;
Expected output: This is the exact spelling — instrumentationHook, not clientInstrumentationHook or instrumentation. Without this flag, the register() function in src/instrumentation.ts is dead code and initOtel() never runs.
Step 4: Define the shared TypeScript types
Create src/lib/types.ts — this is the single source of truth for all data shapes across the system. It defines the Gorgias ticket model, the webhook payload, the triage result discriminated union, and the FAQ agent configuration interface. Types from @reaatech/agent-handoff (like HandoffPayload, CompressedContext, HandoffResult) are re-exported rather than hand-rolled.
Expected output:src/lib/types.ts exists and compiles without errors. The TriageResult type is a discriminated union — you’ll use it to unify the FAQ and escalation paths.
Step 5: Build the Gorgias REST client
Create src/lib/gorgias.ts — a typed REST client that talks to the Gorgias Public API (https://<domain>.gorgias.com/api). It wraps fetch with a 30-second AbortController timeout and converts HTTP errors into typed errors from @reaatech/agent-handoff (TransportError for bad status codes, TimeoutError for aborted requests).
Expected output:src/lib/gorgias.ts compiles cleanly. The factory function createGorgiasClient() reads env vars and throws descriptive errors when they’re missing. Every public method is typed and uses the shared Ticket/TicketMessage interfaces.
Step 6: Build the Gemini LLM wrapper
Create src/lib/gemini.ts — a thin wrapper around the @google/genai SDK that provides a createGeminiClient() factory and two generation functions. This isolates the Google Gen AI import surface so only this file needs to know about the SDK’s API shape.
ts
import { GoogleGenAI } from "@google/genai";import { HandoffError, ConfigurationError } from "@reaatech/agent-handoff";export function createGeminiClient(): GoogleGenAI { const apiKey = process.env.GEMINI_API_KEY; if (!apiKey) { throw new ConfigurationError("GEMINI_API_KEY environment variable is not set"); } return new GoogleGenAI({ apiKey });}export async function generateReply( ai: GoogleGenAI, prompt: string, systemInstruction?: string,): Promise<string> { try { const response = await ai.models.generateContent({ model: "gemini-2.5-flash", contents: prompt, config: { systemInstruction: systemInstruction ?? "" }, }); return response.text ?? ""; } catch (error) { throw new HandoffError( error instanceof Error ? error.message : "Gemini generateContent failed", "unknown_error", ); }}export async function* generateReplyStream( ai: GoogleGenAI, prompt: string,): AsyncIterable<string> { const stream = await ai.models.generateContentStream({ model: "gemini-2.5-flash", contents: prompt, }); for await (const chunk of stream) { const text = chunk.text; if (text) { yield text; } }}
Expected output: The Gemini wrapper compiles. Errors from the SDK are caught and rethrown as HandoffError so the orchestrator can handle them uniformly. generateReplyStream returns an AsyncIterable for streaming use cases.
Step 7: Build the FAQ agent
Create src/services/faq-agent.ts — this agent’s only job is answering FAQ questions. Classification is handled separately by the classifier service. It wraps generateReply with a system prompt that tells Gemini to act as a Gorgias FAQ support agent.
Expected output: The FAQ agent handles empty input without calling Gemini (returns { reply: "", confidence: 0 }). On any error from generateReply, it catches and returns the same fallback — the orchestrator will escalate when confidence is zero. The factory defaults to gemini-2.5-flash with 1024 max tokens and 5 messages of ticket history.
Step 8: Build the agent registry
Create src/services/agent-registry.ts — this populates an AgentRegistry (from @reaatech/agent-handoff-routing) with three agents: the FAQ bot, Tier 1 human support, and Tier 2 specialist. Each agent carries skill tags, domains, load limits, and availability metadata that the CapabilityBasedRouter uses to score and select the best target.
Expected output:buildAgentRegistry() returns a registry with three agents. The load numbers (maxConcurrentSessions) reflect the expected capacity: FAQ bot handles many concurrent sessions, Tier 1 fewer, Tier 2 fewest (specialist).
Step 9: Build the triage orchestrator
Create src/services/triage-orchestrator.ts — this is the central piece. It wires together the Gorgias client, the FAQ agent, the handoff manager (with its compressor, router, and transport), and the classifier into a single processTicket(ticketId) entry point.
The orchestrator:
Fetches the ticket from Gorgias
Detects the language
Classifies intent via classifierService.classify() with a fallback to direct Gemini classification
If classified as faq-bot with confidence >= 0.6: delegates to the FAQ agent, posts the reply back to Gorgias, and tags the ticket as answered
Otherwise: compresses the conversation context via HybridCompressor, builds a HandoffContext, and calls handoffManager.executeHandoff() to route to a human agent
ts
import { createHandoffConfig, withRetry, HandoffError, TypedEventEmitter, pickDefined,} from "@reaatech/agent-handoff";import type { HandoffContext, HandoffResult, Message, AgentCapabilities,} from "@reaatech/agent-handoff";import { HybridCompressor, SimpleTokenCounter } from "@reaatech/agent-handoff-compression";import { CapabilityBasedRouter } from "@reaatech/agent-handoff-routing";import { HandoffManager, TransportFactory, MCPTransport } from "@reaatech/agent-handoff-protocol";import { classifierService, detectLanguage, isRateLimitError,} from "@reaatech/agent-mesh-classifier"
Expected output: The orchestrator compiles. The createOrchestrator() factory lazily initializes the Gorgias client, FAQ agent, and handoff manager as singletons. The HandoffManager is wired with lifecycle listeners that log each event (handoffStart, handoffComplete, handoffReject, handoffError) via @reaatech/agent-mesh-observability. The orchestrator uses *Like interfaces (GorgiasClientLike, FaqAgentLike, HandoffManagerLike, ClassifierLike) rather than concrete types, keeping it testable without coupling to specific implementations. The classifier fallback calls Gemini directly with withRetry (3 retries, exponential backoff, 100ms base delay).
Step 10: Create the webhook API route
Create app/api/webhook/gorgias/route.ts — this is the entry point for Gorgias webhook events. It accepts incoming POST requests with a ticketId, fires off the orchestration in the background (fire-and-forget), and immediately returns a 200 acknowledgment so Gorgias doesn’t retry. A GET endpoint serves as a health check.
Expected output: The route handler uses NextRequest and NextResponse.json() (not bare Request/Response). POST with a missing ticketId returns 400. POST with { ticketId: "123" } returns 200 and starts processing asynchronously. GET returns { status: "ok" }.
Step 11: Add the OpenTelemetry instrumentation hook
Create src/instrumentation.ts — this runs once when the Next.js server starts (on the Node.js runtime only). It dynamically imports initOtel from @reaatech/agent-mesh-observability to avoid module-resolution errors in the Edge runtime.
ts
export async function register(): Promise<void> { if (process.env.NEXT_RUNTIME === "nodejs") { const { initOtel } = await import("@reaatech/agent-mesh-observability"); initOtel(); }}
Expected output: When the dev server starts and experimental.instrumentationHook is enabled, the register() function fires, calling initOtel() to start OpenTelemetry trace export (if OTEL_EXPORTER_OTLP_ENDPOINT is configured).
Step 12: Set up the barrel exports
Replace the placeholder src/index.ts with re-exports that expose the public API surface of the library:
ts
export { TriageOrchestrator } from "./services/triage-orchestrator.js";export { GorgiasClient, createGorgiasClient } from "./lib/gorgias.js";export { createGeminiClient, generateReply } from "./lib/gemini.js";export { FaqAgent, createFaqAgent } from "./services/faq-agent.js";
Expected output:src/index.ts compiles and allows consumers to import the main classes and factories from a single entry point.
Step 13: Run the tests
The project includes a full test suite across all modules. Run type-checking, linting, and the tests with coverage:
terminal
pnpm typecheckpnpm lintpnpm test
Expected output: All three commands exit zero. The test suite covers:
Agent registry (tests/agent-registry.test.ts) — buildAgentRegistry returns three agents with correct capabilities
Coverage thresholds (lines, branches, functions, statements) are all set to 90% or higher.
Next steps
Add webhook signature verification — Use GORGIAS_WEBHOOK_SECRET to validate incoming request signatures using a HMAC check in the webhook route
Implement retry with dead-letter queue — If the orchestrator fails on a ticket, push it to a queue (e.g., Redis or in-memory) for reprocessing instead of silently dropping it
Deploy to production — Wrap the Next.js app in a Docker container and deploy behind a load balancer with the Gorgias webhook URL pointed at your production endpoint
Add real agent transport — Replace the mock MCPTransport[] array with a real MCP client or A2A HTTP transport so the handoff actually reaches a human agent dashboard
Add per-tenant isolation — If serving multiple Gorgias subdomains, wrap the orchestrator factory in a tenant-aware lookup so each domain gets its own GorgiasClient instance
"You are a ticket triage classifier. Given a customer support message, determine if it should be handled by an FAQ bot (faq-bot), Tier 1 support (tier-1), or Tier 2 specialist (tier-2). Return the agent_id and confidence score.";