> **Building with AI coding agents?** If you're using an AI coding agent, install the official Scalekit plugin. It gives your agent full awareness of the Scalekit API — reducing hallucinations and enabling faster, more accurate code generation.
>
> - **Claude Code**: `/plugin marketplace add scalekit-inc/claude-code-authstack` then `/plugin install <auth-type>@scalekit-auth-stack`
> - **GitHub Copilot CLI**: `copilot plugin marketplace add scalekit-inc/github-copilot-authstack` then `copilot plugin install <auth-type>@scalekit-auth-stack`
> - **Codex**: run the bash installer, restart, then open Plugin Directory and enable `<auth-type>`
> - **Skills CLI** (Windsurf, Cline, 40+ agents): `npx skills add scalekit-inc/skills --list` then `--skill <skill-name>`
>
> `<auth-type>` / `<skill-name>`: `agentkit`, `full-stack-auth`, `mcp-auth`, `modular-sso`, `modular-scim` — [Full setup guide](https://docs.scalekit.com/dev-kit/build-with-ai/)

---

# Custom tools

When you need a connector tool that Scalekit doesn't offer as a pre-built tool, use **API Proxy mode**. You define the tool contract and call the provider endpoint through `actions.request`. Scalekit injects the user's credentials from their connected account; your agent never handles raw tokens.

| Option | Best for | Who defines tool schema |
|---|---|---|
| Scalekit optimized tools | Common connector tools | Scalekit |
| Custom tools (API Proxy) | Unsupported or app-specific tools | Your application |

This page assumes the user has an `ACTIVE` connected account. If not, see [Authorize a user](/agentkit/tools/authorize/).

## Find the right endpoint

The `path` you pass to `actions.request` is forwarded directly to the provider's API; Scalekit only adds authentication headers. Look up the provider's API reference to get the correct path, method, and request shape.

| Connector | API reference |
|---|---|
| Gmail | [Google Gmail API](https://developers.google.com/gmail/api/reference/rest) |
| Slack | [Slack API methods](https://api.slack.com/methods) |
| GitHub | [GitHub REST API](https://docs.github.com/en/rest) |
| Salesforce | [Salesforce REST API](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/) |
| HubSpot | [HubSpot API](https://developers.hubspot.com/docs/api/overview) |
**Base URL is managed by Scalekit:** Provide only the path; Scalekit resolves the correct base URL for the connector and injects the user's credentials automatically.

## Define your tool contract

Design the tool around your agent's intent, not the provider's API surface. For example, to list Gmail filters:

- **Tool name:** `gmail_list_filters` (describes the action, not the endpoint)
- **Input:** `identifier` (your app's user ID)
- **Output:** `{ filters: [...], count: N }` (structured, not the raw Gmail response)

Keep schemas focused on what the model needs. Strip provider-specific noise before returning data.

## Proxy the API call

Use `actions.request` to call any provider endpoint. Scalekit handles credential injection.

**GET requests:** pass query parameters as a dict:

```python
def gmail_list_filters(identifier: str):
    response = actions.request(
        connection_name="gmail",
        identifier=identifier,
        method="GET",
        path="/gmail/v1/users/me/settings/filters",
    )
    data = response.json()
    return {"filters": data.get("filter", []), "count": len(data.get("filter", []))}

def gmail_list_unread(identifier: str, max_results: int = 10):
    response = actions.request(
        connection_name="gmail",
        identifier=identifier,
        method="GET",
        path="/gmail/v1/users/me/messages",
        query_params={"q": "is:unread", "maxResults": max_results},
    )
    return {"messages": response.json().get("messages", [])}
```
  ```typescript
async function gmailListFilters(identifier: string) {
  const response = await scalekit.actions.request({
    connectionName: 'gmail',
    identifier,
    method: 'GET',
    path: '/gmail/v1/users/me/settings/filters',
  });
  const filters = response.data?.filter ?? [];
  return { filters, count: filters.length };
}

async function gmailListUnread(identifier: string, maxResults = 10) {
  const response = await scalekit.actions.request({
    connectionName: 'gmail',
    identifier,
    method: 'GET',
    path: '/gmail/v1/users/me/messages',
    queryParams: { q: 'is:unread', maxResults },
  });
  return { messages: response.data?.messages ?? [] };
}
```
  **POST requests:** pass a body for write operations:

```python
def slack_send_message(identifier: str, channel: str, text: str):
    response = actions.request(
        connection_name="slack",
        identifier=identifier,
        method="POST",
        path="/api/chat.postMessage",
        body={"channel": channel, "text": text},
    )
    data = response.json()
    if not data.get("ok"):
        raise ValueError(f"Slack error: {data.get('error')}")
    return {"ts": data.get("ts"), "channel": data.get("channel")}
```
  ```typescript
async function slackSendMessage(identifier: string, channel: string, text: string) {
  const response = await scalekit.actions.request({
    connectionName: 'slack',
    identifier,
    method: 'POST',
    path: '/api/chat.postMessage',
    body: { channel, text },
  });
  if (!response.data?.ok) throw new Error(`Slack error: ${response.data?.error}`);
  return { ts: response.data.ts, channel: response.data.channel };
}
```
  ## Expose as an LLM-callable tool

Define a JSON Schema for each custom tool and pass the list to your LLM. The schema format below matches what Scalekit returns for built-in tools, so it works directly with the Anthropic API. For OpenAI, rename `input_schema` to `parameters`.

```python
custom_tools = [
    {
        "name": "gmail_list_filters",
        "description": "List Gmail filters configured for the current user",
        "input_schema": {
            "type": "object",
            "properties": {"identifier": {"type": "string"}},
            "required": ["identifier"],
        },
    },
    {
        "name": "slack_send_message",
        "description": "Send a message to a Slack channel on behalf of the user",
        "input_schema": {
            "type": "object",
            "properties": {
                "identifier": {"type": "string"},
                "channel": {"type": "string"},
                "text": {"type": "string"},
            },
            "required": ["identifier", "channel", "text"],
        },
    },
]

def route_tool_call(tool_name: str, tool_input: dict):
    if tool_name == "gmail_list_filters":
        return gmail_list_filters(tool_input["identifier"])
    if tool_name == "slack_send_message":
        return slack_send_message(tool_input["identifier"], tool_input["channel"], tool_input["text"])
    raise ValueError(f"Unknown tool: {tool_name}")
```
  ```typescript
const customTools = [
  {
    name: 'gmail_list_filters',
    description: 'List Gmail filters configured for the current user',
    input_schema: {
      type: 'object',
      properties: { identifier: { type: 'string' } },
      required: ['identifier'],
    },
  },
  {
    name: 'slack_send_message',
    description: 'Send a message to a Slack channel on behalf of the user',
    input_schema: {
      type: 'object',
      properties: {
        identifier: { type: 'string' },
        channel: { type: 'string' },
        text: { type: 'string' },
      },
      required: ['identifier', 'channel', 'text'],
    },
  },
];

function routeToolCall(toolName: string, toolInput: Record<string, string>) {
  if (toolName === 'gmail_list_filters') return gmailListFilters(toolInput.identifier);
  if (toolName === 'slack_send_message') return slackSendMessage(toolInput.identifier, toolInput.channel, toolInput.text);
  throw new Error(`Unknown tool: ${toolName}`);
}
```
  ## Check authorization before proxy calls

Verify the connected account is `ACTIVE` before making a proxy call and handle provider errors explicitly:

```python
account = actions.get_or_create_connected_account(
    connection_name="gmail",
    identifier=identifier,
).connected_account

if account.status != "ACTIVE":
    raise ValueError("Connected account is not ACTIVE. Re-authorize the user.")
```
  ```typescript
import { ConnectorStatus } from '@scalekit-sdk/node/lib/pkg/grpc/scalekit/v1/connected_accounts/connected_accounts_pb';

const account = (await scalekit.actions.getOrCreateConnectedAccount({
  connectionName: 'gmail',
  identifier,
})).connectedAccount;

if (account?.status !== ConnectorStatus.ACTIVE) {
  throw new Error('Connected account is not ACTIVE. Re-authorize the user.');
}
```
  ## Best practices

- Expose only the fields your model needs; keep schemas small
- Validate inputs server-side; never trust model-generated parameters
- Use predictable JSON keys; return stable output across calls
- Map provider errors to clear tool errors; don't leak raw provider payloads to prompts

---

## More Scalekit documentation

| Resource | What it contains | When to use it |
|----------|-----------------|----------------|
| [/llms.txt](/llms.txt) | Structured index with routing hints per product area | Start here — find which documentation set covers your topic before loading full content |
| [/llms-full.txt](/llms-full.txt) | Complete documentation for all Scalekit products in one file | Use when you need exhaustive context across multiple products or when the topic spans several areas |
| [sitemap-0.xml](https://docs.scalekit.com/sitemap-0.xml) | Full URL list of every documentation page | Use to discover specific page URLs you can fetch for targeted, page-level answers |
