A working Mastra agent that replies to Intercom conversations and searches contacts for individual users. Scalekit handles OAuth, token storage, and refresh. Your code resolves the identifier and calls scoped tools wrapped as Mastra tools.
User connects their Intercom account once (you verify ownership)
Call listScopedTools with the identifier to get Intercom tools, then wrap them with createTool
Plug into a Mastra Agent. It automatically acts with the connected user's Intercom data.
Getting a Mastra agent to call Intercom is straightforward at the start. The moment it needs to work for different people, things get messy fast.
A teammate on your support team wants replies going out from their own workspace. A customer inside your product expects the agent to only touch their conversations and data. One shared token can't handle that, and writing the auth yourself is the last thing you want to own.
This post shows how to add per-user Intercom access to a Mastra agent without any of the OAuth boilerplate. Scalekit manages the connections. Your agent just gets the right tools for whoever triggered it.
Two common agentic use cases for per-user Intercom in Mastra agents are internal team agents and customer-facing product agents. An internal agent used by multiple teammates (e.g. support or customer success) lets each person ask the agent to reply to conversations or update customer records, executed with that specific user's Intercom permissions and identity. A customer-facing agent (for example, in a SaaS support product) can manage conversations inside the end customer's own Intercom workspace, but only after the customer has connected their Intercom account through your app.
If you're adding Intercom to a multi-user Mastra agent, the schemas are rarely the hard part. The real work is making sure the agent always uses the correct user's credentials and permissions without you writing and maintaining all the OAuth plumbing.
By the end of this tutorial you will have:
A way for users to connect their own Intercom account.
A secure OAuth flow handled by Scalekit.
Per-user Intercom connections stored safely via identifiers.
A Mastra agent that calls Intercom tools scoped to the current user.
Complete working code for per-user Intercom tool calling in Mastra agents that you can extend to reply to conversations, search contacts, and other workflows.
Architecture overview
When you're building a Mastra agent for multiple users, the challenge is letting each person bring their own Intercom account without sharing credentials.
You control the mapping from your users to Scalekit identifiers. Scalekit handles the Intercom OAuth, token vault, and refresh. Your agent only ever uses the identifier for the current user when calling tools.
The two key flows are:
Connection time: Your user connects their Intercom through Scalekit (with proper verification that the real user completed the OAuth).
Runtime: Your agent resolves the identifier for the current user (after your own authorization check) and gets properly scoped Intercom tools via Scalekit.
Here's the architecture:
Key point: The identifier is the secure key that lets your code act as one of your users. Store it securely in your own system and always authorize before using it.
Who is this for
This is for developers building Mastra agents that multiple people will use.
It could be an internal tool your own support or success folks rely on, or a capability you're adding for customers of your product. In either case, the people using it need their own Intercom context to show up correctly.
You'd rather not also become responsible for managing Intercom OAuth and tokens. This approach keeps the focus on the agent work itself.
One shared token breaks down quickly
When you add Intercom to a Mastra agent for a single user or a shared service account, everything feels simple at first. You hard-code a token, the agent can reply to conversations, and you move on.
The moment the agent needs to act for different people, problems appear:
Your support team wants the agent to reply in their Intercom workspaces with their permissions.
A customer-facing agent needs to manage conversations inside the customer's own Intercom.
Different users have different workspaces, teams, and access levels.
If you try to solve this yourself you quickly own a small auth product: OAuth flows, token storage per user, refresh logic, and mapping your internal IDs to Intercom identities. This is exactly the kind of hidden cost of building OAuth internally that slows teams down.
Let Scalekit handle the identity
Scalekit AgentKit lets you map your users (or customers) to a stable identifier. It handles the Intercom OAuth, token vault, and refresh for you.
Your Mastra agent receives clean, already-scoped tools for that specific person. When the agent decides to call a tool, you execute it with the identifier. No OAuth code lives in the agent logic itself.
The pattern is the same whether the agent is an internal team tool or embedded in a product for external customers. For more on how tool calling authentication works for AI agents, see our deep dive.
Prerequisites
Node.js 18+
A Scalekit account with an Intercom connection created in the dashboard (connection name must match exactly, e.g. intercom).
@mastra/core, @scalekit-sdk/node, zod
An LLM key (OpenAI in the examples below).
Basic familiarity with Mastra agents and tools.
Workflows that feel useful once the right account is connected
Once the agent is acting as the actual person, a few things start to click:
Reply to the latest conversation from someone specific. Search for a contact by email and see what they've been talking about lately. Drop a note on a contact after the agent has context.
All of it happens with that user's own permissions and view of the data. No one else's conversations leaking in.
const agent = new Agent({
name: 'Intercom Support Agent',
instructions: 'You are a helpful support agent...',
model: openai('gpt-4o'),
tools: mastraTools,
});
const result = await agent.generate(prompt);
See the full runnable example below.
Complete Code
import { Agent, createTool } from '@mastra/core';
import { openai } from '@ai-sdk/openai';
import { ScalekitClient } from '@scalekit-sdk/node';
import { z } from 'zod';
import 'dotenv/config';
const scalekit = new ScalekitClient(
process.env.SCALEKIT_ENV_URL!,
process.env.SCALEKIT_CLIENT_ID!,
process.env.SCALEKIT_CLIENT_SECRET!
);
async function ensureConnected(identifier: string) {
const { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({
connectionName: 'intercom',
identifier,
});
if (connectedAccount.status !== 'ACTIVE') {
const { authorizationUrl } = await scalekit.actions.getAuthorizationLink({
connectionName: 'intercom',
identifier,
userVerifyUrl: `https://your-app.com/callback?identifier=${encodeURIComponent(identifier)}`,
});
throw new Error(`Authorize at: ${authorizationUrl}`);
}
}
async function getMastraIntercomTools(identifier: string) {
const { tools: scopedTools } = await scalekit.tools.listScopedTools(identifier, {
filter: { connectionNames: ['intercom'] },
});
return scopedTools.map((scoped: any) => {
const t = scoped.tool || scoped;
const def = t.definition || t;
return createTool({
id: def.name,
description: def.description,
inputSchema: z.object({}).passthrough(),
execute: async ({ context }) =>
scalekit.actions.executeTool({
toolName: def.name,
identifier,
toolInput: context,
}),
});
});
}
export async function runIntercomMastraAgent(identifier: string, prompt: string) {
await ensureConnected(identifier);
const tools = await getMastraIntercomTools(identifier);
const agent = new Agent({
name: 'intercom-support',
instructions: 'Help the user with their Intercom conversations using the tools.',
model: openai('gpt-4o'),
tools,
});
const result = await agent.generate(prompt);
return result.text;
}
// Example: runIntercomMastraAgent('user-456', 'Reply to the conversation with john@acme.com')
Full list lives in the Scalekit Intercom connector docs.
The identifier does the heavy lifting
Change the identifier and the agent is suddenly working inside someone else's Intercom. Mastra and the model stay far away from the actual tokens.
Your code resolves the identifier after it has done its own authentication check on the request. Never accept it from the client. This aligns with the broader principle of secure token management for AI agents at scale.
The Claude version of this same Intercom integration uses the identical approach. Different wrapper, same core rule.
Troubleshooting
Connection never ACTIVE?
Missing or incorrect verifyConnectedAccountUser call after the OAuth redirect.
Permission errors from tools?
Insufficient scopes on the connection or token revoked. Re-authorize the same identifier. For a deeper look at handling this, see how to handle token refresh for AI agents.
No tools or wrong user data?
Using an identifier with no ACTIVE connection, or (dangerously) accepting the identifier from the client instead of your server-side auth.
Tradeoffs & Limitations
Approach
Pros
Cons
When to use
Shared bot / service account
Simple
No per-user permissions, audit, or revocation
Prototypes only
You roll your own OAuth + vault
Full control
You now own a security product
Rarely worth it
Scalekit Connected Accounts
Fast, secure, automatic refresh, one integration for many providers