Skip to content

What is TabulaFlow?

TabulaFlow is an open-source data agent built on a modular Python library.

Think of it as Claude Code for data: describe in natural language what you want to analyze, visualize, or transform. It works with all kinds of data, including SQL and graph databases, files, Hugging Face datasets, Wikidata, and web pages.

Unlike existing coding-agent harnesses, which are built around files and the shell, TabulaFlow treats tables as first-class citizens, as its name suggests:

  • Agent ergonomics. The agent writes only queries and visualization specifications. TabulaFlow handles data resolution and rendering, so the agent never wastes tokens embedding data values or writing HTML to create visual artifacts. See the comparison
  • Human ergonomics. Data provenance is automatically tracked: each visualization exposes its underlying data table, and each table exposes the query that produced it.
  • Shell-independent. The core harness remains fully functional for data work even when shell and filesystem access are disabled (e.g., when building hosted applications).

Like a general-purpose coding agent, TabulaFlow can also write code, run shell commands, and browse the web.

Get started

Install TabulaFlow with uv, set a model provider key, and launch it:

uv tool install tabulaflow
export OPENAI_API_KEY="your-api-key"
tabulaflow

See Models and providers for Anthropic, vLLM, and other providers.

We also recommend installing Chromium to enable agent-driven web browsing:

uv tool run --from playwright playwright install chromium

TabulaFlow opens with bundled sample data, so you can start exploring immediately.

What TabulaFlow can do

Consider TabulaFlow if you regularly analyze data in Jupyter notebooks, explore databases with DBeaver or Neo4j Browser, work with Hugging Face datasets or Wikidata, or conduct deep research with structured datasets.

  • Interactive visualization. Create charts, maps, and relationship graphs backed by queryable, parameterized data, including graphs from Neo4j.
  • Structured disambiguation for precise answer. Get precise data and answer when there is ambiguity via interactive structured disambiguation proposed in the ARCS paper.
  • Multimodal data browsing. Browse databases or Hugging Face datasets directly (no LLM needed). View images, PDFs, and other media directly inside tables, or ask an agent to analyze them.
  • Cross-source analysis. Combine files and databases in a local workspace without changing the original sources.
  • Large-scale dataset construction. Combine multiple sources and turn unstructured web pages and documents into structured, normalized tables with thousands of rows for deep research.
  • Agentic data enrichment. Enrich each row with an agent that can browse the web, query connected databases, and return typed results. Process many rows concurrently.
  • Parallel browser use. TabulaFlow's browser harness lets agents interact with many web pages in parallel during complex deep research tasks, including pages that require clicks and forms.

How does TabulaFlow work?

TabulaFlow's agent harness is optimized for data tasks. It handles data resolution, styling, and provenance tracking automatically so the agent doesn't need to. It also does not require shell or filesystem access. The comparison below shows how it creates an interactive chart alongside a typical coding-agent harness:

Show an interactive chart of NYC taxi-zone counts by borough, with an all/Manhattan-only choice control and a minimum-zone-area slider.

TabulaFlow 188 tokens
create_parameterized_source→ S1
SELECT borough
FROM nyc_taxi_zones
WHERE shape_area * 1000000 >= {{ nyc_min_area }}
{% if nyc_scope == 'manhattan' %}
  AND borough = 'Manhattan'
{% endif %}

---

[{"kind":"choice","id":"nyc_scope","label":"Borough scope","choices":[{"id":"all","label":"All boroughs"},{"id":"manhattan","label":"Manhattan only"}]},{"kind":"number","id":"nyc_min_area","label":"Minimum zone area (×10⁻⁶ source units)","min":0,"max":1000,"step":25,"default":0}]
render_chartsource=S1
{
  "mark": "bar",
  "encoding": {
    "x": {"field": "borough", "type": "nominal"},
    "y": {"aggregate": "count", "type": "quantitative"}
  }
}
Codex 2,768 tokens
Show 2 tool callsHide 2 tool calls
exec_command
import sqlite3, json
c = sqlite3.connect('upload/sample(1).sqlite')
rows = c.execute('SELECT borough, shape_area FROM nyc_taxi_zones WHERE borough IS NOT NULL AND shape_area IS NOT NULL').fetchall()
print(len(rows))
print(json.dumps(rows))
print(c.execute('SELECT borough, COUNT(*), MIN(shape_area), MAX(shape_area) FROM nyc_taxi_zones GROUP BY borough').fetchall())
exec_command
import sqlite3, json
c = sqlite3.connect('upload/sample(1).sqlite')
d = {b: [round(a * 1000000, 1) for a, in c.execute('SELECT shape_area FROM nyc_taxi_zones WHERE borough=? ORDER BY shape_area', (b,))] for b in ['Bronx', 'Brooklyn', 'EWR', 'Manhattan', 'Queens', 'Staten Island']}
print(json.dumps(d, separators=(',', ':')))
apply_patchtaxi-zone-borough-counts.html
*** Begin Patch
*** Add File: taxi-zone-borough-counts.html
+<div id="taxi-zone-borough-counts" aria-labelledby="taxi-chart-title">
+  <style>
+    #taxi-zone-borough-counts { color: var(--foreground); font-family: inherit; }
+    #taxi-zone-borough-counts .chart-wrap { margin-top: 1rem; width: 100%; }
+    #taxi-zone-borough-counts svg { display: block; width: 100%; height: auto; }
+    #taxi-zone-borough-counts .grid, #taxi-zone-borough-counts .axis { stroke: var(--border); stroke-width: 1; }
+    #taxi-zone-borough-counts .axis { stroke: var(--foreground); }
+    #taxi-zone-borough-counts .bar { fill: var(--viz-series-1); transition: height 180ms ease, y 180ms ease, opacity 180ms ease; }
+    #taxi-zone-borough-counts .bar-label { fill: var(--foreground); font-size: 12px; font-weight: 500; }
+    #taxi-zone-borough-counts .tick, #taxi-zone-borough-counts .axis-title { fill: var(--muted-foreground); font-size: 12px; }
+    #taxi-zone-borough-counts .status { margin-top: .5rem; color: var(--muted-foreground); }
+    #taxi-zone-borough-counts .control-value { color: var(--foreground); font-weight: 500; }
+  </style>
+  <h2 id="taxi-chart-title">NYC taxi zones by borough</h2>
+  <div class="viz-controls" aria-label="Taxi zone filters">
+    <div>
+      <label class="form-label" for="borough-scope">Borough scope</label>
+      <select id="borough-scope" class="form-select">
+        <option value="all">All boroughs</option>
+        <option value="Manhattan">Manhattan only</option>
+      </select>
+    </div>
+    <div>
+      <label class="form-label" for="min-area">Minimum zone area <span id="area-value" class="control-value">0</span></label>
+      <input id="min-area" class="form-range" type="range" min="0" max="1000" value="0" step="25" aria-describedby="area-value">
+    </div>
+  </div>
+  <div class="chart-wrap">
+    <svg id="taxi-zone-chart" role="img" aria-labelledby="taxi-chart-title taxi-chart-desc" viewBox="0 0 740 390">
+      <desc id="taxi-chart-desc">Bar chart showing the count of taxi zones by borough after applying a borough and minimum-area filter.</desc>
+    </svg>
+  </div>
+  <div id="taxi-status" class="status tabular-nums" aria-live="polite"></div>
+  <script>
+    (() => {
+      const root = document.getElementById('taxi-zone-borough-counts');
+      const scope = root.querySelector('#borough-scope');
+      const minArea = root.querySelector('#min-area');
+      const areaValue = root.querySelector('#area-value');
+      const svg = root.querySelector('#taxi-zone-chart');
+      const status = root.querySelector('#taxi-status');
+      const areas = {"Bronx":[62.6,62.9,91.1,95.2,106.4,134.5,146,148.5,148.9,149.6,150.9,155.9,161.3,161.8,163.2,167.5,171.2,185.8,191.1,199.1,205,205.6,212.8,228.5,241,254.7,288.7,313,314.4,334,360,360.1,394.6,395.8,399.6,547.1,703.3,722.1,744.6,904.1,926.4,1988.8,2020.3],"Brooklyn":[45.2,81.8,101.4,108.4,108.9,113.6,114.7,124.2,130.3,132.5,138.9,143.6,147.4,157.2,158.2,163.3,168.6,172.3,173.9,175.8,191.9,198.9,201.7,203.2,208.7,247.7,264.5,268.3,270.6,270.9,296.4,306.9,310.8,313,323,323.5,323.8,332.6,352.9,353.2,354,380.3,382.6,394.3,407.3,447.5,452.1,453.2,462.1,472.1,510.8,534,537.3,736.3,801.7,868.3,900.3,993.4,1266.6,1381.8,1789.7],"EWR":[782.3],"Manhattan":[6.3,11.9,32.5,34.3,37.1,37.5,38.9,39.7,40.9,41.5,47,47.5,47.9,55.3,55.6,56.1,57.3,58,60.7,65.8,67.2,69.7,71.9,72.2,73.1,74.3,74.6,75.7,76.4,76.7,91.4,93.1,94.3,94.7,96,98.5,102.9,106.2,107.9,111.1,111.9,114.2,116.2,116.5,122.3,128.8,143.1,146.6,149.4,151.1,163.2,166.9,173.2,184.8,185.6,204.7,207.4,216,240.6,255.3,260.4,263.9,273.5,281.3,295,359.7,368.6,379.7,438.4],"Queens":[18,26.6,44.7,78.3,104.6,145.6,145.9,178.3,180.9,183.9,195.5,197.5,202.4,217.5,226.6,247.7,281.3,291.2,291.8,296.6,323.6,327.4,338.4,346.6,354.4,366.2,373.8,374.9,384.6,389.8,395.6,422.3,423.7,428.2,435.8,446.7,452.3,467.9,468.3,474,485.8,499.8,504.7,504.9,520.1,536.8,546.7,547.6,571.2,572.8,594.3,615.1,619.4,623.3,688.5,754.8,761.9,764.4,766.2,816.1,871.9,904.3,925.2,947.5,1025.3,1057.9,1340.9,2038.3,4866.3],"Staten Island":[373.2,421.2,466.2,498,525.7,606.5,625.8,641.4,657.8,658.4,812,890.1,944.4,1052.1,1169.6,1210.2,1826.9,1944.7,2073.8,2195.6]};
+      const order = ['Bronx','Brooklyn','Manhattan','Queens','Staten Island','EWR'];
+      const ns = 'http://www.w3.org/2000/svg';
+      const el = (tag, attrs = {}, text = '') => { const n = document.createElementNS(ns, tag); Object.entries(attrs).forEach(([k,v]) => n.setAttribute(k,v)); n.textContent = text; return n; };
+      function draw() {
+        const threshold = Number(minArea.value); areaValue.textContent = \`\${threshold} × 10⁻⁶ area units\`;
+        const rows = order.map(b => ({borough:b,count:(scope.value === 'all' || scope.value === b) ? areas[b].filter(a => a >= threshold).length : 0,visible:scope.value === 'all' || scope.value === b}));
+        const max = Math.max(1, ...rows.map(x => x.count)); const W=740,H=390,L=64,R=24,T=40,B=76, pw=W-L-R, ph=H-T-B;
+        svg.replaceChildren();
+        [0,Math.ceil(max/2),max].filter((x,i,a)=>a.indexOf(x)===i).forEach(t=>{const y=T+ph-(t/max)*ph;svg.append(el('line',{class:'grid',x1:L,x2:W-R,y1:y,y2:y}));svg.append(el('text',{class:'tick',x:L-9,y:y+4,'text-anchor':'end'},String(t)));});
+        svg.append(el('line',{class:'axis',x1:L,x2:L,y1:T,y2:T+ph}));svg.append(el('line',{class:'axis',x1:L,x2:W-R,y1:T+ph,y2:T+ph}));
+        svg.append(el('text',{class:'axis-title',x:18,y:T+ph/2,transform:\`rotate(-90 18 \${T+ph/2})\`,'text-anchor':'middle'},'Zone count'));
+        const slot=pw/rows.length,bw=Math.min(72,slot*.58);
+        rows.forEach((row,i)=>{const x=L+i*slot+(slot-bw)/2,h=(row.count/max)*ph,y=T+ph-h;const bar=el('rect',{class:'bar',x,y,width:bw,height:h,rx:2,tabindex:0,role:'img','aria-label':\`\${row.borough}: \${row.count} zones\`,opacity:row.visible?1:.18});bar.setAttribute('data-tooltip',\`\${row.borough}: \${row.count} zones\`);svg.append(bar);if(row.visible)svg.append(el('text',{class:'bar-label',x:x+bw/2,y:Math.max(T+14,y-8),'text-anchor':'middle'},String(row.count)));svg.append(el('text',{class:'tick',x:x+bw/2,y:T+ph+26,'text-anchor':'middle'},row.borough));});
+        const total=rows.reduce((s,x)=>s+x.count,0); status.textContent = \`\${total} matching taxi zone\${total===1?'':'s'}.\`;
+      }
+      scope.addEventListener('change',draw); minArea.addEventListener('input',draw); draw();
+    })();
+  </script>
+</div>
*** End Patch

Connected sources are read-only. When necessary, the agent can transform tables in a local workspace and keep intermediate files in a temporary scratch directory, so your source data and project directory remain unchanged by default. You can ask the agent at any time to export results to local files in any format you need for sharing or further use.

TabulaFlow also goes beyond existing AI database assistants (e.g., Chat2DB), which typically focus on generating SQL for a single database. TabulaFlow supports broader, general-purpose workflows across SQL and graph databases, local files, public datasets, and the web.

Build and research with TabulaFlow

Build with the Python library

Create your own data agents and applications with an async-native library written in pure Python. Reuse its connectors, tools, and structured outputs to build workflows tailored to your needs. Explore the library

Run research experiments

Run large-scale experiments on text-to-SQL and text-to-Cypher benchmarks such as Spider 2.0, CypherBench, and ARCS with TabulaFlow's research toolkit. Explore the toolkit

Public beta

TabulaFlow 0.4.2 is a public beta. Patch releases preserve documented public APIs; minor 0.x releases may include documented breaking changes. We welcome your feedback.