Building Your First OpenClaw Plugin: A Gmail & Google Drive Integration Guide

post Main Image

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.

Prerequisites

Note: This guide assumes you have basic knowledge of JavaScript/TypeScript and are comfortable working with command-line tools.

Step 1: Set Up Google Cloud Credentials

Before creating the plugin, you need to set up Google Cloud credentials:

  1. Go to Google Cloud Console
  2. Create a new project (e.g., "MyAgent")
  3. Enable the Gmail API and Google Drive API
  4. Go to APIs & Services → Credentials
  5. Create OAuth 2.0 credentials (Desktop Application)
  6. Download the JSON file

Step 2: Get a Refresh Token

You'll need a refresh token to authenticate. Create a script to get one:

get_refresh_token.js
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.

Step 3: Create the Plugin Directory

On your OpenClaw server, create the plugin directory:

mkdir -p ~/openclaw/extensions/my-email-drive
Tip: Replace "my-email-drive" with your plugin name. Use lowercase and hyphens.

Step 4: Create the Plugin Manifest

Create openclaw.plugin.json in your plugin directory:

openclaw.plugin.json
{
  "id": "my-email-drive",
  "name": "Email & Drive",
  "description": "Email and Google Drive capabilities for the agent",
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {}
  }
}

Step 5: Create package.json

package.json
{
  "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"
  }
}

Step 6: Create the Plugin Code

Create index.ts with your plugin logic:

index.ts
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 }
  );
}

Step 7: Configure Environment Variables

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
Security Note: Never share your client secret or refresh token. Keep the .env file secure.

Step 8: Install the Plugin

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).

Step 9: Restart OpenClaw

cd ~
./openclawctl restart

Step 10: Test the Plugin

Verify the plugin is loaded:

openclaw plugins list

You should see your plugin listed. Now ask your agent:

Understanding the Plugin Structure

Here's what each file does:

Tool Registration API

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
);
Return Format: Tools must return an object with a content array. Each item should have type (usually "text") and text (the result string).

Troubleshooting

Plugin Not Found

If you see "plugin not found" errors, ensure:

Tool.execute is not a function

This usually means the plugin isn't being loaded. Check:

Missing Environment Variables

Ensure all required env vars are set in ~/.openclaw/.env and restart OpenClaw after changes.

Conclusion

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!