Systems Lab

Agent skill

crm-writer

Read and write data to the agency CRM (Google Sheets via webhook or API)

dormantNeeds a keyActs undeclared933 words

Filed under CRM and RevOps.

From ekatasingh1107/b2b-gtm-skills · 99 skills · 2 · pushed 2026-04-11

What it does when it runs

Read and write data to the agency CRM (Google Sheets via webhook or API)

Read from the skill and the 1 file bundled beside it. A skill’s own description is written to be selected by an agent, so it describes the job and not the dependencies.

Keys and connectors you must supply
  • GOOGLE_SERVICE_ACCOUNT_KEY
Hosts it reaches
  • sheets.googleapis.com
Tool permissions it declares
No allowed-tools in the frontmatter. It does act, so it runs under whatever permissions your session already grants.
Actions present in the files
shell

Ask about crm-writer

Opens your assistant with this page's verified links already in the prompt.

Is this safe to install?ClaudeChatGPT
Adapt it to my stackClaudeChatGPT
What else do I need for it to workClaudeChatGPT
Rather ask a human? Talk to Cheetah
git clone --depth 1 --filter=blob:none --sparse https://github.com/ekatasingh1107/b2b-gtm-skills.git /tmp/b2b-gtm-skills
git -C /tmp/b2b-gtm-skills sparse-checkout set "skills/capabilities/crm-writer"
mkdir -p ~/.claude/skills/crm-writer
cp -R "/tmp/b2b-gtm-skills/skills/capabilities/crm-writer/." ~/.claude/skills/crm-writer/

Picked up without a restart. A project skill of the same name is shadowed by your personal one. For one repository only, swap ~/.claude/skills for .claude/skills. Claude Code docs ↗

The folder is the same in every client that implements the format — 46 of them — so if yours is not above, only the destination changes.

Before you install: this skill will not complete its job on a bare agent. It needs GOOGLE_SERVICE_ACCOUNT_KEY, which you have to obtain separately.

Reproduced in full from ekatasingh1107/b2b-gtm-skills/blob/eae8dd0bb98da1c8e84abd297066a87015dd860f/skills/capabilities/crm-writer/SKILL.md, which is licensed MIT (repository). 933 words, 13 headings.

CRM Writer

Reads and writes lead, outreach, and pipeline data to the agency's CRM. Default backend is Google Sheets via webhook. The skill checks tools.crm in agency.config.json to determine the execution path and credentials.

Prerequisites

  • agency.config.json at repo root with crm and tools.crm sections
  • For webhook method: a deployed Google Apps Script webhook URL in crm.webhook_url
  • For API method: GOOGLE_SERVICE_ACCOUNT_KEY env var set (referenced in tools.crm.api_key_env)

Phase 0: Intake

  1. Read agency.config.json from the project root.
  2. Extract:
    • crm.type -- CRM backend type (e.g., google_sheets)
    • crm.sheet_id -- the Google Sheet ID
    • crm.webhook_url -- the Google Apps Script webhook endpoint
    • crm.tabs -- map of logical tab names to actual sheet tab names
    • tools.crm.access -- execution method: webhook | api | browser
    • tools.crm.api_key_env -- env var name for API credentials (if applicable)
  3. Accept parameters:
    • operation -- one of: READ, APPEND, UPDATE, CREATE_TAB
    • tab -- logical tab name (looked up in crm.tabs) or literal tab name
    • data -- the data to write (for APPEND/UPDATE operations)
    • query -- search/filter criteria (for READ operations)

Phase 1: Resolve Tab Name

Map the logical tab name to the actual sheet tab name using crm.tabs:

  • If tab matches a key in crm.tabs, use the mapped value.
  • If tab does not match any key, use it as-is (literal tab name).

Example: tab: "hawk_leads" resolves to "Hawk Leads" per the config.

Phase 2: Execute Operation

APPEND -- Add New Rows

Add one or more rows to a tab. Each row is an object with column headers as keys.

Via Webhook (preferred):

curl -s -X POST "{{crm.webhook_url}}" \
  -H "Content-Type: application/json" \
  -d '{
    "sheet": "{{resolved_tab_name}}",
    "headers": ["Col1", "Col2", "Col3"],
    "row": ["value1", "value2", "value3"]
  }'

Rules:

  • headers must match the existing column headers in the tab exactly (case-sensitive).
  • If the tab does not exist, the webhook will create it with the provided headers.
  • For multiple rows, send one request per row (the webhook processes single rows).
  • Add a timestamp column with ISO datetime for every write.
  • Rate limit: max 1 request per second to avoid Google quota issues.

Via API (if webhook is unavailable):

Use the Google Sheets API v4 with the service account credentials from the env var specified in tools.crm.api_key_env.

POST https://sheets.googleapis.com/v4/spreadsheets/{{crm.sheet_id}}/values/{{resolved_tab_name}}!A:Z:append?valueInputOption=USER_ENTERED
Authorization: Bearer {{access_token}}
Content-Type: application/json

{
  "values": [["value1", "value2", "value3"]]
}

READ -- Query Existing Data

Read rows from a tab, optionally filtered.

Via Webhook:

curl -s -X POST "{{crm.webhook_url}}" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "read",
    "sheet": "{{resolved_tab_name}}",
    "filters": {
      "column": "Stage",
      "value": "HOT"
    }
  }'

Via API:

GET https://sheets.googleapis.com/v4/spreadsheets/{{crm.sheet_id}}/values/{{resolved_tab_name}}!A:Z
Authorization: Bearer {{access_token}}

Then filter the returned data in-memory based on the query criteria.

Query options:

  • stage -- filter by pipeline stage (e.g., HOT, WARM, CONTACTED)
  • date_range -- filter by date column (e.g., last 7 days)
  • search -- substring match across all columns
  • limit -- max rows to return

UPDATE -- Modify Existing Rows

Update specific fields on existing rows. Identify the row by a unique key (usually URL, email, or company name).

Via Webhook:

curl -s -X POST "{{crm.webhook_url}}" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "update",
    "sheet": "{{resolved_tab_name}}",
    "match_column": "Email",
    "match_value": "[email protected]",
    "updates": {
      "Stage": "CONTACTED",
      "Last Touch": "2024-01-15",
      "Notes": "Sent cold email"
    }
  }'

Via API:

  1. Read the sheet to find the row number matching the key.
  2. Use PUT to update the specific cells.

CREATE_TAB -- Create a New Tab

Create a new tab with specified headers.

Via Webhook:

curl -s -X POST "{{crm.webhook_url}}" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create_tab",
    "sheet": "{{new_tab_name}}",
    "headers": ["Date", "Company", "Contact", "Email", "Stage", "Source", "Notes"]
  }'

Via API:

Use the Sheets API batchUpdate to add a new sheet, then write the header row.

Phase 3: Validation

Before executing any write operation:

  1. Data completeness: Verify all required columns have values. Warn if critical fields (company, contact, email) are empty.
  2. Dedup check (for APPEND): If the tab already contains a row with the same URL or email, warn and ask for confirmation before adding a duplicate.
  3. Tab existence: For APPEND/UPDATE, verify the tab exists first with a READ. If it does not exist, offer to CREATE_TAB first.

Phase 4: Error Handling

  • Webhook returns error: Log the error, retry once after 3 seconds. If still failing, report the error and the raw data so the user can manually add it.
  • API auth failure: Check if the env var exists. Report which env var is missing.
  • Rate limit: If Google returns 429, wait 5 seconds and retry. Max 3 retries.
  • Tab not found: Suggest creating the tab with CREATE_TAB.

Phase 5: Confirmation

After each operation, report:

CRM Write Complete:
- Tab: {{tab_name}}
- Operation: APPEND
- Rows written: {{count}}
- Timestamp: {{iso_datetime}}

For READ operations, return the data in a clean table format.

Example Usage

Trigger phrases:

  • "Log this lead to the CRM"
  • "Add these leads to Hawk Leads"
  • "Update the stage for [company]"
  • "Read all HOT leads from the pipeline"
  • "Create a new CRM tab for [purpose]"
  • "Write to the outreach log"
User: Log these 5 scored leads to Hawk Leads
Assistant: [reads config, resolves tab name, sends 5 webhook POSTs with rate limiting, confirms 5 rows written]
User: Show me all leads contacted this week
Assistant: [reads Outreach CRM tab, filters by date range, returns table]

Files bundled with it

These load only when the skill asks for them, so they cost nothing until it runs.

Other skills for the same job

Different authors, same problem. Matched on the words in the skill name, across every library in the catalogue except this one.

Need help setting it up?

This page tells you what crm-writer does and what it needs. Cheetah builds the agent setup it runs inside: data, CRM, sequencing and the guardrails.

Book a call →

The directory stays free. There is nothing gated behind this.