> **Building with AI coding agents?** Install the authstack plugin with one command. This equips your agent with accurate Scalekit implementation patterns.
>
> **Recommended**:
> ```bash
> npx @scalekit-inc/cli setup
> ```
>
> Global:
> ```bash
> npm install -g @scalekit-inc/cli
> scalekit setup
> ```
>
> Supports Claude Code, Cursor, GitHub Copilot, Codex + skills for 40+ agents.
> Features: full-stack-auth, agent-auth, mcp-auth, modular-sso, modular-scim.
> [Full setup guide](https://docs.scalekit.com/dev-kit/build-with-ai/)

---

# Build a daily briefing agent with Vercel AI SDK and Scalekit Agent Auth

Connect a TypeScript or Python agent via Vercel AI SDK and Scalekit AgentKit to Google Calendar and Gmail with authenticated tool calls.
A daily briefing agent needs two things: today's calendar events and the latest unread emails. Both live behind OAuth-protected APIs, and each requires its own token, its own authorization flow, and its own refresh logic. Before you write any scheduling logic, you're already maintaining two parallel token lifecycles.

Scalekit eliminates that overhead. It stores one OAuth session per connector per user, refreshes tokens automatically, and exposes **built-in tools** such as `googlecalendar_list_events` and `gmail_fetch_mails`. Your agent calls those tools through Scalekit; it never talks to the Google Calendar or Gmail REST APIs directly.

**What this recipe covers:**

- **Authorize once per connector** — create connected accounts for Calendar and Gmail, open the OAuth link if needed, and wait until status is `ACTIVE`
- **Built-in tool calls** — `execute_tool("googlecalendar_list_events")` and `execute_tool("gmail_fetch_mails")` so Scalekit runs the provider call and returns structured data
- **Wire both tools into an agent** — Vercel AI SDK (TypeScript) or Anthropic messages (Python)

The complete source used here is available in the [vercel-ai-agent-toolkit](https://github.com/scalekit-developers/vercel-ai-agent-toolkit) repository, with a TypeScript implementation using the Vercel AI SDK and a Python implementation using the Anthropic SDK directly.

### 1. Set up connections in Scalekit

In the [Scalekit Dashboard](https://app.scalekit.com), create two connections under **AgentKit** > **Connections** > **Create Connection**:

- `googlecalendar` — Google Calendar OAuth connection
- `gmail` — Gmail OAuth connection

The connection names are identifiers your code references directly. They must match exactly.

### 2. Install dependencies

  ### TypeScript

```bash
cd typescript
pnpm install
```

The `typescript/package.json` includes:

```json
{
  "dependencies": {
    "ai": "^4.3.15",
    "@ai-sdk/anthropic": "^1.2.12",
    "@scalekit-sdk/node": "2.2.0-beta.1",
    "zod": "^3.0.0",
    "dotenv": "^16.0.0"
  }
}
```

  ### Python

```bash
cd python
uv venv .venv
uv pip install -r requirements.txt
```

The `python/requirements.txt` includes:

```text
scalekit-sdk-python
anthropic
python-dotenv
```

### 3. Configure credentials

Copy the example env file and fill in your credentials:

   ```bash
   cp typescript/.env.example typescript/.env   # TypeScript
   cp typescript/.env.example python/.env       # Python (same variables)
   ```

   ```bash title=".env"
   SCALEKIT_ENV_URL=https://your-env.scalekit.dev
   SCALEKIT_CLIENT_ID=skc_...
   SCALEKIT_CLIENT_SECRET=your-secret

   ANTHROPIC_API_KEY=sk-ant-...
   ```

Get your Scalekit credentials at **app.scalekit.com → Settings → API Credentials**.

### 4. Initialize the Scalekit client

  ### TypeScript

```typescript
import { ScalekitClient } from '@scalekit-sdk/node';
import { ConnectorStatus } from '@scalekit-sdk/node/lib/pkg/grpc/scalekit/v1/connected_accounts/connected_accounts_pb.js';
import 'dotenv/config';

// Never hard-code credentials — they would be exposed in source control.
// Pull them from environment variables at runtime.
const scalekit = new ScalekitClient(
  process.env.SCALEKIT_ENV_URL!,
  process.env.SCALEKIT_CLIENT_ID!,
  process.env.SCALEKIT_CLIENT_SECRET!,
);
const actions = scalekit.actions;

const USER_ID = 'user_123'; // Replace with the real user ID from your session
```

`ConnectorStatus` is imported from the SDK's generated protobuf file. Compare `connectedAccount.status` against `ConnectorStatus.ACTIVE` rather than the string `'ACTIVE'` — TypeScript's type system enforces this.

  ### Python

```python
import os
import json
from datetime import datetime
from dotenv import load_dotenv
import anthropic
from scalekit import ScalekitClient

load_dotenv()

# Never hard-code credentials — they would be exposed in source control.
# Pull them from environment variables at runtime.
# Constructor: env_url, client_id, client_secret
scalekit_client = ScalekitClient(
    os.environ["SCALEKIT_ENV_URL"],
    os.environ["SCALEKIT_CLIENT_ID"],
    os.environ["SCALEKIT_CLIENT_SECRET"],
)
actions = scalekit_client.actions

USER_ID = "user_123"  # Replace with the real user ID from your session
```

`scalekit_client.actions` is the entry point for connected-account operations and built-in tool execution.

### 5. Ensure each connector is authorized

Before calling any API, check whether the user has an active connected account. If not, print an authorization link and wait for them to complete the browser OAuth flow.

  ### TypeScript

```typescript
async function ensureConnected(connectionName: string) {
  let { connectedAccount } = await actions.getOrCreateConnectedAccount({
    connectionName,
    identifier: USER_ID,
  });

  // Do not call tools until the user finishes OAuth and status is ACTIVE.
  if (connectedAccount?.status !== ConnectorStatus.ACTIVE) {
    const { link } = await actions.getAuthorizationLink({
      connectionName,
      identifier: USER_ID,
    });
    console.log(`\n[${connectionName}] Authorization required.`);
    console.log(`Open this link:\n\n  ${link}\n`);
    console.log('Press Enter once you have completed the OAuth flow...');
    await new Promise<void>(resolve => {
      process.stdin.resume();
      process.stdin.once('data', () => { process.stdin.pause(); resolve(); });
    });

    const refreshed = await actions.getOrCreateConnectedAccount({
      connectionName,
      identifier: USER_ID,
    });
    connectedAccount = refreshed.connectedAccount;
  }

  if (connectedAccount?.status !== ConnectorStatus.ACTIVE) {
    throw new Error(`${connectionName} is still not ACTIVE. Complete authorization and try again.`);
  }

  return connectedAccount;
}
```

  ### Python

```python
def ensure_connected(connection_name: str):
    response = actions.get_or_create_connected_account(
        connection_name=connection_name,
        identifier=USER_ID,
    )
    connected_account = response.connected_account

    # Do not call tools until the user finishes OAuth and status is ACTIVE.
    if connected_account.status != "ACTIVE":
        link_response = actions.get_authorization_link(
            connection_name=connection_name,
            identifier=USER_ID,
        )
        print(f"\n[{connection_name}] Authorization required.")
        print(f"Open this link:\n\n  {link_response.link}\n")
        input("Press Enter once you have completed the OAuth flow...")
        response = actions.get_or_create_connected_account(
            connection_name=connection_name,
            identifier=USER_ID,
        )
        connected_account = response.connected_account

    if connected_account.status != "ACTIVE":
        raise RuntimeError(
            f"{connection_name} is still not ACTIVE. Complete authorization and try again."
        )

    return connected_account
```

After the first successful authorization, `getOrCreateConnectedAccount` / `get_or_create_connected_account` returns an active account on all subsequent calls. Scalekit refreshes expired tokens automatically — your code never calls a token-refresh endpoint.

### 6. Fetch calendar events with a built-in tool

Call `execute_tool` with `googlecalendar_list_events`. Scalekit uses the stored OAuth session, calls Google Calendar, and returns structured event data. Your agent never handles a Google access token or the Calendar REST API.

  ### TypeScript

```typescript
import { tool } from 'ai';
import { z } from 'zod';

const getCalendarEvents = tool({
  description: "Fetch today's events from Google Calendar via Scalekit",
  parameters: z.object({
    maxResults: z.number().optional().default(5),
  }),
  execute: async ({ maxResults }) => {
    const response = await actions.executeTool({
      toolName: 'googlecalendar_list_events',
      connectedAccountId: calendarAccount?.id,
      toolInput: {
        max_results: maxResults,
      },
    });
    // Tool output lives under data — log once when integrating a new tool.
    return response.data ?? {};
  },
});
```

  ### Python

```python
def fetch_calendar_events(connected_account_id: str, max_results: int = 5) -> dict:
    response = actions.execute_tool(
        tool_name="googlecalendar_list_events",
        connected_account_id=connected_account_id,
        tool_input={
            "max_results": max_results,
        },
    )
    # Tool output lives under data — log once when integrating a new tool.
    return response.data
```

### 7. Fetch emails with a built-in tool

Use the same `execute_tool` pattern for Gmail with `gmail_fetch_mails`. Scalekit runs the Gmail API call and returns structured data.

  ### TypeScript

```typescript
const getUnreadEmails = tool({
  description: 'Fetch top unread emails from Gmail via Scalekit actions',
  parameters: z.object({
    maxResults: z.number().optional().default(5),
  }),
  execute: async ({ maxResults }) => {
    const response = await actions.executeTool({
      toolName: 'gmail_fetch_mails',
      connectedAccountId: gmailAccount?.id,
      toolInput: {
        query: 'is:unread',
        max_results: maxResults,
      },
    });
    return response.data ?? {};
  },
});
```

  ### Python

```python
def fetch_unread_emails(connected_account_id: str, max_results: int = 5) -> dict:
    response = actions.execute_tool(
        tool_name="gmail_fetch_mails",
        connected_account_id=connected_account_id,
        tool_input={
            "query": "is:unread",
            "max_results": max_results,
        },
    )
    return response.data
```

You do not need Google Calendar or Gmail API docs for the common path — tool names and parameters are consistent across Scalekit connectors. Browse [all supported agent connectors](/agentkit/connectors/) for the full tool list.

### 8. Wire the agent together

Pass both tools to the LLM and ask for a daily summary.

  ### TypeScript

The TypeScript version uses the Vercel AI SDK's `generateText` with `maxSteps` to allow the LLM to call multiple tools in sequence before producing the final response.

```typescript
import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';

const [calendarAccount, gmailAccount] = await Promise.all([
  ensureConnected('googlecalendar'),
  ensureConnected('gmail'),
]);

const today = new Date();

const { text } = await generateText({
  model: anthropic('claude-sonnet-4-6'),
  prompt: `Give me a summary of my day for ${today.toDateString()}: list today's calendar events and my top 5 unread emails.`,
  tools: {
    getCalendarEvents,
    getUnreadEmails,
  },
  maxSteps: 5, // allow the LLM to call multiple tools before responding
});

console.log(text);
```

`maxSteps` controls how many tool-call rounds the LLM can make before it must return a final text response. Without it, `generateText` stops after the first tool call.

  ### Python

The Python version uses the Anthropic SDK directly with a manual agentic loop. The loop continues until the model returns `stop_reason == "end_turn"` with no pending tool calls.

 ```python
 def run_agent():
     calendar_account = ensure_connected("googlecalendar")
     gmail_account = ensure_connected("gmail")

     client = anthropic.Anthropic()
     today = datetime.now().strftime("%A, %B %d, %Y")

     tools = [
         {
             "name": "get_calendar_events",
             "description": "Fetch today's events from Google Calendar via Scalekit",
             "input_schema": {
                 "type": "object",
                 "properties": {"max_results": {"type": "integer", "default": 5}},
             },
         },
         {
             "name": "get_unread_emails",
             "description": "Fetch top unread emails from Gmail via Scalekit actions",
             "input_schema": {
                 "type": "object",
                 "properties": {"max_results": {"type": "integer", "default": 5}},
             },
         },
     ]

     messages = [
         {
             "role": "user",
             "content": f"Give me a summary of my day for {today}: list today's calendar events and my top 5 unread emails.",
         }
     ]

     while True:
         response = client.messages.create(
             model="claude-sonnet-4-6",
             max_tokens=1024,
             tools=tools,
             messages=messages,
         )
         messages.append({"role": "assistant", "content": response.content})

         if response.stop_reason == "end_turn":
             for block in response.content:
                 if hasattr(block, "text"):
                     print(block.text)
             break

         tool_results = []
         for block in response.content:
             if block.type == "tool_use":
                 max_results = block.input.get("max_results", 5)
                 if block.name == "get_calendar_events":
                     result = fetch_calendar_events(calendar_account.id, max_results)
                 elif block.name == "get_unread_emails":
                     result = fetch_unread_emails(gmail_account.id, max_results)
                 else:
                     result = {"error": f"Unknown tool: {block.name}"}
                 tool_results.append({
                     "type": "tool_result",
                     "tool_use_id": block.id,
                     "content": json.dumps(result),
                 })

         if tool_results:
             messages.append({"role": "user", "content": tool_results})
         else:
             break

 if __name__ == "__main__":
     run_agent()
 ```

### 9. Testing

Run the agent:

  ### TypeScript

```bash
cd typescript && pnpm start
```

  ### Python

```bash
cd python && .venv/bin/python index.py
```

On first run, you see two authorization prompts in sequence:

```text
[googlecalendar] Authorization required.
Open this link:

  https://auth.scalekit.dev/connect/...

Press Enter once you have completed the OAuth flow...

[gmail] Authorization required.
Open this link:

  https://auth.scalekit.dev/connect/...

Press Enter once you have completed the OAuth flow...
```

After both connectors are authorized, the agent fetches your data and returns a summary:

```text
Here's your day for Friday, March 27, 2026:

📅 Calendar — 3 events today
• 9:00 AM  Team standup (30 min)
• 1:00 PM  Product review
• 4:00 PM  1:1 with manager

📧 Unread emails — top 5
• "Q1 roadmap feedback needed" — Sarah Chen, 1h ago
• "Deploy failed: production" — GitHub Actions, 2h ago
• "New PR review requested" — Lin Feng, 3h ago
...
```

On subsequent runs, both authorization prompts are skipped. Scalekit returns the active session directly.

## Common mistakes

## Connection name mismatch

- **Symptom**: `getOrCreateConnectedAccount` returns an error for `googlecalendar` or `gmail`
- **Cause**: The connection name in the Scalekit Dashboard does not match the literal string in your code
- **Fix**: Make the dashboard connection name match your code exactly, for example `googlecalendar` instead of `google-calendar`

## TypeScript status compared to a string

- **Symptom**: TypeScript raises `TS2367` for `connectedAccount?.status !== 'ACTIVE'`
- **Cause**: The SDK returns a `ConnectorStatus` enum, not a string literal
- **Fix**: Import `ConnectorStatus` from the SDK's generated protobuf file and compare against `ConnectorStatus.ACTIVE`

## `maxSteps` missing in the Vercel AI SDK

- **Symptom**: `generateText` stops after the first tool call instead of returning a final summary
- **Cause**: The model is not allowed to make enough tool-call rounds
- **Fix**: Set `maxSteps` to at least `3`, and increase it if your workflow needs more than one tool call plus a final response

## Tool called before authorization finishes

- **Symptom**: First run prints an auth link, then `execute_tool` fails because the account is not `ACTIVE`
- **Cause**: The sample continued to the tool call without waiting for the browser OAuth flow
- **Fix**: Block until the user completes authorization (for example `input(...)` in Python or a stdin wait in Node), then re-fetch the connected account before calling tools

## Production notes

**User ID from session** — Both implementations hardcode `USER_ID = "user_123"`. In production, replace this with the real user identifier from your application's session. A mismatch means Scalekit looks up the wrong user's connected accounts.

**Token freshness** — Scalekit refreshes OAuth tokens automatically before tool execution. You do not fetch provider tokens or call a refresh endpoint in application code.

**First-run blocking** — The authorization prompt blocks the process until the user completes OAuth in the browser. In a web application, redirect the user to `link` instead of printing it, and handle the callback before proceeding.

**`execute_tool` response shape** — Tool output lives under `response.data` (Python and Node). Keys inside `data` depend on the tool. Log the raw response once when integrating a new tool, then pass that structure to the LLM.

**Rate limits** — Google Calendar and Gmail both enforce per-user quotas. If your agent runs frequently, avoid tight polling loops and cache briefing data where freshness allows.

## Next steps

- **Add more connectors** — The same `ensureConnected` + `execute_tool` pattern works for any Scalekit-supported connector. Swap the connection name and tool name. See [all supported connectors](/agentkit/connectors/).
- **Need a raw provider call** — Prefer built-in tools first. If a tool does not cover your case, see [proxy API calls](/agentkit/advanced/proxy-api-calls/) rather than extracting tokens in app code.
- **Stream the response** — Replace `generateText` with `streamText` in the Vercel AI SDK to stream the LLM's summary token-by-token instead of waiting for the full response.
- **Handle re-authorization** — If a user revokes access, `getOrCreateConnectedAccount` returns an inactive account. Add a re-authorization path to recover gracefully instead of crashing.
- **Review the agent auth quickstart** — For a broader overview of the connected-accounts model and supported providers, see the [agent auth quickstart](/agentkit/quickstart/).


---

## 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 |
