Setup Arcade with OpenAI Agents (TypeScript)
The OpenAI Agents SDK for JavaScript provides a framework for building AI in TypeScript and JavaScript. Arcade’s @arcadeai/arcadejs library converts Arcade to the format OpenAI Agents expects.
Outcomes
Build an that uses Arcade to help with Gmail and Slack
You will Learn
- How to retrieve Arcade tools and convert them to OpenAI format
- How to use the factory pattern for
- How to handle authorization with
executeOrAuthorizeZodTool
Prerequisites
- Arcade account
- Obtain an Arcade API key
- Node.js 18+ (includes npm) or Bun
How Arcade integrates with OpenAI Agents
The OpenAI JS SDK uses Zod schemas for definitions. Arcade’s toZod() function converts Arcade tool definitions to Zod schemas, and the executeFactory parameter determines how tools execute.
TypeScript uses a factory pattern to bind to functions. executeFactory receives (client, userId, tool definition) and returns an execute function with that context “baked in”.
Build the agent
Create a new project
Create a new directory and install dependencies:
npm
mkdir openai-agents-arcade-ts
cd openai-agents-arcade-ts
npm init -y
npm install @openai/agents @arcadeai/arcadejs dotenvCreate a .env file with your :
# Arcade API key from https://app.arcade.dev/api-keys
ARCADE_API_KEY=YOUR_ARCADE_API_KEY
# Your Arcade user ID (the email you used to sign up)
ARCADE_USER_ID={arcade_user_id}
# OpenAI API key
OPENAI_API_KEY=YOUR_OPENAI_API_KEYCreate agent file
Create index.ts (or index.mjs for plain JavaScript):
import Arcade from "@arcadeai/arcadejs";
import { executeOrAuthorizeZodTool, toZod } from "@arcadeai/arcadejs/lib/index";
import { Agent, run, tool } from "@openai/agents";
import "dotenv/config";
import readline from "node:readline/promises";
// Configuration
const ARCADE_USER_ID = process.env.ARCADE_USER_ID || "default-user";
const MCP_SERVERS = ["Slack"];
const INDIVIDUAL_TOOLS = ["Gmail_ListEmails", "Gmail_SendEmail", "Gmail_WhoAmI"];
const SYSTEM_PROMPT = "You are a helpful assistant that can assist with Gmail and Slack.";
const MODEL = "gpt-4o-mini";Retrieve and convert Arcade tools
Use toZod() to convert Arcade to Zod schemas, then tool() to convert them to OpenAI format:
async function getArcadeTools(client: Arcade, userId: string) {
// Fetch tools from MCP servers
const mcpServerTools = await Promise.all(
MCP_SERVERS.map(async (serverName) => {
const response = await client.tools.list({
toolkit: serverName,
limit: 30,
});
return response.items;
})
);
// Fetch individual tools by name
const individualToolDefs = await Promise.all(
INDIVIDUAL_TOOLS.map((toolName) => client.tools.get(toolName))
);
// Combine and deduplicate
const allTools = [...mcpServerTools.flat(), ...individualToolDefs];
const uniqueTools = Array.from(
new Map(allTools.map((t) => [t.qualified_name, t])).values()
);
// Convert to Zod format with the execute factory
// This is the TypeScript equivalent of Python's functools.partial -
// the factory receives context and returns an execute function
const zodTools = toZod({
tools: uniqueTools,
client,
userId,
executeFactory: executeOrAuthorizeZodTool,
});
// Convert Zod tools to OpenAI Agents format
return zodTools.map(tool);
}Understanding the factory pattern: In TypeScript, executeFactory binds context to a function. It receives the (client, userId, tool definition) and returns an execute function with that already bound.
Create and run the agent
async function main() {
// Initialize Arcade client
const client = new Arcade();
// Get tools converted to OpenAI Agents format
const tools = await getArcadeTools(client, ARCADE_USER_ID);
// Create the agent
const agent = new Agent({
name: "Inbox Assistant",
instructions: SYSTEM_PROMPT,
model: MODEL,
tools,
});
// Set up interactive chat
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.log("Hello! I'm your helpful OpenAI Agent! I use Arcade Tools to access your Gmail and Slack. Try asking me to summarize your recent emails or DM you on Slack!\n\nType 'exit' to quit.\n");
// Track conversation history for multi-turn context
let conversationHistory: any[] = [];
while (true) {
const input = await rl.question("> ");
if (input.toLowerCase() === "exit") {
break;
}
try {
// Pass conversation history for context, add new user message
const result = await run(
agent,
conversationHistory.concat({ role: "user", content: input })
);
// Update history with full conversation
conversationHistory = result.history;
console.log(`\n${result.finalOutput}\n`);
} catch (error) {
console.error("Error:", error);
}
}
console.log("Goodbye!");
rl.close();
process.exit(0);
}
main().catch(console.error);Handle authorization
The executeOrAuthorizeZodTool factory automatically handles authorization. When a tool needs OAuth authorization (like Gmail), instead of throwing an error, it returns a response with the authorization URL. The ’s output will include something like the following:
Please authorize access: https://accounts.google.com/...After the visits the URL and authorizes, running the same request again will succeed.
For more control over authorization flow, you can create a custom execute factory:
import { isAuthorizationRequiredError, executeZodTool } from "@arcadeai/arcadejs/lib/index";
// Custom factory that waits for authorization
function executeWithWait({ zodToolSchema, toolDefinition, client, userId }) {
return async (input) => {
try {
return await executeZodTool({ zodToolSchema, toolDefinition, client, userId })(input);
} catch (error) {
if (error instanceof Error && isAuthorizationRequiredError(error)) {
const response = await client.tools.authorize({
tool_name: zodToolSchema.name,
user_id: userId,
});
if (response.status !== "completed") {
console.log(`Please authorize: ${response.url}`);
// Wait for the user to complete authorization
await client.auth.waitForCompletion(response);
}
// Retry after authorization
return await executeZodTool({ zodToolSchema, toolDefinition, client, userId })(input);
}
throw error;
}
};
}
// Use with toZod
const zodTools = toZod({
tools: uniqueTools,
client,
userId,
executeFactory: executeWithWait,
});Run the agent
npm
Use npx (included with npm) to run the TypeScript file:
npx tsx index.tsKey takeaways
toZod()converts Arcade tools to Zod schemas that OpenAI can useexecuteFactoryis the TypeScript equivalent of Python’sfunctools.partial- it binds to execute functionsexecuteOrAuthorizeZodToolautomatically returns authorization URLs when need OAuthisAuthorizationRequiredErrorlets you detect and handle authorization errors in custom factories- The
userIdparameter tracks authorization per - use a consistent ID for each user result.historycontains the full conversation history - pass it back torun()for multi-turn
Complete code
import Arcade from "@arcadeai/arcadejs";
import { executeOrAuthorizeZodTool, toZod } from "@arcadeai/arcadejs/lib/index";
import { Agent, run, tool } from "@openai/agents";
import "dotenv/config";
import readline from "node:readline/promises";
// Configuration
const ARCADE_USER_ID = process.env.ARCADE_USER_ID || "default-user";
const MCP_SERVERS = ["Slack"];
const INDIVIDUAL_TOOLS = ["Gmail_ListEmails", "Gmail_SendEmail", "Gmail_WhoAmI"];
const SYSTEM_PROMPT = "You are a helpful assistant that can assist with Gmail and Slack.";
const MODEL = "gpt-4o-mini";
async function getArcadeTools(client: Arcade, userId: string) {
// Fetch tools from MCP servers
const mcpServerTools = await Promise.all(
MCP_SERVERS.map(async (serverName) => {
const response = await client.tools.list({
toolkit: serverName,
limit: 30,
});
return response.items;
})
);
// Fetch individual tools by name
const individualToolDefs = await Promise.all(
INDIVIDUAL_TOOLS.map((toolName) => client.tools.get(toolName))
);
// Combine and deduplicate
const allTools = [...mcpServerTools.flat(), ...individualToolDefs];
const uniqueTools = Array.from(
new Map(allTools.map((t) => [t.qualified_name, t])).values()
);
// Convert to Zod format with the execute factory
const zodTools = toZod({
tools: uniqueTools,
client,
userId,
executeFactory: executeOrAuthorizeZodTool,
});
// Convert Zod tools to OpenAI Agents format
return zodTools.map(tool);
}
async function main() {
const client = new Arcade();
const tools = await getArcadeTools(client, ARCADE_USER_ID);
const agent = new Agent({
name: "Inbox Assistant",
instructions: SYSTEM_PROMPT,
model: MODEL,
tools,
});
// Set up interactive chat
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.log("Hello! I'm your helpful OpenAI Agent! I use Arcade Tools to access your Gmail and Slack. Try asking me to summarize your recent emails or DM you on Slack!\n\nType 'exit' to quit.\n");
// Track conversation history for multi-turn context
let conversationHistory: any[] = [];
while (true) {
const input = await rl.question("> ");
if (input.toLowerCase() === "exit") {
break;
}
try {
const result = await run(
agent,
conversationHistory.concat({ role: "user", content: input })
);
conversationHistory = result.history;
console.log(`\n${result.finalOutput}\n`);
} catch (error) {
console.error("Error:", error);
}
}
console.log("Goodbye!");
rl.close();
process.exit(0);
}
main().catch(console.error);Next steps
- Add more by modifying
MCP_SERVERSandINDIVIDUAL_TOOLS - Build a web interface using frameworks like Next.js or Express
- See the Vercel AI SDK tutorial or TanStack AI tutorial for complete web chatbot examples
- Explore creating custom tools with the Arcade SDK