Google Drive & Docs
Category: Cloud Storage / Documents
Integration type: Tenant OAuth (Google user account — stored in integrations table)
External API: Google Drive API v3, Google Docs API v1
OAuth Scopes: drive.file, documents
Purpose
The Google Drive integration stores platform-generated deliverables — reports, strategy documents, and content packages — directly in the client’s Google Drive as native Google Docs. Clients can view, comment on, and share documents without logging into the Leadmetrics platform.
Use cases:
- Monthly performance reports — Report Writer output is pushed as a Google Doc into a designated Drive folder
- Strategy documents — Strategy Writer output is mirrored into Drive so clients can annotate and share internally
- Content brief export — Content briefs can be exported to Drive for client sign-off before content production begins
- Deliverable archive — A structured Drive folder hierarchy organises all deliverables by month and content type
The platform creates documents as view-only shared links by default. Comment access is optionally granted. Edit access is never granted automatically.
Config Structure
Tenant integration (OAuth)
interface GoogleDriveConfig {
accessToken: string; // Short-lived OAuth access token
refreshToken: string; // Long-lived refresh token
rootFolderId: string; // Top-level Drive folder for this client (e.g. "Acme Corp — Leadmetrics")
reportFolderId?: string; // Subfolder for reports (created during setup if absent)
strategyFolderId?: string; // Subfolder for strategy documents
}Stored in integrations:
provider: 'google-drive'
api_key: encrypt(refreshToken)
metadata: {
rootFolderId: string,
reportFolderId: string | null,
strategyFolderId: string | null,
connectedEmail: string, // Google account email — shown in UI
}Tokens are refreshed automatically before API calls using the Google OAuth2 client.
Integration Pattern
Tool layer (packages/tools/src/google-drive.ts)
import { google } from 'googleapis';
class GoogleDriveTool {
private driveClient;
private docsClient;
constructor(private cfg: GoogleDriveConfig) {
const auth = new google.auth.OAuth2();
auth.setCredentials({
access_token: cfg.accessToken,
refresh_token: cfg.refreshToken,
});
this.driveClient = google.drive({ version: 'v3', auth });
this.docsClient = google.docs({ version: 'v1', auth });
}
/**
* Create a new Google Doc from Markdown content.
* Converts Markdown to Docs batchUpdate requests.
*/
async createDoc(options: {
folderId: string;
title: string;
markdown: string; // Converted to Docs API batchUpdate requests
}): Promise<{ docId: string; url: string }> {
// Step 1: Create empty document
const createResponse = await this.driveClient.files.create({
requestBody: {
name: options.title,
mimeType: 'application/vnd.google-apps.document',
parents: [options.folderId],
},
fields: 'id,webViewLink',
});
const docId = createResponse.data.id as string;
// Step 2: Populate content via Docs batchUpdate
const requests = markdownToDocsRequests(options.markdown);
if (requests.length > 0) {
await this.docsClient.documents.batchUpdate({
documentId: docId,
requestBody: { requests },
});
}
return {
docId,
url: createResponse.data.webViewLink as string,
};
}
/**
* Upload a binary file (PDF, image) to a Drive folder.
*/
async uploadFile(options: {
folderId: string;
filename: string;
buffer: Buffer;
mimeType: string;
}): Promise<{ fileId: string; url: string }> {
const { Readable } = require('stream');
const stream = Readable.from(options.buffer);
const response = await this.driveClient.files.create({
requestBody: {
name: options.filename,
parents: [options.folderId],
},
media: {
mimeType: options.mimeType,
body: stream,
},
fields: 'id,webViewLink',
});
return {
fileId: response.data.id as string,
url: response.data.webViewLink as string,
};
}
/**
* Set sharing permissions on a file or folder.
*/
async shareFile(options: {
fileId: string;
role: 'reader' | 'commenter' | 'writer';
type: 'user' | 'anyone';
email?: string; // Required when type = 'user'
}): Promise<{ permissionId: string }> {
const response = await this.driveClient.permissions.create({
fileId: options.fileId,
requestBody: {
role: options.role,
type: options.type,
emailAddress: options.email,
},
sendNotificationEmail: false,
});
return { permissionId: response.data.id as string };
}
/**
* Create a subfolder inside an existing folder.
*/
async createFolder(options: {
parentFolderId: string;
name: string;
}): Promise<{ folderId: string }> {
const response = await this.driveClient.files.create({
requestBody: {
name: options.name,
mimeType: 'application/vnd.google-apps.folder',
parents: [options.parentFolderId],
},
fields: 'id',
});
return { folderId: response.data.id as string };
}
async verify(): Promise<void> {
await this.driveClient.about.get({ fields: 'user' });
}
}Markdown → Docs API requests helper
The Google Docs API uses batchUpdate requests to insert content. A helper (markdownToDocsRequests) converts Markdown to an array of InsertText, UpdateParagraphStyle, and UpdateTextStyle requests.
function markdownToDocsRequests(markdown: string): object[] {
// Uses a lightweight custom parser or a library such as `md-to-google-docs`
// to produce Docs API batchUpdate request objects.
// Headings → UpdateParagraphStyle (HEADING_1/2/3)
// Bold/italic → UpdateTextStyle
// Lists → bullet/numbered list InsertText
// Tables → InsertTable + InsertText
return []; // Placeholder — implement with parser
}Agent Workflow
Report delivery to Drive
Report Writer agent generates monthly performance report (Markdown)
│
▼ (HITL approval by DM team)
Report Publisher worker
├── Resolve tenant's Google Drive integration
├── Determine report folder (create if absent: "Reports / April 2026")
├── Call createDoc() with report title and Markdown content
├── Call shareFile() with role = 'commenter' for client email
└── Store docId + url in deliverable_periods.google_doc_id
│
▼
DM Portal shows "Open in Google Docs" link on deliverable periodFolder structure created during setup
📁 [Client Name] — Leadmetrics ← rootFolderId
├── 📁 Reports
│ ├── 📄 March 2026 Performance Report
│ └── 📄 April 2026 Performance Report
└── 📁 Strategy
└── 📄 Q2 2026 Marketing StrategyThe folder hierarchy is created during integration setup and subfolders for each month are created by the Report Publisher worker.
OAuth Setup Flow
- Tenant clicks “Connect Google Drive” in Dashboard → Settings → Integrations
- Platform redirects to Google OAuth with scopes:
drive.file,documents,userinfo.email - Google redirects back with auth code → platform exchanges for
access_token+refresh_token - Platform creates root folder
"[Client Name] — Leadmetrics"in the user’s Drive - Platform creates subfolders:
Reports,Strategy - Folder IDs + connected email saved to
integrationstable
Token Refresh
Access tokens expire after 1 hour. Before each API call, the tool checks token expiry and calls oauth2Client.refreshAccessToken() if within 5 minutes of expiry. The new access_token is written back to integrations.api_key (encrypted).
Test Cases
Unit tests (packages/tools/src/google-drive.test.ts)
| Test | Approach |
|---|---|
createDoc() creates file then calls batchUpdate | Mock drive and docs clients; assert both called in order |
createDoc() skips batchUpdate when no requests | Mock markdownToDocsRequests returning []; assert batchUpdate not called |
createDoc() returns docId and url | Mock files.create returning { id, webViewLink }; assert mapped return |
uploadFile() streams buffer to Drive | Assert media.body is a Readable stream |
shareFile() uses correct role and type | Assert permissions.create called with expected requestBody |
createFolder() sets mimeType to folder MIME type | Assert mimeType: 'application/vnd.google-apps.folder' |
verify() throws on invalid credentials | Mock about.get throws 401; assert propagated |
Related
- Slack Provider — notification channel for report delivery
- Notion Provider — document delivery alternative
- Report Writer Agent — generates monthly performance reports
- Strategy Writer Agent — generates multi-month strategy documents
- Tool Integration Layer — integration management