Blog · Claude Code × CSV

CSV analysis with Claude Code: from export to live dashboard

Published July 20, 2026 · 8 min read

Claude Code is genuinely good at CSVs — right up to the moment the analysis needs to survive the week. Point it at a file and it will read the header, guess the delimiter, write the pandas or DuckDB you would have written, and explain the result. For a one-off question that's the whole job, and you should just do it. This guide is about the other case: the export that arrives every Monday, the file too big to sit in context, the answer a client needs as a link rather than a paragraph — and the channel that has no API at all, so a file is the only way in.

First, the case where you don't need anything

If the question is "what's in this file and what does it say," ask the agent. It will pick a tool and go:

claude code
# the shape of a one-off: read it, group it, answer, done
head -3 tiktok-export.csv
python3 -c "import pandas as pd; d=pd.read_csv('tiktok-export.csv'); \
  print(d.groupby('campaign')[['cost','conversions']].sum())"

This is fine. It's fast, it's local, nothing leaves your machine, and no tooling decision is required. Reach for it whenever the answer is disposable.

Where the one-off approach breaks

Four failure modes show up in roughly this order, and each one is a symptom of the same thing — the CSV is data, but you've been treating it as a message:

  • Context is not a database. Paste a 40,000-row export into the conversation and you're paying tokens to store rows the model will summarize lossily anyway. Past a certain size the agent starts sampling, and a sampled answer looks exactly like a real one.
  • Every export is a snapshot. Monday's file answered Monday's question. Next week the analysis has to be rebuilt from scratch, because nothing about it was durable — not the parsing, not the column mapping, not the query.
  • The answer isn't shareable. A number in your terminal isn't something a client can open. The usual patch is a screenshot, which is a snapshot of a snapshot.
  • One file is never the question. "How did TikTok do" is really "how did TikTok do compared to Meta and Google" — and now you're hand-joining an export against two APIs, reconciling three different spellings of conversion, and hoping you picked right.

That last one is the expensive failure. An agent handed raw exports will silently choose an interpretation, compute something plausible, and narrate a wrong story with total confidence. The failure mode isn't bad arithmetic — it's mislabeled arithmetic.

Load the CSV once, query it forever

TableBI is a data backend built to be driven from the CLI by an agent like Claude Code, and CSV is a first-class way in. One command normalizes a file into the same cross-channel definitions your live sources use:

terminal
# install the CLI, then teach your agent to drive it
npm i -g @tablebi/cli
tablebi login
tablebi install

# one-shot: the file lands in the shared definitions
tablebi connect csv --file tiktok-export.csv --platform tiktok_ads

The --platform tag is the important half. It's the label that lets a TikTok export sit in the same table as live Meta Ads, Google Ads, GA4 and Search Console rows, sharing dimensions — date, platform, account, campaign, ad_group, channel, country — and measures: impressions, clicks, cost, conversions, conversion_value. Once it's in, check what landed before you trust it:

terminal
tablebi sources          # what's connected, and how fresh
tablebi sample           # a few real rows per table
tablebi schema           # dimensions, measures, definition macros

CSV is one-shot by design. A file has no API to poll, so it loads once. Sources that do have an API — Search Console, GA4, Meta Ads, Google Ads — authorize once via OAuth and then sync on a schedule. Use CSV for the channels that only give you an export button.

Now the queries are boring — which is the point

With the file in the same shape as everything else, cross-channel questions stop being joins you maintain:

claude code → tablebi
# CSV-loaded TikTok next to live Meta + Google Ads, one query
tablebi ask "SELECT platform, SUM(cost) AS spend,
             SUM(conversions) AS conv, cpa(SUM(cost), SUM(conversions)) AS cpa
             FROM facts
             WHERE date >= (SELECT MAX(date) FROM facts) - 28
             GROUP BY platform ORDER BY spend DESC"

Ratios come from definition macros (roas, ctr, cpc, cpa, cvr, aov…) that live in the query engine, so the agent never re-derives one slightly wrong. And campaign-level triage inside the imported channel is one more query:

claude code → tablebi
# which imported campaigns burned budget with nothing to show
tablebi ask "SELECT campaign, SUM(cost) AS spend, SUM(conversions) AS conv
             FROM facts
             WHERE platform = 'tiktok_ads'
               AND date >= (SELECT MAX(date) FROM facts) - 30
             GROUP BY campaign HAVING SUM(conversions) = 0
             ORDER BY spend DESC LIMIT 10"

Note the date windows: (SELECT MAX(date) FROM facts) - N, never current_date. Exports and platform APIs both land late, so clock-anchored windows report a half-filled final day as a collapse. Anchoring to the data's own high-water mark is what separates a real alarm from a fake one — and every answer also carries a trust block (per-source freshness, caveats, lineage) so the agent states which window it actually analyzed. More on that discipline in Claude Code analytics.

Pin it so the answer outlives the file

The last gap the one-off approach can't close is delivery. A pinned analysis is a public, read-only URL — title, optional chart kind after two colons (line, bar, hbar, pie, area, scorecard), and the query:

claude code → tablebi
tablebi pin --title "Paid channels overview" \
  --widget "Spend by platform::bar=SELECT platform, SUM(cost) AS spend FROM facts …" \
  --widget "Daily spend (28d)::line=SELECT date, SUM(cost) AS spend FROM facts …"
✓ published → https://dk.tablebi.com/d/dsh_…  (public, read-only, self-refreshing)

Widgets built on live sources keep refreshing as those sources sync; the CSV portion holds the rows you imported. Here's a real pinned dashboard — not a mockup:

A rule of thumb for choosing

You don't need a backend for every file. The dividing line is durability:

  • Local pandas or DuckDB when the question is disposable, the file is small, and you're the only consumer. One question, one answer, no residue.
  • Load it into shared definitions when the export repeats, when it must be compared against another channel, when someone else needs the result, or when the file is large enough that "pasting it in" means sampling it.
  • Prefer the live connector whenever one exists. If the channel has an API, a CSV of that channel is a strictly worse version of it. Use files for what only ships as files.

If most of your channels do have APIs, start with Claude Code for marketing for the full connect-ask-pin loop, or Claude Code + GA4 for the analytics side specifically. For the many-clients version of this problem — a stack of exports and one Monday deadline — see the agency reporting tool guide.

FAQ

Can Claude Code analyze a CSV file?

Yes — for a one-off it reads the file and writes pandas or DuckDB against it directly. That breaks when the file is too large for context, when the answer has to be shared, or when the same analysis runs again next week on a fresh export.

How do I load a CSV so my agent can query it repeatedly?

tablebi connect csv --file export.csv --platform tiktok_ads. The file is normalized into the same definitions as live sources, so any session can query it with read-only SQL — no re-pasting, no re-parsing.

Can CSV data sit alongside Search Console, GA4 and ad platforms?

That's what --platform is for. The export lands in the same facts table as your live sources, sharing dimensions like date, campaign and channel, so one query spans every channel.

Is a CSV upload a one-time thing?

Yes — a file has no API to poll, so CSV is one-shot. Sources with an API authorize once via OAuth and sync on a schedule. Use CSV for channels that only offer an export button.

Try it

Turn the export you re-paste every Monday into a query your agent already knows.

terminal
npm i -g @tablebi/cli && tablebi install