Build Apify Actors that connect to users' SaaS accounts with per-user OAuth that persists across runs.

Scalekit is auth infrastructure for AI agents. With the Scalekit Node SDK inside your Actor, each user who runs it can connect their own third-party accounts — Notion, Gmail, Slack, Google Drive, and 90+ other services. Scalekit stores the OAuth tokens server-side, refreshes them automatically, and proxies every API call. Your Actor never touches a token directly.

What makes this different from wiring up OAuth yourself:

  • Zero-input user identity. Scalekit uses Actor.getEnv().userId as the connected-account key. The user never types an email or pastes a token — their Apify identity maps to their OAuth session automatically.
  • Tokens that survive across runs. Apify Actors are stateless containers. Scalekit stores tokens server-side keyed by (connector, identifier), so the first run completes OAuth and every subsequent run finds an active session instantly.
  • Built-in API tools. Instead of writing API clients for Notion, YouTube, or Gmail, call scalekit.actions.executeTool() with a tool name and input. Scalekit handles authentication, request formatting, and response parsing.
  • Interactive auth UX. When a user needs to authorize, your Actor serves a branded consent page on Apify's live-view port. The user clicks a button, completes OAuth, and the Actor continues automatically — no raw URLs buried in JSON output.
  • Shared and per-user in the same Actor. Use a derived identifier for private connectors (each user's own Notion) and a hardcoded identifier for shared ones (a company YouTube account). Both patterns coexist in a single Actor.


Why Scalekit + Apify?

Apify Actors run in isolated containers. Every run starts cold — no session, no cookies, no concept of "who is logged in." When your Actor needs to access a user's Notion workspace, send email through their Gmail, or read their Google Calendar, you need to solve three problems at once:

  1. Where do you store the OAuth token between runs? Actors have no persistent filesystem.
  2. How do you handle the OAuth redirect flow inside a container that only exposes a web server port?
  3. How do you refresh expired tokens without asking the user to re-authorize every time?

Doing this manually means building a token vault, writing redirect handlers, tracking expiry, and repeating all of it for every API you connect to. For Actors sold on the Apify Store — where hundreds of different users each need their own credentials — the complexity multiplies fast.

Scalekit solves all three with a connected-accounts model. For each (connector, identifier) pair, it stores one OAuth session and refreshes it automatically. Your Actor calls getOrCreateConnectedAccount to check the status (it's idempotent — safe to call on every run), getAuthorizationLink to generate a magic link when needed, and executeTool to make API calls. That's the entire surface area.

The result: you write an Actor that focuses on its actual job — research, automation, data processing — and delegates all auth mechanics to Scalekit.

Compare the two approaches:

Without Scalekit:

  1. Register an OAuth app with the third-party provider.
  2. Build a redirect handler that works inside an Actor container.
  3. Store the token somewhere persistent (key-value store, external DB).
  4. Track token expiry and refresh before every API call.
  5. Write an API client for each service you connect to.
  6. Repeat for every connector, for every user.

With Scalekit:

  1. Create a connection in the Scalekit dashboard.
  2. Call getOrCreateConnectedAccount() to check status.
  3. Call getAuthorizationLink() if the user hasn't authorized yet.
  4. Call executeTool() or actions.request() to make API calls.

Four function calls replace hundreds of lines of auth plumbing. And when you add a second connector — Gmail, Slack, Google Drive — you add four more function calls, not another OAuth integration.



Use case: Per-user OAuth Actors

The most common pattern is an Actor where each user connects their own SaaS accounts. A research agent that writes to the user's Notion. A scheduling assistant that reads the user's Google Calendar. A CRM sync tool that accesses the user's HubSpot.

How it works:

  1. The Actor reads Actor.getEnv().userId — a stable string unique to each Apify account.
  2. It calls getOrCreateConnectedAccount({ connectionName: 'notion', identifier: userId }).
  3. If the account is already ACTIVE, the Actor proceeds immediately — no auth step.
  4. If not, it generates a magic link, serves a branded auth page on the live-view port, and polls until the user completes OAuth.
  5. Once connected, every API call goes through executeTool(). Scalekit attaches the right token automatically.

The user authorizes once. Every future run for that Apify account skips the auth step entirely.

// Derive the identifier from Apify's runtime — no input field needed.
const { userId } = Actor.getEnv();
const notionIdentifier = userId;
// Check if this user's Notion account is already connected.
const resp = await scalekit.actions.getOrCreateConnectedAccount({
connectionName: 'notion',
identifier: notionIdentifier,
});
const account = resp.connectedAccount ?? resp;
if (account.status === 1) {
// ACTIVE — skip auth, start working.
console.log('Notion connected. Proceeding.');
}

This pattern works identically for any of the 90+ connectors Scalekit supports. Change connectionName and you're connected to Gmail, Slack, Google Drive, Jira, or any other service.



Use case: AI agents with tool auth

When your Actor is an LLM-driven agent, Scalekit's built-in tools let the agent call third-party APIs without you writing API clients. You define tool schemas, the LLM decides which tools to call, and Scalekit executes them with the right credentials attached.

How it works:

  1. Define tool schemas matching Scalekit's pre-built tools (e.g., notion_page_search, notion_page_content_append, youtube_search).
  2. Pass them to the LLM as function definitions.
  3. When the LLM calls a tool, execute it through scalekit.actions.executeTool().
  4. Scalekit authenticates the request, calls the third-party API, and returns the result.

The agent reasons about what to do. Scalekit handles how to authenticate.

// The LLM decided to search the user's Notion workspace.
// Execute the tool call through Scalekit — it handles auth automatically.
const result = await scalekit.actions.executeTool({
toolName: 'notion_page_search',
connectedAccountId: account.id,
toolInput: { query: 'Meeting Notes', page_size: 5 },
});
// Feed the result back to the LLM for reasoning.
messages.push({
role: 'tool_result',
tool_use_id: toolCall.id,
content: JSON.stringify(result),
});

This is the pattern used in the Notion + YouTube Agent , an open-source Apify Actor that accepts a natural-language task, connects to the user's Notion and a shared YouTube account, runs YouTube research, scores channels by relevance, and writes the results to a Notion page — all through Scalekit tool calls.



How it works

A complete Actor that connects to a user's Notion workspace and searches their pages:

import { Actor } from 'apify';
import { ScalekitClient } from '@scalekit-sdk/node';
await Actor.init();
try {
const input = await Actor.getInput();
const { task } = input;
// Initialize Scalekit with operator credentials (set as Actor env vars).
const scalekit = new ScalekitClient(
process.env.SCALEKIT_ENV_URL,
process.env.SCALEKIT_CLIENT_ID,
process.env.SCALEKIT_CLIENT_SECRET,
);
// Derive user identity from Apify's runtime.
// userId is stable per Apify account — same user, same token, every run.
const { userId } = Actor.getEnv();
const notionIdentifier = userId;
// Check if this user's Notion account is connected.
const resp = await scalekit.actions.getOrCreateConnectedAccount({
connectionName: 'notion',
identifier: notionIdentifier,
});
let account = resp.connectedAccount ?? resp;
// If not connected, generate a magic link for the user.
if (account.status !== 1) {
const { link } = await scalekit.actions.getAuthorizationLink({
connectionName: 'notion',
identifier: notionIdentifier,
});
// Surface the magic link in the Actor's output panel.
await Actor.setValue('OUTPUT', {
status: 'AWAITING_AUTH',
magicLink: link,
message: 'Open the magic link to authorize Notion.',
});
await Actor.setStatusMessage(`Authorize Notion → ${link}`);
// Poll until the user completes OAuth.
const deadline = Date.now() + 300_000;
let connected = false;
while (Date.now() < deadline) {
await new Promise(r => setTimeout(r, 5_000));
const poll = await scalekit.actions.getOrCreateConnectedAccount({
connectionName: 'notion',
identifier: notionIdentifier,
});
if ((poll.connectedAccount ?? poll).status === 1) {
account = poll.connectedAccount ?? poll;
connected = true;
break;
}
}
if (!connected) throw new Error('Timed out waiting for Notion authorization.');
}
// Execute a Notion tool call through Scalekit.
const result = await scalekit.actions.executeTool({
toolName: 'notion_page_search',
connectedAccountId: account.id,
toolInput: { query: task, page_size: 5 },
});
await Actor.setValue('OUTPUT', { status: 'DONE', task, result });
await Actor.pushData({ task, result });
console.log('Result:', JSON.stringify(result, null, 2));
} catch (err) {
console.error('Actor failed:', err.message);
await Actor.fail(err.message);
}
await Actor.exit();

The flow:

  1. Initialize — Scalekit client uses operator credentials from Actor environment variables.
  2. Identify — Actor.getEnv().userId maps the Apify user to a Scalekit connected account.
  3. Authorize — If the account isn't active, a magic link is surfaced in the Actor's output panel. The user clicks it, completes OAuth, and the Actor picks up automatically.
  4. Execute — executeTool() makes the Notion API call with the user's token. No API client, no token handling.

Set your Scalekit credentials as Actor environment variables in the Apify Console:

SCALEKIT_ENV_URL=https://your-env.scalekit.com
SCALEKIT_CLIENT_ID=skc_...
SCALEKIT_CLIENT_SECRET=your-secret

Install the SDK:

$npm install @scalekit-sdk/node apify

Alternative: direct API calls with actions.request()

executeTool() calls Scalekit's pre-built tools that return structured, AI-friendly responses. If you need to hit an arbitrary API endpoint — or one that doesn't have a pre-built tool — use actions.request() instead. Scalekit injects the user's token and handles refresh automatically; you just provide the path and method.

// Instead of executeTool(), call the Notion API directly via Scalekit's proxy.
// Scalekit attaches the user's token — no manual extraction needed.
const result = await scalekit.actions.request({
connectionName: 'notion',
identifier: notionIdentifier,
path: '/v1/search',
method: 'POST',
body: { query: task, page_size: 5 },
});

Both approaches use the same connected account and the same token vault. Choose executeTool() when a pre-built tool exists for what you need; choose actions.request() when you want full control over the API call.



Sample output

First run (user has not authorized Notion yet):

Actor web view: https://abc123--run456-4321.runs.apify.net
Notion account for "user_abc123" is PENDING_AUTH. Generating magic link...
ACTION REQUIRED: Authorize Notion → https://abc123--run456-4321.runs.apify.net
Waiting up to 300s for Notion authorization...
Notion account for "user_abc123" is now ACTIVE.

The Actor's OUTPUT panel shows:

{
"status": "AWAITING_AUTH",
"magicLink": "https://your-env.scalekit.com/authorize/notion?id=...",
"message": "Open the magic link to authorize Notion."
}

The user clicks the link, completes OAuth, and the Actor continues.

Second run (token already active — no auth step):

Notion account for "user_abc123" is already ACTIVE — skipping authorization.
[tool] notion_page_search → success (3 pages found)
[tool] notion_page_content_get → success
[done] Agent finished.
Result:
Found 3 pages matching "Q3 Planning":
1. "Q3 Planning — Engineering" (last edited 2 hours ago)
2. "Q3 Planning — Marketing" (last edited 1 day ago)
3. "Q3 Planning Notes" (last edited 3 days ago)


Sample agentic output

The Notion + YouTube Agent  demonstrates the full pattern. Given the task "Search YouTube for Python tutorial channels and append the top 10 to my Research page":

[YouTube] Expanding keyword...
Variations: [
'Python tutorial for beginners 2024',
'Python programming full course walkthrough',
'Python projects tutorial intermediate',
'Learn Python fast developer tutorial',
'Python tutorial automation scripting 2024'
]
[YouTube] Searching...
Found 31 unique channels
[YouTube] Fetching channel details...
Channel details fetched: 30/30
[YouTube] Scoring channels...
[tool] youtube_search_channels → success
[tool] notion_find_or_create_page → success
Created page "Research" (abc123-def456)
[tool] notion_page_content_append → success
Appended 81 blocks to "Research"
[done] Agent finished.
Top 10 Python Tutorial YouTube Channels:
| # | Channel | Subscribers | Score |
|----|----------------------|-------------|-------|
| 1 | Python Programmer | 781K | 10/10 |
| 2 | Tech With Tim | 2.0M | 9/10 |
| 3 | Programming with Mosh| 5.0M | 8/10 |
| 4 | freeCodeCamp.org | 11.6M | 8/10 |
| 5 | Bro Code | 3.2M | 8/10 |

Each channel entry in Notion includes the handle, video count, URL, relevance score with reasoning, and a sample video link. The agent searched YouTube, scored channels using an LLM, created a new Notion page, and wrote the results — all authenticated through Scalekit.



Scalekit capabilities

CapabilityDetail
OAuth connectors90+ pre-built connectors — Notion, Gmail, Google Calendar, Slack, HubSpot, Jira, GitHub, Salesforce, and more. Each connector handles redirect URIs, scopes, and token exchange.
Per-user connected accountsEach user gets their own OAuth session keyed by any unique identifier. Multiple users of the same Actor each connect their own accounts independently.
Shared connected accountsUse a hardcoded identifier for connectors shared across all users — e.g., a company YouTube or Slack account.
Automatic token refreshScalekit refreshes expired tokens automatically. If a refresh fails (e.g., the user revoked access), the account status changes to EXPIRED and your Actor re-runs the authorization flow.
Built-in API toolsCall third-party APIs via executeTool() for structured, AI-friendly responses, or actions.request() for direct API access. Scalekit handles auth and token refresh in both cases.
Magic link auth flowGenerate a one-time authorization link. The user completes OAuth in their browser; your Actor polls and continues automatically.
User verificationVerify the identity of the user completing the OAuth flow with verifyConnectedAccountUser(), ensuring the right person authorized the right account.
Live-view auth UXServe a branded OAuth consent page on Apify's live-view port instead of surfacing raw URLs.
Node.js and Python SDKs@scalekit-sdk/node and scalekit Python package. Both support connected accounts, tool execution, and authorization flows.


Services your Actors can connect to

Scalekit provides pre-built OAuth connectors and API tools for these services. Your Actor calls executeTool() or actions.request() — Scalekit handles authentication and API access either way.

  • Notion  — Search, read, create, and update pages and databases
  • Gmail  — Read, send, and manage email on behalf of users
  • Google Calendar  — Create events, check availability, and manage calendars
  • Google Drive  — Read, upload, and organize files and folders
  • Slack  — Send messages, read channels, and manage workspaces
  • HubSpot  — Access contacts, deals, and CRM data
  • GitHub  — Read repos, create issues, and manage pull requests
  • Jira  — Create and manage issues, projects, and boards
  • Salesforce  — Query and update CRM records
  • Linear  — Manage issues, projects, and engineering workflows

Browse all 90+ connectors → 


Getting started checklist

  1. Create a Scalekit environment — Sign up at app.scalekit.com  and note your SCALEKIT_ENV_URL, SCALEKIT_CLIENT_ID, and SCALEKIT_CLIENT_SECRET.

  2. Create connections — In the Scalekit dashboard, go to AgentKit → Connections and create connections for the services your Actor needs (e.g., Notion, YouTube, Gmail). For OAuth connectors, you'll enter the client ID and secret from the third-party provider.

  3. Set Actor environment variables — In the Apify Console, go to your Actor's Settings → Environment variables and add your three Scalekit credentials.

  4. Install the SDK — Add @scalekit-sdk/node to your Actor's package.json:

    $npm install @scalekit-sdk/node
  5. Initialize and connect — Use the pattern from the quickstart above: initialize the client, derive the identifier from Actor.getEnv().userId, check account status, and generate a magic link if needed.

  6. Deploy and test — Run apify push to deploy. On the first run, the Actor will surface an auth link. Complete the OAuth flow once, and every subsequent run will skip straight to execution.

For a complete walkthrough with troubleshooting tips, see the Apify Actor per-user OAuth cookbook .


Category

AI SDKs & frameworks

Website

scalekit.com 

Documentation

Learn more 

Cookbook

Apify Actor per-user OAuth 

Example repo

Notion + YouTube Agent 

Get started 

Build Actors that connect to your users' accounts

Category

AI SDKs & frameworks

Documentation

Learn more
Get started

Get data for your Scalekit workflows