New: Explore the AI Stack for Claude skills, outbound automations, and operator workflows
Tools & Stack15 min read·

Automated Cold-Email Agents: Google CLI

How to use Google's GWS CLI to automate your entire cold email operation with Claude — the infrastructure layer most outbound teams don't know exists.

Automated Cold-Email Agents: Google CLI

Most teams treat cold email like a copy problem. They spend weeks perfecting subject lines, stress over open rates, A/B test openers — and still hit a ceiling. The real bottleneck isn't the copy. It's the infrastructure underneath it.

Cleaning lists. Enriching company data. Writing personalized variables at scale. Managing live sheets. Keeping everything in sync. The teams pulling real volume have solved this at the infrastructure layer.

The tool that makes that possible — quietly, without a lot of fanfare — is Google's GWS CLI. This article explains what it is, how to set it up, and how it connects Claude to your entire cold email stack.

What Is the GWS CLI?

GWS stands for Google Workspace. The CLI is a command-line interface built and maintained by Google that lets you interact with your entire Google Workspace environment — Drive, Sheets, Docs, Gmail, Tasks — directly from your terminal.

No browser. No clicking. No copy-pasting between tabs.

It means you can write a command that says: “Open that Google Sheet, read columns A through P, give me the rows where column F says ‘Verified’, and write the output to a new tab” — and it just runs.

When you connect that to an AI agent like Claude, you now have something more powerful: a system where the AI can read your live data, process it, and write the results back — all without you touching a spreadsheet. That is the foundation of a proper cold email engine.

Setting It Up

Step 1: Install the CLI and Cloud SDK

brew install googleworkspace-cli
brew install --cask google-cloud-sdk

Step 2: Create a Google Cloud Project

Go to the Google Cloud Console and create a new project. Name it something like outbound-engine — this is just an identifier. You’ll attach your OAuth credentials to this project.

Step 3: Enable the APIs You Need

  • Google Drive API
  • Google Sheets API
  • Google Docs API
  • Gmail API (optional, if you want inbox access)

Step 4: Create an OAuth Desktop App Client

In the Cloud Console, go to Credentials → Create Credentials → OAuth Client ID and select Desktop App as the application type. Download the JSON file. Do not copy-paste the values manually — the CLI is strict about the format of this file and manual copies break silently.

Step 5: Authenticate

gws auth login --client-secret /path/to/client_secret.json

This opens a browser window, you grant the scopes, and the CLI stores encrypted credentials locally at ~/.config/gws/credentials.enc. You authenticate once and it persists.

Step 6: Test Your Connection

gws drive files list

If your Drive files come back, you’re live.

What You Can Now Do From the Terminal

Once authenticated, every resource in your Workspace is addressable as a command.

Read a spreadsheet

gws sheets +read \
  --spreadsheet SPREADSHEET_ID \
  --range 'Sheet1!A1:Z500'

Write to a spreadsheet

gws sheets spreadsheets values update \
  --params '{"spreadsheetId":"SHEET_ID","range":"Sheet1!A1","valueInputOption":"RAW"}' \
  --json '{"range":"Sheet1!A1","majorDimension":"ROWS","values":[["Value1","Value2"]]}'

Create a Google Doc

gws docs documents create \
  --json '{"title":"Copy | Commercial Finance | Campaign 01"}'

Write structured content into that Doc

gws docs documents batchUpdate \
  --params '{"documentId":"DOC_ID"}' \
  --json '{"requests":[{"insertText":{"location":{"index":1},"text":"Your email copy here."}}]}'

Search Drive for a file

gws drive files list \
  --params '{"q":"name contains \"Campaign\" and trashed = false"}'

This is the primitive layer. Raw, fast, scriptable. Now let’s connect Claude to it.

How Claude Takes Control Through GWS

When you give Claude access to your terminal — through Claude Code, the API, or a custom agent — it can call any of these GWS commands as part of its workflow.

This changes the relationship between you and your AI agent fundamentally.

Instead of: “Here’s a paste of my spreadsheet data, help me write copy” — you get: “Read my live campaign sheet, find every row where the enrichment column is blank, scrape those companies, classify them, and write the results back into the sheet.”

The agent operates on your live data. It reads, writes, creates, and updates. You supervise and approve.

A Real Workflow Example

Here’s the actual flow a well-configured system runs when building a commercial finance campaign targeting lenders.

1. Read the master tracker to get the campaign context

gws sheets +read \
  --spreadsheet MASTER_TRACKER_ID \
  --range 'Campaigns Housing!B2:O50'

Claude reads this to understand what campaigns are running, which lists are in use, and what the current campaign’s ID and naming spine is.

2. Read the live campaign sheet

gws sheets +read \
  --spreadsheet CAMPAIGN_SHEET_ID \
  --range 'Enrichment!A1:AZ3500'

It now has the full working dataset — company names, websites, titles, verified emails.

3. Process and write back

After running its enrichment logic, it writes results back:

gws sheets spreadsheets values update \
  --params '{"spreadsheetId":"CAMPAIGN_SHEET_ID","range":"Enrichment!AB1:AD3500","valueInputOption":"RAW"}' \
  --json '...'

No local CSV files created. No copy-paste. The sheet is always the source of truth.

Pointing It at a CSV — List Cleaning at Scale

The GWS CLI’s reach extends to your file system too. When you drop a raw lead list into a folder — say, an Apollo export or a list pulled from a data provider — Claude can:

  • Read the raw CSV
  • Identify and remove junk columns (company phone, irrelevant enrichment fields, duplicates)
  • Normalize fields — first name case, email format, company name cleanup
  • Flag rows that are missing required fields
  • Write the cleaned version to a new file or push it directly to a Google Sheet

In practice this looks like a working tab structure. Raw data goes into a Source Pull tab. The agent applies cleaning rules and creates a Cleaned tab. After email verification runs, a Verified tab appears. After enrichment, an Enrichment tab. Each stage is preserved as an audit trail. The final Launch tab is the only thing that touches Instantly.

Claude knows the column rules because they live in your project’s brain files. You define the spec once. Every campaign inherits it automatically.

Connecting Cloudflare to Scrape on Your Behalf

Once you have a clean, verified list, you often want per-company context before writing copy. That’s where Cloudflare Browser Rendering comes in.

Cloudflare offers a headless browser API with two endpoints:

  • /markdown — synchronous, renders a URL and returns the page content as clean Markdown. Fast, cheap, handles JavaScript-rendered sites.
  • /crawl — asynchronous job that spiders multiple pages (homepage, About, Services, Solutions, etc.) and returns everything it found.

Your Python script — triggered by Claude or run directly — loops through every unique domain in your list:

scrape.py
response = requests.post(
    "https://api.cloudflare.com/client/v4/accounts/YOUR_ACCOUNT_ID/browser-rendering/markdown",
    headers={"Authorization": "Bearer YOUR_CF_TOKEN"},
    json={"url": "https://example.com"},
    timeout=60
)
page_text = response.json()["result"]["markdown"]

The workflow: for each unique domain, try /markdown first — homepage + 1–2 supporting pages. If that returns thin content (under a word-count threshold), fall back to /crawl which discovers and retrieves deeper pages. Cache results by domain — if a domain already exists in your cache, skip the scrape.

Pass the rendered content to OpenAI for classification. For a list of 2,500 unique domains, a well-tuned script can process the entire batch in a few hours, with an 85–90% fill rate on the first pass. A recovery pass targeting only the thin-result rows typically pushes you to 95%+ overall.

Connecting OpenAI to Write Custom Lines

The scraped content per company becomes the input for a GPT call. Depending on your campaign model, this can mean two different approaches.

Option A: Single custom variable per company (Doc-First model)

You write the main email copy yourself. You just need one variable that changes per row — something like what type of financing is most relevant to this business, or what sector they operate in.

classify.py
prompt = f"""
You are classifying companies for a commercial finance outreach campaign.

Company: {company_name}
Website content:
{page_text[:3000]}

Classify the company's most relevant financing type from this list:
- equipment financing
- receivables financing
- working capital lines
- construction financing
- commercial real estate
- SBA/government programs

Return ONLY the financing type, no explanation.
"""

response = openai.chat.completions.create(
    model="gpt-4.1-mini",
    messages=[{"role": "user", "content": prompt}],
    temperature=0.2
)
variable_value = response.choices[0].message.content.strip()

This generates one clean variable per row. In your email copy, it becomes {{sector_relevantfundingtype}} — and every recipient sees a sentence that references their actual business context.

Option B: Fully AI-written opener per company (Row-Level model)

For smaller, high-value lists, you go deeper. Every row gets a unique first line written from scratch based on the company’s website:

opener.py
prompt = f"""
Write a single cold email opener for a commercial finance outreach.
One sentence. Factual. No hype. No buzzwords.
Reference something specific about this company that makes them
a likely candidate for financing needs.

Company: {company_name}
Context: {company_summary}

Start the sentence directly. No greeting. No "I noticed". No em dashes.
"""

The result is a column like {{opener}} with a unique observation for every single contact. The main copy stays fixed — only that first line changes.

Both models run through a banned-word filter before anything touches your sheet. Any spam-trigger word in the output triggers a retry with explicit substitution guidance. If the retries fail, a conservative fallback fires. Nothing with a flagged word ever makes it into a live campaign.

What This Stack Actually Looks Like End-to-End

Here’s the full picture when it’s wired together for a demand generation campaign targeting lenders — say, you’re offering them borrower referrals or deal flow:

  1. 1Raw List (CSV export from data provider)
  2. 2Claude reads headers, applies cleaning rules → Cleaned Tab (Google Sheet, live)
  3. 3Email verification tool runs (external step) → Verified Tab (valid emails only)
  4. 4ICP filter: remove nonprofits, gov entities, ineligible verticals
  5. 5Cloudflare scrapes every unique domain
  6. 6GPT classifies each company → financing sector variable
  7. 7Subject line generation + spam gate
  8. 8Company name cleanup — remove banned tokens from names
  9. 9Enrichment Tab (full audit trail, all columns)
  10. 10Duplicate → prune to send columns only → Launch Tab (send-ready, ~10 columns)
  11. 11Instantly CLI creates campaign and imports leads
  12. 12Pre-launch audit runs → human reviews and approves
  13. 13Instantly CLI activates campaign

Every stage writes back to the live Google Sheet through GWS. Every decision is logged. The source data is never touched after the first read. The Launch tab is the only thing that goes to Instantly.

Real Campaign Examples — What This Produces

Here’s how the copy looks when this infrastructure is in use for lender-side demand generation. These are illustrative examples modeled on real campaign structures.

Initial Email — Doc-First Model with Sector Variable

Email 1
Subject: {{firstName}} - {{subject_line}}

{{firstName}},

{{RANDOM|Figured|Thought|Assumed}} you {{RANDOM|would|might}} {{RANDOM|value|appreciate|welcome}} a {{RANDOM|connection|relationship}} - we {{RANDOM|regularly work with|consistently see|frequently run into}} {{RANDOM|operators|businesses|companies}} in {{sector_relevantfundingtype}} {{RANDOM|who|that}} are either {{RANDOM|actively looking for capital|in the market for financing|evaluating their options}} or {{RANDOM|underserved by their current lender|past their lenders appetite|ready to move to a better structure}}.

{{RANDOM|When timing works, we make introductions.|When there's a fit, we facilitate the conversation.|We move quickly when the situation makes sense.}}

{{RANDOM|Worth a quick conversation|Worth a chat|Worth a convo}}?

{{accountSignature}}

The {{sector_relevantfundingtype}} variable makes every email feel like it was written for that specific type of lender — because the classification actually reflects what that company finances.

Follow-Up Bump — Assumption Pivot

Email 2 — Follow-Up
{{firstName}},

{{RANDOM|Assuming you're already working with an intro source|If you're already getting new opportunities from a referral partner|Guessing you've already got something in place}} - {{RANDOM|worth knowing|just worth flagging|good to know}} there are {{RANDOM|situations|deals|opportunities}} that {{RANDOM|fall outside typical channels|don't always make it to traditional desks|go to whoever responds first}}.

{{RANDOM|Worth a quick conversation|Worth a chat|Worth a convo}}?

{{accountSignature}}

Row-Level Opener — High-Personalization Model

For a smaller list of commercial lenders where every row has been individually scraped and classified:

Email 1 — Row-Level
{{opener}},

We work with {{sector_relevantfundingtype}} operators who {{RANDOM|need capital|are evaluating their financing options|have outgrown their current structure}}.
When there's a fit, we {{RANDOM|make introductions|connect them with the right desk|route the conversation}}.

{{RANDOM|Worth a quick conversation|Worth a chat|Worth a convo}}?

{{accountSignature}}

Where {{opener}} for a row at a regional equipment finance company might read: “Saw Lakeside Equipment focuses on middle-market manufacturing — that’s a credit box a lot of lenders won’t touch.” Every recipient gets something that could only have been written for them.

Why This Matters for Teams Doing Outbound at Scale

The GWS CLI solves a specific and important problem: coordination between AI and live data.

Without it, your AI agent is working off static pastes. The moment you close the conversation, the connection breaks. Results pile up in a chat window. Someone has to manually move them into the sheet.

With GWS as the bridge, your agent operates on the same data surface you use. It reads what you see. It writes back to what you see. Nothing gets lost in translation. The AI becomes a persistent operator in your workflow — not a one-time assistant.

Add Cloudflare for reliable website access at scale, OpenAI for classification and copy generation, and Instantly for sending — and you have a stack that can take a raw list of 5,000 contacts from scratch to a live, personalized campaign in a single controlled build session.

The copy your prospects receive reflects their actual business. The subject line they see is specific to their sector. The follow-up they get assumes something true about their situation. That’s what this infrastructure makes possible. Not just automation — relevant automation.

Getting Started

  • GWS CLI — authenticate to your Workspace, confirm Drive and Sheets access work
  • Cloudflare Browser Rendering — enable it in your Cloudflare dashboard, get an API token with Browser Rendering permissions
  • OpenAI API — standard API key, models gpt-4o or gpt-4.1-mini depending on volume
  • A Python script — the glue layer that reads from GWS, calls Cloudflare, calls OpenAI, and writes back
  • A sending platform — Instantly, EmailBison, or equivalent with a CLI or API

The GWS CLI is the piece most teams don’t have. Everything else is already familiar. Once the read/write layer to Google is working, the entire stack becomes composable.

Need cold email volume?

Done-for-you mailboxes for outbound

InfraSuite is built for teams that rely on cold email as a core revenue channel and need stable, high-performing Outlook mailboxes. You subscribe to a proven Microsoft-based sending environment that's already configured for cold outreach — provisioning, DNS, mailbox setup, and deliverability hygiene handled for you. A completely hands-free and automated solution so you can focus on campaigns, clients, and revenue instead of infrastructure risk.

  • Stable inbox placement across Outlook and Google
  • Fewer resets, fewer domain swaps
  • Capacity ready when clients sign
  • Calm, competent support when something looks off
Learn more by clicking here

Frequently asked questions