Custom plugin for Google drive and gmail for OpenClaw
In this guide, I'll walk you through creating a custom OpenClaw plugin that gives your AI agent access to Gmail and Google Drive. This is perfect for creating an agent that can check emails, send responses, and manage files in a shared Drive folder.
Before creating the plugin, you need to set up Google Cloud credentials:
You'll need a refresh token to authenticate. Create a script to get one:
const { OAuth2Client } = require('google-auth-library');
const fs = require('fs');
const readline = require('readline');
const SCOPES = [
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/gmail.send',
'https://www.googleapis.com/auth/drive.file'
];
const oauth2Client = new OAuth2Client(
'YOUR_CLIENT_ID',
'YOUR_CLIENT_SECRET',
'http://localhost:8080'
);
const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
prompt: 'consent'
});
console.log('Authorize this app by visiting this url:', authUrl);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('\nEnter the code from authorization page: ', async (code) => {
const { tokens } = await oauth2Client.getToken(code);
console.log('\n--- REFRESH TOKEN ---\n');
console.log(tokens.refresh_token);
console.log('\n------------------------');
rl.close();
});
Run it with node get_refresh_token.js, then copy the refresh token.
On your OpenClaw server, create the plugin directory:
mkdir -p ~/openclaw/extensions/my-email-drive
Create openclaw.plugin.json in your plugin directory:
{
"id": "my-email-drive",
"name": "Email & Drive",
"description": "Email and Google Drive capabilities for the agent",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}
{
"name": "@openclaw/my-email-drive",
"version": "1.0.0",
"private": true,
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"
]
},
"devDependencies": {
"openclaw": "workspace:*"
},
"dependencies": {
"@sinclair/typebox": "workspace:*",
"googleapis": "^171.0.0",
"google-auth-library": "^9.0.0"
}
}
Create index.ts with your plugin logic:
import { Type } from "@sinclair/typebox";
import { google } from "googleapis";
let gmail: any;
let drive: any;
function getEnv(key: string): string {
const value = process.env[key];
if (!value) {
throw new Error(`Missing required env var: ${key}`);
}
return value;
}
function initializeApis() {
if (!gmail || !drive) {
const { OAuth2Client } = require("google-auth-library");
const clientId = getEnv("GOOGLE_CLIENT_ID");
const clientSecret = getEnv("GOOGLE_CLIENT_SECRET");
const refreshToken = getEnv("GOOGLE_REFRESH_TOKEN");
const oauth2Client = new OAuth2Client(clientId, clientSecret);
oauth2Client.setCredentials({ refresh_token: refreshToken });
gmail = google.gmail({ version: "v1", auth: oauth2Client });
drive = google.drive({ version: "v3", auth: oauth2Client });
}
return { gmail, drive };
}
export default function (api: any) {
// Tool 1: Check Email
api.registerTool(
{
name: "check_email",
label: "Check Email",
description: "Check the Gmail inbox for emails from the owner. Returns recent emails with subject, sender, date, and snippet.",
parameters: Type.Object({
maxResults: Type.Optional(Type.Number()),
}),
async execute(_id: string, params: Record) {
const { gmail: gmailApi } = initializeApis();
const ownerEmail = getEnv("OWNER_EMAIL");
const maxResults = (params.maxResults as number) || 10;
const response = await gmailApi.users.messages.list({
userId: "me",
q: `from:${ownerEmail}`,
maxResults,
});
const messages = response.data.messages || [];
if (messages.length === 0) {
return { content: [{ type: "text", text: JSON.stringify({ count: 0, emails: [] }) }] };
}
const detailedMessages = await Promise.all(
messages.map(async (msg: any) => {
const detail = await gmailApi.users.messages.get({
userId: "me",
id: msg.id,
format: "full",
});
return detail.data;
})
);
const emails = detailedMessages.map((email: any) => ({
id: email.id,
subject: email.payload?.headers?.find((h: any) => h.name === "Subject")?.value,
from: email.payload?.headers?.find((h: any) => h.name === "From")?.value,
date: email.payload?.headers?.find((h: any) => h.name === "Date")?.value,
snippet: email.snippet,
}));
return {
content: [{ type: "text", text: JSON.stringify({ count: messages.length, emails }) }],
};
},
},
{ optional: false }
);
// Tool 2: Send Email
api.registerTool(
{
name: "send_email",
label: "Send Email",
description: "Send an email. SECURITY: Can ONLY send to the owner's email address.",
parameters: Type.Object({
subject: Type.String(),
body: Type.String(),
}),
async execute(_id: string, params: Record) {
const { gmail: gmailApi } = initializeApis();
const ownerEmail = getEnv("OWNER_EMAIL");
const subject = params.subject as string;
const body = params.body as string;
const message = ["To: " + ownerEmail, "Subject: " + subject, "", body].join("\n");
const encoded = Buffer.from(message).toString("base64").replace(/\+/g, "-").replace(/\//g, "_");
const result = await gmailApi.users.messages.send({
userId: "me",
requestBody: { raw: encoded },
});
return {
content: [{ type: "text", text: JSON.stringify({ success: true, messageId: result.data.id }) }],
};
},
},
{ optional: false }
);
// Tool 3: Read Drive File
api.registerTool(
{
name: "read_drive_file",
label: "Read Drive File",
description: "Read a file from the shared Google Drive folder.",
parameters: Type.Object({
filename: Type.String(),
}),
async execute(_id: string, params: Record) {
const { drive: driveApi } = initializeApis();
const folderId = getEnv("GOOGLE_DRIVE_FOLDER_ID");
const filename = params.filename as string;
const searchResponse = await driveApi.files.list({
q: `name='${filename}' and '${folderId}' in parents and trashed=false`,
fields: "files(id, name, mimeType)",
});
if (!searchResponse.data.files?.length) {
return { content: [{ type: "text", text: JSON.stringify({ error: `File not found: ${filename}` }) }] };
}
const file = searchResponse.data.files[0];
let content: any;
if (file.mimeType === "application/vnd.google-apps.document") {
const doc = await driveApi.files.export({ fileId: file.id, mimeType: "text/plain" });
content = doc.data;
} else {
const fileContent = await driveApi.files.get({ fileId: file.id, alt: "media" });
content = fileContent.data;
}
return { content: [{ type: "text", text: JSON.stringify({ filename: file.name, content }) }] };
},
},
{ optional: false }
);
// Tool 4: Write Drive File
api.registerTool(
{
name: "write_drive_file",
label: "Write Drive File",
description: "Write a file to the shared Google Drive folder.",
parameters: Type.Object({
filename: Type.String(),
content: Type.String(),
}),
async execute(_id: string, params: Record) {
const { drive: driveApi } = initializeApis();
const folderId = getEnv("GOOGLE_DRIVE_FOLDER_ID");
const filename = params.filename as string;
const content = params.content as string;
const existingResponse = await driveApi.files.list({
q: `name='${filename}' and '${folderId}' in parents and trashed=false`,
fields: "files(id)",
});
if (existingResponse.data.files?.length) {
const fileId = existingResponse.data.files[0].id;
await driveApi.files.update({ fileId, media: { mimeType: "text/plain", body: content } });
return { content: [{ type: "text", text: JSON.stringify({ success: true, action: "updated", filename }) }] };
}
await driveApi.files.create({
requestBody: { name: filename, parents: [folderId] },
media: { mimeType: "text/plain", body: content },
});
return { content: [{ type: "text", text: JSON.stringify({ success: true, action: "created", filename }) }] };
},
},
{ optional: false }
);
// Tool 5: List Drive Files
api.registerTool(
{
name: "list_drive_files",
label: "List Drive Files",
description: "List all files in the shared Google Drive folder.",
parameters: Type.Object({}),
async execute() {
const { drive: driveApi } = initializeApis();
const folderId = getEnv("GOOGLE_DRIVE_FOLDER_ID");
const response = await driveApi.files.list({
q: `'${folderId}' in parents and trashed=false`,
fields: "files(id, name, mimeType, modifiedTime)",
});
return { content: [{ type: "text", text: JSON.stringify({ files: response.data.files || [] }) }] };
},
},
{ optional: false }
);
}
Add these to your ~/.openclaw/.env file:
# Google OAuth Credentials
GOOGLE_CLIENT_ID=your-client-id-from-google-cloud
GOOGLE_CLIENT_SECRET=your-client-secret-from-google-cloud
GOOGLE_REFRESH_TOKEN=your-refresh-token-from-step-2
# Configuration
OWNER_EMAIL=your-email@example.com
GOOGLE_DRIVE_FOLDER_ID=your-shared-folder-id
On the OpenClaw server, run:
cd ~/openclaw
openclaw plugins install -l ./extensions/my-email-drive
The -l flag links the plugin for development (changes are picked up without reinstalling).
cd ~
./openclawctl restart
Verify the plugin is loaded:
openclaw plugins list
You should see your plugin listed. Now ask your agent:
Here's what each file does:
The key function is api.registerTool():
api.registerTool(
{
name: "tool_name", // Unique tool identifier
label: "Tool Label", // Display name
description: "What it does",
parameters: Type.Object({ // JSON Schema for parameters
param1: Type.String(),
param2: Type.Optional(Type.Number()),
}),
async execute(id, params) {
// Your logic here
return {
content: [{ type: "text", text: "Result" }]
};
}
},
{ optional: false } // Set to true for opt-in tools
);
content array. Each item should have type (usually "text") and text (the result string).
If you see "plugin not found" errors, ensure:
extensions/openclaw.plugin.json is valid JSONopenclaw plugins installThis usually means the plugin isn't being loaded. Check:
openclaw plugins list~/.openclaw/openclaw.logEnsure all required env vars are set in ~/.openclaw/.env and restart OpenClaw after changes.
You've successfully created a custom OpenClaw plugin! The agent can now:
This opens up many possibilities for agent workflows - file sharing, email notifications, automated responses, and more!
Happy building!