
Support teams collect satisfaction ratings and then do nothing with them for a quarter.
The survey goes out, the customer clicks a face, the score lands in a report, and the report gets read in aggregate three months later by someone deciding whether to expand headcount. The individual angry customer who rated a ticket negative on a Tuesday hears nothing back. By the time the number matters, the moment to fix it has passed.
The gap is not measurement. It is the loop between a bad score and someone doing something about it, and that loop is small enough to automate: a negative rating should reopen the ticket and produce an apology while the customer still remembers the ticket exists.
There is no planning loop. The agent polls on a fixed interval, and each cycle walks the same five steps and exits.
Step 4 is where most implementations go wrong, and it gets its own section below. Steps 1, 3, and 5 are the ones the connector does the heavy lifting for.
Freshdesk and Gmail sit at opposite ends of the auth spectrum, and this agent has to hold both at once.
Every other connector in this series is an OAuth flow. Freshdesk is not. It authenticates with an API key passed as Basic Auth, and each agent inside a customer's helpdesk has their own key tied to their profile and role. There is no consent screen, no access token to exchange, no refresh cycle.
That sounds like a simplification. It is the opposite. Because these are long-lived credentials rather than short-lived tokens, the security burden goes up. An OAuth access token that leaks expires in an hour. A Freshdesk API key that leaks works until somebody notices and rotates it, and rotation is a manual action inside a helpdesk profile.
The second constraint is routing. Freshdesk API calls are subdomain-scoped: every customer's helpdesk lives at their own company.freshdesk.com. If you are connecting to several customers' helpdesks, you have to store and route to each subdomain alongside each key. Get the pairing wrong and your calls land in the wrong account entirely, with no error that makes the cause obvious. A ticket reply posted into someone else's helpdesk is not a bug you find in staging.
There is also no OAuth revocation event to listen for. When a support agent resets their key, the next call fails, and how cleanly that surfaces is on you.
This is the case for putting a credential vault in front of Freshdesk rather than an OAuth library. The problem is not the flow; there is no flow. The problem is collecting per-agent keys through a UX users can actually complete, isolating them per tenant, routing each call to the right subdomain, and detecting revocation. Full tool list is in the Freshdesk connector docs.
Recommended Reading: OAuth vs API Keys for AI Agents
The Gmail connector is 56 tools on OAuth 2.0, and it will work immediately on Scalekit's shared credentials. That is a testing convenience, not a deployment path.
Before production you click Use your own credentials, create an OAuth client ID of type Web application in the Google Cloud Console, enable the Gmail API in the API Library, and paste the Scalekit redirect URI into Authorized redirect URIs. From that point every user's consent screen shows your Google Cloud project, and a project requesting user-data scopes has to clear Google's verification before more than 100 accounts can connect. That review takes days, so it belongs on the schedule before launch rather than after the first enterprise customer asks.
One format detail costs an afternoon if you miss it. Gmail tool responses come back in camelCase (threadId, messageId, internalDate), and tool input parameters are snake_case (thread_id, message_id). You read a value one way and pass it back the other. It is consistent once you know, and invisible until you don't.
Recommended Reading: Freshdesk automation: Build a Google ADK agent for consistent CSAT follow-ups
Both of these come from the agent's own source and are worth knowing before you write a comparison.
Freshdesk list tools return {"array": [...]} rather than a bare list, so the payload needs unwrapping before you iterate it. And JSON numbers arrive as floats, which means a status comparison against the integer 4 silently matches nothing. The working check is against both:
That is not defensive padding. Drop the 4.0 and the agent reports zero resolved tickets forever while looking completely healthy.
Here is the design question this agent is good for, and it generalizes well past CSAT.
Freshdesk encodes satisfaction as two integers: 103 is satisfied, -103 is not. The routing rule is that 103 thanks the customer and closes the ticket, and -103 apologizes and reopens it. Written out, the whole decision is this:
A dictionary. Deterministic, testable, free, and correct every time.
It is very tempting to hand that decision to a model instead, because the model is already in the stack. Doing so buys a network round trip, a quota you can exhaust, a fallback chain across three model versions, an event loop, a session lifecycle, and a class of failure where the agent does nothing because the classifier returned null. It buys all of that to compute a two-branch mapping over an integer you already have.
Meanwhile the part that genuinely needs a model gets a constant:
Every unhappy customer gets the identical sentence. The customer who waited three days for a password reset and the customer whose data export silently failed get the same apology, which reads as exactly what it is.
The customer's verbatim feedback is right there in the survey payload. Writing an apology that references the specific complaint is a language task, it is not deterministic, and no dictionary will do it. That is the LLM's job.
So: route with code, write with the model.
The routing still comes from the dictionary. The model only decides wording, and if it fails you fall back to the constant string and the ticket still gets handled correctly. That is the right dependency direction: the deterministic path works without the model, and the model makes it better.
Recommended Reading: How to implement least privilege in AI agent tool calls
Two connectors, two entirely different credential models, one method signature. That is the point.
That last point is the one to check against your own configuration. If a Freshdesk API key or a helpdesk domain appears in your environment file, you have moved the credential problem rather than solved it.
Between them, these two connectors expose 90 tools: 56 for Gmail, 34 for Freshdesk. This agent calls six. The agent should see the tools the current rep's connected account authorizes, not the catalog; surface reduction is what makes tool selection reliable and keeps context cost down.
Recommended Reading: Token-Efficient Tool Calling: Auth Overhead in Agent Context
Ready to build? Start free and follow the AgentKit quickstart.
Create a workspace at app.scalekit.com and copy SCALEKIT_ENV_URL, SCALEKIT_CLIENT_ID, and SCALEKIT_CLIENT_SECRET into your .env.
Go to AgentKit then Connections then Create Connection and find Freshdesk. Because Freshdesk uses Basic Auth rather than OAuth, the connected account is created from an API key and the helpdesk subdomain rather than a redirect flow. The support agent's API key comes from their Freshdesk profile settings, and the subdomain is the part before .freshdesk.com in their helpdesk URL.
Create the Gmail connection and complete OAuth. For testing, Scalekit's shared credentials work immediately. Before production, click Use your own credentials, create a Web application OAuth client in the Google Cloud Console, enable the Gmail API, and paste the redirect URI into Authorized redirect URIs.
Then copy the exact connection names from the dashboard into .env. Scalekit auto-suffixes them per workspace, so the connection you think of as freshdesk may not be spelled that way. Passing connection_name explicitly on every call is what prevents a multiple connected accounts found error when one identifier holds more than one connection of the same provider type.
Then prompt Claude Code:
The generated execution wrapper is the whole auth surface of the agent:
Locally the identifier comes from .env. In production, resolve each support rep's real user ID server-side and pass it, so the survey email leaves that rep's own Gmail address and the Freshdesk reply is attributed to them rather than to a shared automation account. That is not cosmetic: a shared account means every ticket reply in the audit log says the bot did it, and what the rep cannot do in the helpdesk, the agent could.
Recommended Reading: Why Admin Accounts Are the Wrong Default for AI Agents
Freshdesk status 4 is Resolved, 5 is Closed, 2 is Open. The agent fetches a page of tickets and filters client-side, including the requester so it has an email address to survey:
Note the bound. This reads one page of the most recent tickets, not the full backlog. A helpdesk resolving more than 100 tickets between polls needs the page parameter incremented in a loop, and it is worth adding that before you find out the hard way.
This is the step that makes the agent worth deploying rather than a report. The survey goes out from the rep who handled the ticket, so the reply-to is a human the customer already spoke with.
Two verified tools do the work. gmail_create_draft builds the message, and gmail_send_draft delivers it:
Splitting create from send is deliberate. It gives you a natural approval gate: stop after gmail_create_draft and a human reviews the outbound queue, or chain straight through to gmail_send_draft for full automation. The same two calls support both postures, and the choice becomes a config flag rather than a rewrite.
If you would rather keep the survey inside an existing email thread, gmail_reply_to_thread takes thread_id, to, and body, and sets the In-Reply-To and References headers so mail clients thread it correctly.
Freshdesk's legacy satisfaction-survey endpoint is not exposed as a connector tool. The rating is reachable a better way: ask for the ticket and include the ratings.
A ticket with no rating yet is the normal case, not an error. It means the customer has not responded. Mark it as surveyed so the agent does not re-send, and let the next cycle check again for a rating rather than re-polling the ticket from scratch every 60 seconds. Getting this wrong is how a polling agent quietly burns its API quota on tickets nobody will ever rate.
The routing is the dictionary from earlier. The writing is the model:
The failure path matters more than the happy path. If Gemini is rate-limited, the model returns nothing usable, or the API key expired, the agent still reopens the right ticket with the right status and a generic apology. The customer gets a slightly worse message. They do not get silence, and no ticket is left in the wrong state because a classifier was unavailable.
Two calls, both through the same interface:
Order matters. Reply first, then change status. If the reply fails you do not want a ticket already flipped to Closed with nothing written on it, and you do not want the ticket marked processed either. Leave it unmarked and the next cycle retries the whole thing.
A typical cycle:
For scheduled operation rather than a resident loop, drop the while and run it from cron:
Tool names. Call list_scoped_tools against your own connected account and confirm every tool name in your code appears in the result. Two minutes, and it catches the most common class of agent bug.
Freshdesk subdomain. Confirm the connection points at the right helpdesk. Wrong-subdomain calls do not error usefully, they just act on someone else's tickets.
Gmail verification. If more than 100 accounts will connect, your Google Cloud project needs to clear verification first. Start that before launch week.
Float comparisons. Any status or rating comparison against a Freshdesk integer must also match the float. This is the failure that looks like an empty helpdesk.
Volume. More than 100 resolved tickets between polls overflows a single page. Add pagination.
Send posture. Decide whether gmail_send_draft runs automatically or a human approves the queue. Outbound email to customers is the one action in this pipeline you cannot take back.
The pattern extends without touching auth. Sending the survey through Outlook instead of Gmail, posting negative ratings into a Slack channel for a support lead, or writing scores into a warehouse all follow the same three steps: add the connection in the dashboard, look up the real tool name, call execute_tool.
Scalekit maintains each connector. You maintain none of them.
The full source is on GitHub. Browse the catalog in the connector docs, check the Python SDK reference, or start free and work through the AgentKit quickstart.
For adjacent support agents on the same auth layer, see the Zendesk, Notion, and Slack triage agent, the customer escalation agent with Intercom, Linear, and Slack, or Zendesk, Slack, and Notion automation with Google ADK.
Use it. Freshdesk sends the survey and collects the rating perfectly well, and this agent reads that rating rather than replacing it. What Freshdesk does not do is act on a negative score the same day. Sending from the rep's own Gmail is the variant worth building when you want the survey to come from a person the customer recognizes rather than a no-reply address.
Because it is just an API key. There is no expiry, no refresh, and no revocation event, so a leaked key stays valid until a human rotates it. Add per-agent keys across multiple customer subdomains and you have long-lived secrets that must be collected, isolated per tenant, routed correctly, and monitored. That is more infrastructure than an OAuth flow, not less.
No. Freshdesk gives you an integer, and the mapping from integer to action is fixed. Put it in a dictionary, where it is testable and cannot fail. Give the model the job it is actually good at, which is writing a reply that references what the customer said.
The routing still works, because routing does not depend on the model. The agent falls back to default copy, posts the reply, and sets the correct status. The message is generic; the ticket is still handled.
Yes, and you should. Pass each rep's real user ID as identifier so the survey sends from their Gmail and the Freshdesk reply is attributed to them. Scalekit stores each rep's Freshdesk key and Gmail token independently. See single-tenant versus multi-tenant tool calling.
Track surveyed ticket IDs in the state file separately from processed ones. A ticket moves from surveyed to processed only once a rating arrives and the follow-up is posted. Conflating the two states is how customers get two surveys, which is worse than getting none.