Setup Arcade with LangChain
LangChain is a popular agentic framework that abstracts a lot of the complexity of building AI agents. It is built on top of LangGraph, a lower level orchestration framework that offers more control over the inner flow of the .
Outcomes
Learn how to integrate Arcade using LangChain primitives
You will Learn
- How to retrieve Arcade and transform them into LangChain tools
- How to build a LangChain
- How to integrate Arcade into the agentic flow
- How to manage Arcade authorization using LangChain interrupts
Prerequisites
A primer on agents and interrupts
LangChain provides multiple abstractions for building AI , and it’s very useful to internalize how some of these primitives work, so you can understand and extend the different agentic patterns LangChain supports.
- : Most agentic frameworks, including LangChain, provide an abstraction for a ReAct agent. This is the most common agentic pattern, where an LLM runs in a loop, with the option to call tools to perform actions or retrieve information into its . The initial input is a system prompt together with a user prompt. The agent may then iteratively call tools and update its context until it generates a response that does not involve a call.
- Interrupts: Interrupts in LangChain are a way to control the flow of the agentic loop when something needs to be done outside of the normal ReAct flow. For example, if a tool requires authorization, we can interrupt the and ask the user to authorize the before continuing.
Integrate Arcade tools into a LangChain agent
Create a new project
mkdir langchain-arcade-example
cd langchain-arcade-example
bun install @arcadeai/arcadejs langchain @langchain/openai @langchain/core @langchain/langgraphCreate a new file called .env and add the following :
ARCADE_API_KEY=YOUR_ARCADE_API_KEY
OPENAI_API_KEY=YOUR_OPENAI_API_KEYImport the necessary packages
Create a new file called main.ts and add the following code:
"use strict";
import { Arcade } from "@arcadeai/arcadejs";
import {
type ToolExecuteFunctionFactoryInput,
executeZodTool,
isAuthorizationRequiredError,
toZod,
} from "@arcadeai/arcadejs/lib";
import { type ToolExecuteFunction } from "@arcadeai/arcadejs/lib/zod/types";
import { createAgent, tool } from "langchain";
import {
Command,
interrupt,
MemorySaver,
type Interrupt,
} from "@langchain/langgraph";
import chalk from "chalk";
import readline from "node:readline/promises";This is quite a number of imports, let’s break them down:
- Arcade imports:
Arcade: This is the , you’ll use it to interact with the Arcade API.type ToolExecuteFunctionFactoryInput: This type encodes the input to execute Arcade .executeZodTool: You’ll use this function to execute an Arcade .isAuthorizationRequiredError: You’ll use this function to check if a requires authorization.toZod: You’ll use this function to convert an Arcade definition into a Zod schema.
- LangChain imports:
createAgent: This is the main function to create a LangChain .tool: You’ll use this function to turn an Arcade definition into a LangChain tool.Command: You’ll use this class to communicate the user’s decisions to the ’s interrupts.interrupt: You’ll use this function to interrupt the ReAct flow and ask the for input.MemorySaver: This is a LangGraph construct that stores the ’s state, and is required for checkpointing and interrupts.
- Other imports:
chalk: This is a library to colorize the console output.readline: This is a library to read input from the console.
Write a helper function to execute Arcade tools
function executeOrInterruptTool({
zodToolSchema,
toolDefinition,
client,
userId,
}: ToolExecuteFunctionFactoryInput): ToolExecuteFunction<any> {
const { name: toolName } = zodToolSchema;
return async (input: unknown) => {
try {
// Try to execute the tool
const result = await executeZodTool({
zodToolSchema,
toolDefinition,
client,
userId,
})(input);
return result;
} catch (error) {
// If the tool requires authorization, we interrupt the flow and ask the user to authorize the tool
if (error instanceof Error && isAuthorizationRequiredError(error)) {
const response = await client.tools.authorize({
tool_name: toolName,
user_id: userId,
});
// We interrupt the flow here, and pass everything the handler needs to get the user's authorization
const interrupt_response = interrupt({
authorization_required: true,
authorization_response: response,
tool_name: toolName,
url: response.url ?? "",
});
// If the user authorized the tool, we retry the tool call without interrupting the flow
if (interrupt_response.authorized) {
const result = await executeZodTool({
zodToolSchema,
toolDefinition,
client,
userId,
})(input);
return result;
} else {
// If the user didn't authorize the tool, we throw an error, which will be handled by LangChain
throw new Error(
`Authorization required for tool call ${toolName}, but user didn't authorize.`
);
}
}
throw error;
}
};
}In essence, this function is a wrapper around the executeZodTool function. When it fails, you interrupt the flow and send the authorization request for the harness to handle (the app that is running the ). If the user authorizes the , the harness will reply with an {authorized: true} object, and the tool call will be retried without interrupting the flow.
Retrieve Arcade tools and transform them into LangChain tools
// Initialize the Arcade client
const arcade = new Arcade();
// Get the Arcade tools, you can customize the MCP Server (e.g. "github", "notion", "gmail", etc.)
const googleToolkit = await arcade.tools.list({ toolkit: "gmail", limit: 30 });
const arcadeTools = toZod({
tools: googleToolkit.items,
client: arcade,
executeFactory: executeOrInterruptTool,
userId: "{arcade_user_id}", // Replace this with your application's user ID (e.g. email address, UUID, etc.)
});
// Convert Arcade tools to LangGraph tools
const tools = arcadeTools.map(({ name, description, execute, parameters }) =>
(tool as Function)(execute, {
name,
description,
schema: parameters,
})
);Here you get the Arcade tools we want to use in our agent, and transform them into LangChain tools. The first step is to initialize the , and get the we want to use. Then, use the toZod function to convert the Arcade tools into a Zod schema, and pass it to the executeOrInterruptTool function to create a LangChain tool.
Write the interrupt handler
async function handleAuthInterrupt(
interrupt: Interrupt
): Promise<{ authorized: boolean }> {
const value = interrupt.value;
const authorization_required = value.authorization_required;
if (authorization_required) {
const tool_name = value.tool_name;
const authorization_response = value.authorization_response;
console.log("⚙️: Authorization required for tool call", tool_name);
console.log("⚙️: Authorization URL", authorization_response.url);
console.log("⚙️: Waiting for authorization to complete...");
try {
await arcade.auth.waitForCompletion(authorization_response.id);
console.log("⚙️: Authorization granted. Resuming execution...");
return { authorized: true };
} catch (error) {
console.error("⚙️: Error waiting for authorization to complete:", error);
return { authorized: false };
}
}
return { authorized: false };
}This helper function receives an interrupt object and returns a decision object. In LangChain, decisions can be of any serializable type, so you can return any object you need to continue the flow. In this case, we return an object with a boolean flag indicating if the authorization was successful.
It is important to note that this function captures the authorization flow outside of the agent’s , which is a good practice for security and context engineering. By handling everything in the harness, you remove the risk of the LLM replacing the authorization URL or leaking it, and you keep the context free from any authorization-related traces, which reduces the risk of hallucinations.
Create the agent
const agent = createAgent({
systemPrompt:
"You are a helpful assistant that can use GMail tools. Your main task is to help the user with anything they may need.",
model: "gpt-4o-mini",
tools: tools,
checkpointer: new MemorySaver(),
});Here you create the using the createAgent function. You pass the system prompt, the model, the tools, and the checkpointer. When the agent runs, it will automatically use the helper function you wrote earlier to handle calls and authorization requests.
Write the invoke helper
async function streamAgent(
agent: any,
input: any,
config: any
): Promise<Interrupt[]> {
const stream = await agent.stream(input, {
...config,
streamMode: "updates",
});
const interrupts: Interrupt[] = [];
for await (const chunk of stream) {
if (chunk.__interrupt__) {
interrupts.push(...(chunk.__interrupt__ as Interrupt[]));
continue;
}
for (const update of Object.values(chunk)) {
for (const msg of (update as any)?.messages ?? []) {
console.log("🤖: ", msg.toFormattedString());
}
}
}
return interrupts;
}This last helper function handles the streaming of the ’s response, and captures the interrupts. When an interrupt is detected, it is added to the interrupts array, and the flow is interrupted. If there are no interrupts, it will just stream the agent’s to the console.
Write the main function
Finally, write the main function that will call the and handle the input.
async function main() {
const config = { configurable: { thread_id: "1" } };
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.log(chalk.green("Welcome to the chatbot! Type 'exit' to quit."));
while (true) {
const input = await rl.question("> ");
if (input.toLowerCase() === "exit") {
break;
}
rl.pause();
try {
// Stream the agent response and collect any interrupts
const interrupts = await streamAgent(
agent,
{
messages: [{ role: "user", content: input }],
},
config
);
// Handle authorization interrupts
const decisions: any[] = [];
for (const interrupt of interrupts) {
decisions.push(await handleAuthInterrupt(interrupt));
}
// Resume agent after authorization
if (decisions.length > 0) {
await streamAgent(
agent,
new Command({ resume: { decisions } }),
config
);
}
} catch (error) {
console.error(error);
}
rl.resume();
}
console.log(chalk.red("👋 Bye..."));
process.exit(0);
}
// Run the main function
main().catch((err) => console.error(err));Here the config object is used to configure the thread ID, which tells the to store the state of the conversation in a specific thread. Like any typical agent loop, you:
- Capture the input
- Stream the ’s response
- Handle any authorization interrupts
- Resume the after authorization
- Handle any errors
- Exit the loop if the wants to quit
Run the agent
bun run main.tsYou should see the responding to your prompts like any model, as well as handling any calls and authorization requests. Here are some example prompts you can try:
- “Send me an email with a random haiku about LangChain ”
- “Summarize my latest 3 emails”
Key takeaways
- Arcade can be integrated into any agentic framework like LangChain, all you need is to transform the Arcade tools into LangChain tools and handle the authorization flow.
- isolation: By handling the authorization flow outside of the ’s context, you remove the risk of the LLM replacing the authorization URL or leaking it, and you keep the context free from any authorization-related traces, which reduces the risk of hallucinations.
- You can leverage the interrupts mechanism to handle human intervention in the ’s flow, useful for authorization flows, policy enforcement, or anything else that requires input from the .
Example code
"use strict";
import { Arcade } from "@arcadeai/arcadejs";
import {
type ToolExecuteFunctionFactoryInput,
executeZodTool,
isAuthorizationRequiredError,
toZod,
} from "@arcadeai/arcadejs/lib";
import { type ToolExecuteFunction } from "@arcadeai/arcadejs/lib/zod/types";
import { createAgent, tool } from "langchain";
import {
Command,
interrupt,
MemorySaver,
type Interrupt,
} from "@langchain/langgraph";
import chalk from "chalk";
import readline from "node:readline/promises";
function executeOrInterruptTool({
zodToolSchema,
toolDefinition,
client,
userId,
}: ToolExecuteFunctionFactoryInput): ToolExecuteFunction<any> {
const { name: toolName } = zodToolSchema;
return async (input: unknown) => {
try {
// Try to execute the tool
const result = await executeZodTool({
zodToolSchema,
toolDefinition,
client,
userId,
})(input);
return result;
} catch (error) {
// If the tool requires authorization, we interrupt the flow and ask the user to authorize the tool
if (error instanceof Error && isAuthorizationRequiredError(error)) {
const response = await client.tools.authorize({
tool_name: toolName,
user_id: userId,
});
// We interrupt the flow here, and pass everything the handler needs to get the user's authorization
const interrupt_response = interrupt({
authorization_required: true,
authorization_response: response,
tool_name: toolName,
url: response.url ?? "",
});
// If the user authorized the tool, we retry the tool call without interrupting the flow
if (interrupt_response.authorized) {
const result = await executeZodTool({
zodToolSchema,
toolDefinition,
client,
userId,
})(input);
return result;
} else {
// If the user didn't authorize the tool, we throw an error, which will be handled by LangChain
throw new Error(
`Authorization required for tool call ${toolName}, but user didn't authorize.`
);
}
}
throw error;
}
};
}
// Initialize the Arcade client
const arcade = new Arcade();
// Get the Arcade tools, you can customize the MCP Server (e.g. "github", "notion", "gmail", etc.)
const googleToolkit = await arcade.tools.list({ toolkit: "gmail", limit: 30 });
const arcadeTools = toZod({
tools: googleToolkit.items,
client: arcade,
executeFactory: executeOrInterruptTool,
userId: "{arcade_user_id}", // Replace this with your application's user ID (e.g. email address, UUID, etc.)
});
// Convert Arcade tools to LangGraph tools
const tools = arcadeTools.map(({ name, description, execute, parameters }) =>
(tool as Function)(execute, {
name,
description,
schema: parameters,
})
);
async function handleAuthInterrupt(
interrupt: Interrupt
): Promise<{ authorized: boolean }> {
const value = interrupt.value;
const authorization_required = value.authorization_required;
if (authorization_required) {
const tool_name = value.tool_name;
const authorization_response = value.authorization_response;
console.log("⚙️: Authorization required for tool call", tool_name);
console.log("⚙️: Authorization URL", authorization_response.url);
console.log("⚙️: Waiting for authorization to complete...");
try {
await arcade.auth.waitForCompletion(authorization_response.id);
console.log("⚙️: Authorization granted. Resuming execution...");
return { authorized: true };
} catch (error) {
console.error("⚙️: Error waiting for authorization to complete:", error);
return { authorized: false };
}
}
return { authorized: false };
}
const agent = createAgent({
systemPrompt:
"You are a helpful assistant that can use GMail tools. Your main task is to help the user with anything they may need.",
model: "gpt-4o-mini",
tools: tools,
checkpointer: new MemorySaver(),
});
async function streamAgent(
agent: any,
input: any,
config: any
): Promise<Interrupt[]> {
const stream = await agent.stream(input, {
...config,
streamMode: "updates",
});
const interrupts: Interrupt[] = [];
for await (const chunk of stream) {
if (chunk.__interrupt__) {
interrupts.push(...(chunk.__interrupt__ as Interrupt[]));
continue;
}
for (const update of Object.values(chunk)) {
for (const msg of (update as any)?.messages ?? []) {
console.log("🤖: ", msg.toFormattedString());
}
}
}
return interrupts;
}
async function main() {
const config = { configurable: { thread_id: "1" } };
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.log(chalk.green("Welcome to the chatbot! Type 'exit' to quit."));
while (true) {
const input = await rl.question("> ");
if (input.toLowerCase() === "exit") {
break;
}
rl.pause();
try {
// Stream the agent response and collect any interrupts
const interrupts = await streamAgent(
agent,
{
messages: [{ role: "user", content: input }],
},
config
);
// Handle authorization interrupts
const decisions: any[] = [];
for (const interrupt of interrupts) {
decisions.push(await handleAuthInterrupt(interrupt));
}
// Resume agent after authorization
if (decisions.length > 0) {
await streamAgent(
agent,
new Command({ resume: { decisions } }),
config
);
}
} catch (error) {
console.error(error);
}
rl.resume();
}
console.log(chalk.red("👋 Bye..."));
process.exit(0);
}
// Run the main function
main().catch((err) => console.error(err));