Developer Handover Pack
Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.
Developer Handover Pack
FAQ & First Response Automation
[YourCompany.com] · Customer Support Department · Prepared by FullSpec · [Today's Date]
This document is the complete technical reference for the FullSpec team building the FAQ and First Response Automation. It covers the current-state step map with bottleneck identification, full specifications for each of the three agents, an end-to-end data flow trace, and the recommended build stack with time estimates. The FullSpec team uses this document to drive every build and integration decision. Your team's responsibility is to confirm credentials, tool tiers, and knowledge base readiness before build begins.
01Process snapshot
02Agent specifications
Reads each incoming Zendesk ticket body and subject line, identifies the intent category (FAQ, billing, technical, or escalation), and assigns a numerical confidence score between 0 and 1. Tickets scoring above the agreed auto-reply threshold are passed to the First Response Drafting Agent. Tickets scoring below the threshold are tagged and placed in a dedicated human-review queue in Zendesk without any automated reply being sent. This agent is the decision gate for the entire workflow. Estimated build: 14 hours. Complexity: Moderate.
// Input
zendesk.ticket.id : string
zendesk.ticket.subject : string
zendesk.ticket.body : string
zendesk.ticket.requester : { email: string, name: string }
zendesk.ticket.created_at : ISO8601 timestamp
// Output
classification.category : enum(faq | billing | technical | escalation)
classification.confidence : float (0.00 to 1.00)
classification.priority : enum(low | normal | high | urgent)
classification.route_to : string (agent queue name in Zendesk)
classification.flag_human : boolean (true if confidence < threshold)
zendesk.tags_applied : array<string>
// On low confidence
zendesk.queue : 'human-review'
zendesk.internal_note : 'Low confidence classification: <category> (<score>). Assigned for manual review.'- Zendesk plan must be Suite Team or above to support webhook triggers and tag-write API access. Confirm plan tier before build.
- Notion integration requires a Notion internal integration token with read access to the FAQ knowledge base database. The knowledge base must be fully audited and structured before this agent is built.
- The confidence threshold value (recommended starting point: 0.80) must be agreed with the support team lead before the agent is deployed. Store the threshold as a configurable environment variable, not hardcoded.
- Category labels (faq, billing, technical, escalation) must match exactly the Zendesk tag naming convention already in use, or new tags must be created before build.
- Dedupe behaviour: if a ticket already carries a classification tag from a previous webhook fire, the agent must skip re-classification and exit cleanly to prevent duplicate tagging.
- Fallback: if the Zendesk API returns an error on tag-write, log to the error table and send a Slack alert to the support team channel; do not fail silently.
- Confirm with the team lead which ticket types must always be routed to human review regardless of confidence score (for example, legal queries, complaints, or sensitive account issues).
Triggers only when the Ticket Classification Agent returns an FAQ category with a confidence score at or above the configured threshold. Queries the Notion knowledge base for the article matching the classified FAQ topic, then combines the retrieved content with the customer's name and specific ticket detail to produce a personalised, on-brand first response. The completed reply is submitted to Zendesk for immediate send, and the ticket is marked as pending. For tickets that required Gmail as the send channel (fallback), the reply is dispatched via the Gmail API. Estimated build: 12 hours. Complexity: Moderate.
// Input
classification.category : 'faq'
classification.confidence : float >= threshold
zendesk.ticket.id : string
zendesk.ticket.body : string
zendesk.ticket.requester : { email: string, name: string }
notion.db_id : string (FAQ knowledge base database ID)
// Notion query
notion.filter : { property: 'Topic', contains: classification.faq_topic }
notion.result.title : string
notion.result.body_text : string (plain text extracted from page blocks)
// Output
reply.body : string (personalised, on-brand reply text)
reply.channel : enum(zendesk | gmail)
zendesk.ticket.status : 'pending'
zendesk.public_comment : reply.body
gmail.to : zendesk.ticket.requester.email // fallback only
gmail.subject : 'Re: ' + zendesk.ticket.subject // fallback only
// On Notion miss (no matching article)
classification.flag_human : true
zendesk.queue : 'human-review'
zendesk.internal_note : 'No matching Notion article found for topic: <faq_topic>. Routed for manual reply.'- Notion database must have a 'Topic' property (select or multi-select type) with values that map to the classification categories used by the Classification Agent. Confirm property names match before build.
- If the Notion query returns zero results for a classified FAQ topic, the agent must fall back to the human-review queue rather than attempting to generate a reply from no source. This must be tested for every FAQ category during QA.
- Gmail fallback is triggered only when the Zendesk send API call fails or when the originating ticket arrived via Gmail rather than the Zendesk web widget. Confirm the routing logic with the support team lead.
- Gmail OAuth 2.0 scopes required: gmail.send and gmail.compose. A service account or delegated admin credential is preferred over a personal OAuth token.
- Reply tone and brand voice must be confirmed with the support team lead and encoded in the system prompt before build. Provide at least three approved example replies as few-shot examples in the prompt.
- The agent must not auto-send any reply for ticket categories other than FAQ, even if invoked erroneously. Add a category guard as the first conditional in the workflow.
Runs as a post-send step for every ticket processed by the workflow, regardless of whether a reply was auto-sent or the ticket was routed to human review. Creates an activity note on the matching HubSpot contact record containing the ticket category, the reply text or routing outcome, and the timestamp. For tickets classified as urgent or escalation, the agent also posts a structured Slack message to the designated support channel, tagging the on-call agent with a ticket summary and a direct Zendesk link. Estimated build: 8 hours. Complexity: Simple.
// Input
zendesk.ticket.id : string
zendesk.ticket.requester : { email: string, name: string }
zendesk.ticket.subject : string
classification.category : enum(faq | billing | technical | escalation)
classification.priority : enum(low | normal | high | urgent)
reply.body : string (or 'Routed to human review' if no auto-reply sent)
reply.sent_at : ISO8601 timestamp
zendesk.ticket.url : string (direct link)
// HubSpot output
hubspot.contact.match_by : zendesk.ticket.requester.email
hubspot.activity.type : 'NOTE'
hubspot.activity.body : 'Support ticket received. Category: <category>. Reply sent: <reply.body>. Timestamp: <reply.sent_at>.'
hubspot.contact.action : enum(append_to_existing | create_new) // create_new if no email match
// Slack output (urgent or escalation tickets only)
slack.channel : '#support-escalations'
slack.message : ':rotating_light: *Urgent ticket* | <zendesk.ticket.url> | Customer: <requester.name> | Subject: <ticket.subject> | Priority: <priority>'
slack.mention : '@oncall-support',
// On HubSpot API error
error_log.table : 'automation_errors'
error_log.fields : { ticket_id, error_code, timestamp, agent: 'EscalationLoggingAgent' }
slack.alert_channel : '#support-ops-alerts'- HubSpot Private App token requires scopes: crm.objects.contacts.read, crm.objects.contacts.write, crm.objects.notes.write. Confirm the HubSpot subscription tier supports Private Apps (Starter or above).
- Contact matching is by email address only. If the requester email does not exist in HubSpot, a new contact is created rather than failing. Post-launch deduplication review is strongly recommended.
- Slack app must be installed to the workspace with chat:write and chat:write.public scopes. The target channel (#support-escalations) must be confirmed with the support team lead, including whether the channel is private.
- The Slack @oncall-support mention must reference a real Slack user group or user handle. Confirm the exact handle before build to avoid silent mention failures.
- The Slack alert fires for classification.priority = urgent OR classification.category = escalation. Confirm both conditions with the support team lead before wiring the conditional.
- All HubSpot and Slack API errors must be written to the error log table and trigger a secondary Slack alert to the ops channel (#support-ops-alerts), not the customer-facing escalation channel.
03End-to-end data flow
// ─────────────────────────────────────────────
// TRIGGER: Zendesk webhook fires on ticket.created
// ─────────────────────────────────────────────
RECEIVE zendesk.webhook.event = 'ticket.created'
ticket.id = event.ticket.id
ticket.subject = event.ticket.subject
ticket.body = event.ticket.description
ticket.requester = { email: event.ticket.requester.email, name: event.ticket.requester.name }
ticket.created_at = event.ticket.created_at
ticket.url = 'https://<subdomain>.zendesk.com/agent/tickets/' + ticket.id
// ─────────────────────────────────────────────
// AGENT HANDOFF 1: Ticket Classification Agent
// ─────────────────────────────────────────────
CALL ClassificationAgent(
input: { ticket.id, ticket.subject, ticket.body, ticket.requester, ticket.created_at }
)
// AI prompt receives ticket.subject + ticket.body
// Notion category reference queried for label validation
classification.category = AI_OUTPUT.category // enum: faq | billing | technical | escalation
classification.faq_topic = AI_OUTPUT.faq_topic // string: matched FAQ topic label
classification.confidence = AI_OUTPUT.confidence // float: 0.00 to 1.00
classification.priority = AI_OUTPUT.priority // enum: low | normal | high | urgent
classification.route_to = LOOKUP(category -> zendesk_queue_name)
classification.flag_human = (confidence < ENV.CONFIDENCE_THRESHOLD) // threshold stored as env var
// Write to Zendesk
ZENDESK PUT /tickets/{ticket.id}
tags += [classification.category, classification.priority]
assignee = classification.route_to
// ─────────────────────────────────────────────
// DECISION GATE: confidence threshold check
// ─────────────────────────────────────────────
IF classification.flag_human = true
ZENDESK PUT /tickets/{ticket.id}
tags += ['human-review']
queue = 'human-review'
comment = INTERNAL_NOTE('Low confidence: ' + classification.category + ' (' + classification.confidence + ')')
GOTO LoggingAgent // skip drafting; still log and alert if urgent
// ─────────────────────────────────────────────
// AGENT HANDOFF 2: First Response Drafting Agent
// ─────────────────────────────────────────────
IF classification.category = 'faq' AND classification.flag_human = false
CALL DraftingAgent(
input: { ticket.id, ticket.body, ticket.requester, classification.faq_topic, notion.db_id = ENV.NOTION_FAQ_DB_ID }
)
// Notion query
NOTION POST /databases/{notion.db_id}/query
filter = { property: 'Topic', select: { equals: classification.faq_topic } }
notion.article.title = NOTION_RESULT.properties.Name.title[0].plain_text
notion.article.body_text = NOTION_RESULT.extract_plain_text(page_blocks)
IF notion.article = NULL
classification.flag_human = true
ZENDESK internal_note = 'No Notion article for topic: ' + classification.faq_topic
GOTO LoggingAgent
// AI drafting
reply.body = AI_DRAFT(
system_prompt = ENV.DRAFT_SYSTEM_PROMPT,
few_shot = ENV.APPROVED_EXAMPLES,
context = { notion.article.body_text, ticket.requester.name, ticket.body }
)
reply.sent_at = NOW()
// Send reply
ZENDESK POST /tickets/{ticket.id}/comments
public = true
body = reply.body
ZENDESK PUT /tickets/{ticket.id}
status = 'pending'
IF zendesk.send_error = true
GMAIL POST /users/me/messages/send
to = ticket.requester.email
subject = 'Re: ' + ticket.subject
body = reply.body
// ─────────────────────────────────────────────
// AGENT HANDOFF 3: Escalation and Logging Agent
// ─────────────────────────────────────────────
LoggingAgent:
CALL EscalationLoggingAgent(
input: {
ticket.id, ticket.requester, ticket.subject, ticket.url,
classification.category, classification.priority,
reply.body OR 'Routed to human review',
reply.sent_at OR NOW()
}
)
// HubSpot contact match
HUBSPOT GET /crm/v3/objects/contacts?email={ticket.requester.email}
IF hubspot.contact EXISTS
hubspot.action = 'append_to_existing'
hubspot.contact_id = hubspot.contact.id
ELSE
HUBSPOT POST /crm/v3/objects/contacts
{ email: ticket.requester.email, firstname: ticket.requester.name }
hubspot.action = 'create_new'
hubspot.contact_id = NEW_CONTACT.id
// Create HubSpot note
HUBSPOT POST /crm/v3/objects/notes
hs_note_body = 'Category: ' + classification.category + '. Reply: ' + reply.body + '. Sent: ' + reply.sent_at
hs_timestamp = reply.sent_at
HUBSPOT POST /crm/v3/associations/notes/{note.id}/contacts/{hubspot.contact_id}
// Slack escalation alert (conditional)
IF classification.priority = 'urgent' OR classification.category = 'escalation'
SLACK POST /chat.postMessage
channel = ENV.SLACK_ESCALATION_CHANNEL // e.g. '#support-escalations'
text = ':rotating_light: *Urgent ticket* | <' + ticket.url + '> | ' + ticket.requester.name + ' | ' + ticket.subject + ' | Priority: ' + classification.priority
mention = ENV.SLACK_ONCALL_HANDLE // e.g. '@oncall-support'
// ─────────────────────────────────────────────
// END STATE
// ─────────────────────────────────────────────
ticket.status = 'pending' (auto-replied) OR 'open' (human-review queue)
hubspot.note = written to contact record
slack.alert = sent (urgent/escalation) OR skipped (routine)
total_elapsed = < 2 minutes for FAQ auto-reply path04Recommended build stack
More documents for this process
Every document generated for FAQ & First Response Automation.