Turn a markdown report (competitive analysis, ad recon, performance
review) into a client-ready Google Sheet: multi-tab layout, themed
formatting, conditional colors. The shipped script bootstraps the
spreadsheet; everything the client actually sees is composed by the agent
per references/presentation-contract.md's
division of labor. Judgment calls — when to present vs hand over a CSV,
theme handling, tab structure — live in rules.md.
What this skill deliberately does NOT do
- Never edits report content. This is a formatting pass: numbers, ad copy, and quotes land verbatim, ad copy never truncates (rules, invariant 1). Wrong findings get fixed in the source report, not here.
- Ships no converter.
create_spreadsheet.pycreates one header-formatted tab and exits — parsing, population, and conditional formatting are agent-composed API calls, not hidden code (contract). - No Drive operations. Sheets-only scope: the file lands in the Drive root of the authorized account and gets moved/shared manually.
- Not a data pipeline. Recurring exports and re-import workflows belong to raw CSVs (rules.md § presenter vs raw CSV), not a presentation layer.
Prerequisites
- Credentials:
google-ads.yamlat project root — see the google-ads-api-setup skill if you don't have one. The script reuses this same file's OAuth credentials for the Sheets API — its refresh token must carry thespreadsheetsscope, which the setup skill's generator grants by default (token predates that? re-run the generator once) - Python packages:
pip install google-api-python-client google-auth pyyaml
Workflow
Step 1: Identify Input
When triggered, identify the source:
- If path provided: Use that path directly
- If recent analysis in conversation: Use that content
- If ambiguous: Ask user to specify
Step 2: Parse Markdown Structure
Analyze the markdown to determine:
- Document title and metadata
- Section hierarchy (H1, H2, H3)
- Tables with headers and data
- Lists (bulleted, numbered, checkbox)
- Scoring data (X/45, percentages, etc.)
Step 3: Determine Tab Structure
For Competitive Analysis / Ad Recon Reports:
| Section | Tab Name | Notes |
|---|---|---|
| Executive Summary | Summary |
First tab, KPIs |
| Strategic Analysis | Strategic_Scores |
15-attribute scoring |
| Tactical Scan | Tactical_Scores |
7-attribute flags |
| Gap Identification | Gaps |
Opportunities list |
| Verified Angles | Recommendations |
Client-ready angles |
| Per Competitor | Competitor_[Name] |
If multiple competitors |
For Performance Reports:
| Section | Tab Name | Notes |
|---|---|---|
| Overview | Dashboard |
KPI metrics |
| Campaign Data | Campaigns |
Main data table |
| Recommendations | Action_Items |
Next steps |
These are the standard maps. When to deviate — merging small sections, single-table reports, prose-only sources — is rules.md § table-structure judgment.
Step 4: Load the Theme
Every presentation value — colors (with API-ready RGB decimals),
typography tiers, conditional-formatting thresholds, row heights and
column widths — lives in
templates/professional-blue-theme.json.
Read it before composing any formatting call; don't improvise values
inline. Client-brand variants are copies of that file
(rules.md § theme judgment). What the file owns, block by
block: references/presentation-contract.md.
Step 5: Create Google Sheet
Use the create_spreadsheet.py script from your project root (where google-ads.yaml lives):
python .claude/skills/markdown-to-sheets-presenter/scripts/create_spreadsheet.py "Report Name"
Pass --config path/to/google-ads.yaml if your credentials live elsewhere. The sheet is created in the Drive root of the authorized account — move it into a client folder in Drive afterward if needed. The script returns the spreadsheet ID and URL as JSON; use the ID for the data-population and formatting calls that follow.
Naming Convention:
[Client Name] - [Report Type] - [YYYY-MM-DD]
Example: "Example Client - Competitive Analysis - 2025-12-16"
Step 6: Populate and Format
The agent-composed pass (no shipped code — contract § division of
labor): rename Data/add tabs per the Step 3 plan, write values, then
format from the theme — header/subheader styling, alternating rows,
conditional formatting on scores and status columns, column widths,
number formats. Formatting decisions (truncation ban, currency and
percentage handling, missing data as "—", large tables, chart placement)
are rules.md's tables — apply them as you build.
Step 7: Return Results
Provide user with:
- Clickable Google Sheets link
- Summary of tabs created
- Data rows per tab
Output Format
## Google Sheet Created
**Sheet Name:** [Name]
**Link:** [URL]
### Tabs Created:
| Tab | Rows | Description |
|-----|------|-------------|
| Summary | 15 | Executive overview with KPIs |
| Strategic_Scores | 20 | 15-attribute competitive scoring |
| ... | ... | ... |
---
*Click the link above to open your Google Sheet.*
Error Handling
| Error | Action |
|---|---|
| No credentials | Point at the google-ads-api-setup skill |
| 403 / PERMISSION_DENIED | Refresh token predates the Sheets scope — re-run the api-setup generator once, paste the new refresh_token into google-ads.yaml |
| File not found | Ask user to verify path |
| API quota | Suggest waiting or manual copy |
| Parse failure | Fall back to raw text layout |
Files
| File | Purpose |
|---|---|
SKILL.md |
This file — workflow + routing |
rules.md |
Judgment layer: presenter vs raw CSV, theme judgment, table structure, false alarms, escalation |
examples.md |
Three worked decisions incl. the asks this skill should decline |
references/presentation-contract.md |
The script/agent division of labor, exact script behavior, the theme file's contract |
SETUP.md |
Credential setup + quick test + troubleshooting |
scripts/create_spreadsheet.py |
The bootstrap: one spreadsheet, one formatted header row, JSON on stdout |
templates/professional-blue-theme.json |
The theme — single home of every presentation value, consumed by the agent (no script reads it) |
README.md |
User-facing overview + install |
After a Run
The Google Sheet is the artifact; nothing lands on local disk. The spreadsheet ID and URL print as JSON on stdout (contract) — capture them if you'll need the sheet programmatically later, because creation is never idempotent: re-running makes a second spreadsheet with the same name, and Drive allows that.
When to Load What
| Moment | Load |
|---|---|
| Deciding sheet-vs-CSV, structuring tabs, theming | rules.md |
| Exact script behavior, stdout/stderr contract, theme file blocks | references/presentation-contract.md |
| First run, or the ask smells like a decline case | examples.md |
| The input is a competitive analysis | competitor-analysis-v2 produces it — its 15 strategic + 7 tactical scoring is what the Strategic/Tactical tab map was built for |
| The ask is machine-readable data, not presentation | google-ads-query — its CSVs are the re-import surface |
| The source skill already writes a Sheet natively | Use it directly — e.g. account-diagnostic with --sheet-id; don't re-present a sheet |
| No credentials yet / 403 at the Sheets step | google-ads-api-setup (SETUP.md has the short version) |