Use built-in tools
Find the tools a user can call, run one with execute_tool, and pass the tool list to your model. Python, Node.js and cURL.
Every connector ships prebuilt tools, such as github_user_repos_list or slack_send_message. Each has a JSON Schema for its input and returns structured output. Your code names the tool and the user; Scalekit adds that user’s credentials, calls the app’s API and returns the result. This page finds a tool, runs it, and hands the tool list to your model.
Before you start
Section titled “Before you start”-
An
ACTIVEconnected account for the user. Authorize a user shows how to get one. -
The connection name from AgentKit > Connections. These examples use
github-connect, the GitHub connection new environments include. -
A client set up as in the Quickstart:
actionsin Python,scalekitin Node.js. For cURL, get an access token and keep it inTOKEN:Terminal window TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \-d grant_type=client_credentials \-d client_id="$SCALEKIT_CLIENT_ID" \-d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token)
-
Find the right tool
Section titled “Find the right tool”Search by the job you want done. Pass the user’s identifier to see, for each tool, whether that user can call it now.
from scalekit.v1.tools.tools_pb2 import ToolReadinessStateresults, _ = actions.tools.search_tools(query="list my GitHub repositories",identifier="user_123",top_k=5,)for tool in results.tools:ready = [c.connected_account_idfor c in tool.connectionsif c.readiness_state == ToolReadinessState.TOOL_READINESS_STATE_READY]print(tool.name, round(tool.score, 2), "ready" if ready else "not connected")import { ToolReadinessState } from '@scalekit-sdk/node';const { tools } = await scalekit.tools.searchTools('list my GitHub repositories', {identifier: 'user_123',topK: 5,});for (const tool of tools) {const ready = tool.connections.filter((c) => c.readinessState === ToolReadinessState.READY);console.log(tool.name, tool.score.toFixed(2), ready.length ? 'ready' : 'not connected');}Terminal window curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/tools:search" \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '{"query": "list my GitHub repositories", "identifier": "user_123", "top_k": 5}' |jq '.tools[] | {name, score, connections}'Each result has a
score(higher is closer) and aconnectionslist with areadiness_stateper connection:READY: the user has anACTIVEaccount on that connection. Call the tool now.NEEDS_CONNECTION: the account exists but isn’t active. Send a new authorization link.NEEDS_REAUTH: the token expired. Send a new authorization link.
A connector the user has never connected isn’t in
connectionsat all.To get the tools of specific connections instead, list the scoped tools. Pass the connections your agent works with, such as the ones the user picked in your app, and get the tools of those connections in one list:
- The user needs a connected account on each connection you pass. Otherwise the call fails.
- Pass one connection per connector. Two connections of the same connector, such as two GitHub connections, fail with
INVALID_ARGUMENT. - Pages hold up to 100 tools. Pass
next_page_tokenback until it’s empty.
The samples list the tools of
github-connectand of a Gmail connection namedgmail. Changegmailto a connection in your environment, or remove it.from google.protobuf.json_format import MessageToDictdefinitions = []page_token = Nonewhile True:page, _ = actions.tools.list_scoped_tools(identifier="user_123",filter={"connection_names": ["github-connect", "gmail"]},page_size=100,page_token=page_token,)definitions += [MessageToDict(t.tool)["definition"] for t in page.tools]page_token = page.next_page_tokenif not page_token:breakprint(len(definitions), [d["name"] for d in definitions[:5]])const definitions = [];let pageToken: string | undefined;do {const page = await scalekit.tools.listScopedTools('user_123', {filter: { connectionNames: ['github-connect', 'gmail'] },pageSize: 100,pageToken,});for (const t of page.tools) {if (t.tool?.definition) definitions.push(t.tool.definition);}pageToken = page.nextPageToken || undefined;} while (pageToken);console.log(definitions.length, definitions.slice(0, 5).map((d) => d.name));Terminal window curl -sS -G "$SCALEKIT_ENVIRONMENT_URL/api/v1/tools/scoped" \-H "Authorization: Bearer $TOKEN" \--data-urlencode "identifier=user_123" \--data-urlencode "filter.connection_names=github-connect" \--data-urlencode "filter.connection_names=gmail" \--data-urlencode "page_size=100" |jq '{names: [.tools[].tool.definition.name], next_page_token}' -
Call the tool
Section titled “Call the tool”Pass the tool name, its input, and the user. Identify the user with
identifierplus the connection name, or withconnected_account_idalone, which you get from a search result’sconnectionsor fromget_connected_account. When you passconnected_account_id, Scalekit ignoresidentifier.from scalekit.common.exceptions import ScalekitServerExceptiontry:result = actions.execute_tool(tool_name="github_user_repos_list",identifier="user_123",connection_name="github-connect",tool_input={"per_page": 5, "sort": "updated"},)print(result.execution_id)print(result.data)except ScalekitServerException as e:print("Tool call failed:", e.error_code, e.message)import { ScalekitServerException } from '@scalekit-sdk/node';try {const result = await scalekit.actions.executeTool({toolName: 'github_user_repos_list',identifier: 'user_123',connector: 'github-connect',toolInput: { per_page: 5, sort: 'updated' },});console.log(result.executionId);console.log(result.data);} catch (err) {if (!(err instanceof ScalekitServerException)) throw err;console.error('Tool call failed:', err.errorCode, err.message);}Terminal window curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/execute_tool" \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '{"tool_name": "github_user_repos_list","connector": "github-connect","identifier": "user_123","params": {"per_page": 5, "sort": "updated"}}' |jq '{execution_id, data}' -
Read the result
Section titled “Read the result”The response wraps the app’s output. Read the tool’s fields from
data, and keepexecution_idto find the call in your logs.Some apps report an error in a successful (
2xx) response. Then the call still returns, anddataholds the app’s error. Slack, for example, returns:{ "ok": false, "error": "missing_scope", "needed": "users:read" }Check
datafor the app’s error shape before you use it. When the app answers with an error status instead, such as401,403,429or another4xxor5xx, the call fails with an error whose message starts withtool execution failed. See Common problems. -
Give the tools to your model
Section titled “Give the tools to your model”Tool definitions carry more than a model needs. Keep
name,descriptionandinput_schema, a JSON Schema object for the tool’s input. Anthropic’s API takes it asinput_schemaunchanged, and OpenAI’s takes it as a function’sparameters. This continues from the list in step 1.llm_tools = [{"name": d["name"],"description": d.get("description", ""),"input_schema": d.get("input_schema", {"type": "object", "properties": {}}),}for d in definitions]read_only = [d["name"] for d in definitions if d.get("annotations", {}).get("read_only_hint")]print(len(llm_tools), "tools,", len(read_only), "read-only")const llmTools = definitions.map((d) => ({name: String(d.name),description: String(d.description ?? ''),input_schema: d.input_schema ?? { type: 'object', properties: {} },}));const readOnly = definitions.filter((d) => (d.annotations as { read_only_hint?: boolean } | undefined)?.read_only_hint,);console.log(llmTools.length, 'tools,', readOnly.length, 'read-only');Terminal window curl -sS -G "$SCALEKIT_ENVIRONMENT_URL/api/v1/tools/scoped" \-H "Authorization: Bearer $TOKEN" \--data-urlencode "identifier=user_123" \--data-urlencode "filter.connection_names=github-connect" \--data-urlencode "page_size=100" |jq '[.tools[].tool.definition | {name, description, input_schema}]'Each definition’s
annotationsincluderead_only_hintanddestructive_hint. Use them to start an agent with read-only tools, or to ask the user before a destructive call.When the model asks for a tool, call it as in step 2 and send
databack as the tool result. LangChain and Google ADK get native tool objects fromactions.langchain.get_toolsandactions.google.get_tools. Choose a framework has a complete agent for each framework.
Check it worked
Section titled “Check it worked”The call returns an execution_id and a data object with the app’s response, such as a list of repositories for github_user_repos_list. Search results for the user show READY for the connection you called.
Common problems
Section titled “Common problems”failed to get tool: <tool_name>
Section titled “failed to get tool: <tool_name>”That tool name doesn’t exist in your environment. Use a name from search or from the scoped list, exactly as returned.
connection not found for the given key
Section titled “connection not found for the given key”The connection name doesn’t match AgentKit > Connections exactly. Connection names can differ between environments, so read them from configuration rather than hard-coding them.
connected account not found
Section titled “connected account not found”No connected account exists for this identifier on this connection. Check that you pass the same identifier you used when the user authorized, or authorize the user first.
connected account is not active
Section titled “connected account is not active”The user hasn’t finished authorizing, or the account expired or was disconnected. Send a new authorization link and wait until the status is ACTIVE.
missing or invalid <parameter> parameter
Section titled “missing or invalid <parameter> parameter”The input doesn’t match the tool’s input_schema. Check required fields and types in the tool’s definition.
The call returns, but data holds an error such as missing_scope
Section titled “The call returns, but data holds an error such as missing_scope”The app refused the call in a 2xx response. For a missing scope, add the scope to the connection, then have the user authorize again so their token includes it.
tool execution failed - unauthorized access
Section titled “tool execution failed - unauthorized access”The app answered 401: it rejected the user’s token. Check the connected account’s status, and if the user revoked access, send a new authorization link.
tool execution failed - forbidden access
Section titled “tool execution failed - forbidden access”The app answered 403. The user’s token is usually missing a scope the tool needs, or the user lacks permission in the app. Add the scope to the connection, then have the user authorize again.
tool execution failed - rate limited
Section titled “tool execution failed - rate limited”The app answered 429. Wait before you retry, and slow down calls for this user.
tool execution failed - <message>
Section titled “tool execution failed - <message>”The app answered with another error status. The rest of the message is the app’s own error. Check the input against the tool’s input_schema and the app’s API reference.
To serve these tools to an MCP client instead, create a Virtual MCP server.
API reference
The endpoints this page's code calls, with every field and the SDK method for each: