Scalekit
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().userIdas 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.
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:
- Where do you store the OAuth token between runs? Actors have no persistent filesystem.
- How do you handle the OAuth redirect flow inside a container that only exposes a web server port?
- 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:
- Register an OAuth app with the third-party provider.
- Build a redirect handler that works inside an Actor container.
- Store the token somewhere persistent (key-value store, external DB).
- Track token expiry and refresh before every API call.
- Write an API client for each service you connect to.
- Repeat for every connector, for every user.
With Scalekit:
- Create a connection in the Scalekit dashboard.
- Call
getOrCreateConnectedAccount()to check status. - Call
getAuthorizationLink()if the user hasn't authorized yet. - Call
executeTool()oractions.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.
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:
- The Actor reads
Actor.getEnv().userId— a stable string unique to each Apify account. - It calls
getOrCreateConnectedAccount({ connectionName: 'notion', identifier: userId }). - If the account is already ACTIVE, the Actor proceeds immediately — no auth step.
- If not, it generates a magic link, serves a branded auth page on the live-view port, and polls until the user completes OAuth.
- 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.
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:
- Define tool schemas matching Scalekit's pre-built tools (e.g.,
notion_page_search,notion_page_content_append,youtube_search). - Pass them to the LLM as function definitions.
- When the LLM calls a tool, execute it through
scalekit.actions.executeTool(). - 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.
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:
- Initialize — Scalekit client uses operator credentials from Actor environment variables.
- Identify —
Actor.getEnv().userIdmaps the Apify user to a Scalekit connected account. - 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.
- 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.comSCALEKIT_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.
First run (user has not authorized Notion yet):
Actor web view: https://abc123--run456-4321.runs.apify.netNotion account for "user_abc123" is PENDING_AUTH. Generating magic link...ACTION REQUIRED: Authorize Notion → https://abc123--run456-4321.runs.apify.netWaiting 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)
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 → successCreated page "Research" (abc123-def456)[tool] notion_page_content_append → successAppended 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.
| Capability | Detail |
|---|---|
| OAuth connectors | 90+ 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 accounts | Each 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 accounts | Use a hardcoded identifier for connectors shared across all users — e.g., a company YouTube or Slack account. |
| Automatic token refresh | Scalekit 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 tools | Call 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 flow | Generate a one-time authorization link. The user completes OAuth in their browser; your Actor polls and continues automatically. |
| User verification | Verify the identity of the user completing the OAuth flow with verifyConnectedAccountUser(), ensuring the right person authorized the right account. |
| Live-view auth UX | Serve 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. |
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
-
Create a Scalekit environment — Sign up at app.scalekit.com and note your
SCALEKIT_ENV_URL,SCALEKIT_CLIENT_ID, andSCALEKIT_CLIENT_SECRET. -
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.
-
Set Actor environment variables — In the Apify Console, go to your Actor's Settings → Environment variables and add your three Scalekit credentials.
-
Install the SDK — Add
@scalekit-sdk/nodeto your Actor'spackage.json:$npm install @scalekit-sdk/node -
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. -
Deploy and test — Run
apify pushto 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
Documentation
Cookbook
Example repo