packages/desktop/apps/electron/resources/docs/data-tables.md
This guide covers how to present structured data using datatable and spreadsheet blocks, and how to use the transform_data tool for large datasets.
Qwen Code supports three ways to display tabular data:
| Format | Best For | Interactivity |
|---|---|---|
| Markdown table | Small, simple data (3-4 rows) | None |
datatable block | Query results, comparisons, any data users may sort/filter | Sort, filter, group-by, search |
spreadsheet block | Financial reports, exports, data users may download as .xlsx | Sort, export to Excel/CSV |
Key principle: For datasets with 20+ rows, use the transform_data tool to write data to a JSON file and reference it via "src" instead of inlining all rows. This dramatically reduces token usage and cost.
For datasets under 20 rows, inline the data directly in the markdown block:
```datatable
{
"title": "Top Users",
"columns": [
{ "key": "name", "label": "Name", "type": "text" },
{ "key": "revenue", "label": "Revenue", "type": "currency" },
{ "key": "growth", "label": "Growth", "type": "percent" },
{ "key": "active", "label": "Active", "type": "boolean" },
{ "key": "tier", "label": "Tier", "type": "badge" }
],
"rows": [
{ "name": "Acme Corp", "revenue": 4200000, "growth": 0.152, "active": true, "tier": "Enterprise" },
{ "name": "StartupCo", "revenue": 85000, "growth": -0.03, "active": true, "tier": "Starter" }
]
}
```
```spreadsheet
{
"filename": "q4-revenue.xlsx",
"sheetName": "Revenue",
"columns": [
{ "key": "month", "label": "Month", "type": "text" },
{ "key": "revenue", "label": "Revenue", "type": "currency" }
],
"rows": [
{ "month": "October", "revenue": 125000 },
{ "month": "November", "revenue": 142000 }
]
}
```
| Type | Input Format | Rendered As | Example Input | Example Output |
|---|---|---|---|---|
text | Any string | Plain text | "John Doe" | John Doe |
number | Number | Formatted number | 1500000 | 1,500,000 |
currency | Raw number (not formatted) | Dollar amount | 4200000 | $4,200,000 |
percent | Decimal (0-1 range) | Percentage with color | 0.152 | +15.2% (green) |
boolean | true/false | Yes/No | true | Yes |
date | Date string | Formatted date | "2025-01-15" | Jan 15, 2025 |
badge | String | Colored status pill | "Active" | Active (badge) |
Important notes:
currency — Pass the raw number, NOT a formatted string. 4200000 renders as $4,200,000.percent — Pass as decimal. 0.152 renders as +15.2%. Positive values are green, negative are red.boolean — Use actual true/false, not strings.Use the transform_data tool + "src" field when:
transform_data runs a script in an isolated subprocess that reads input files and writes structured JSON output.
Parameters:
| Parameter | Type | Description |
|---|---|---|
language | "python3" | "node" | "bun" | Script runtime |
script | string | Transform script source code |
inputFiles | string[] | Input file paths relative to session dir |
outputFile | string | Output file name (written to session data/ dir) |
Path conventions:
long_responses/tool_result_abc.txt — saved tool resultsdata/previous_output.json — output from a prior transformattachments/data.csv — user-attached filesdata/ directory. Just provide the filename (e.g., "transactions.json")Script argument conventions:
sys.argv[1:-1] = input files, sys.argv[-1] = output pathprocess.argv.slice(2, -1) = input files, process.argv.at(-1) = output pathThe output file should contain valid JSON in one of these formats:
Full format (recommended):
{
"title": "Recent Transactions",
"columns": [
{ "key": "date", "label": "Date", "type": "date" },
{ "key": "amount", "label": "Amount", "type": "currency" },
{ "key": "status", "label": "Status", "type": "badge" }
],
"rows": [
{ "date": "2025-01-15", "amount": 250.00, "status": "Completed" }
]
}
Rows-only format:
{
"rows": [
{ "date": "2025-01-15", "amount": 250.00, "status": "Completed" }
]
}
Or just a bare array:
[
{ "date": "2025-01-15", "amount": 250.00, "status": "Completed" }
]
Merge semantics: When using "src", inline columns and title in the markdown block take precedence over values in the file. This lets you define column types in the block while pulling rows from the file.
After transform_data succeeds, it returns the absolute path to the output file. Use that exact path as the "src" value in your datatable or spreadsheet block:
```datatable
{
"src": "/absolute/path/returned/by/transform_data",
"title": "Recent Transactions",
"columns": [
{ "key": "date", "label": "Date", "type": "date" },
{ "key": "amount", "label": "Amount", "type": "currency" },
{ "key": "status", "label": "Status", "type": "badge" }
]
}
```
Important: Always use the absolute path from the transform_data tool result. Do not construct relative paths manually.
User asks: "Show me all Stripe transactions from last month"
Step 1: Call the Stripe API via MCP tool — get large JSON response
Step 2: Call transform_data to extract and structure the data:
transform_data({
language: "python3",
script: "import json, sys\nwith open(sys.argv[1]) as f:\n data = json.load(f)\nrows = [{\n 'id': t['id'],\n 'date': t['created'],\n 'amount': t['amount'] / 100,\n 'status': t['status'].title(),\n 'customer': t.get('customer_email', 'N/A')\n} for t in data.get('data', data.get('transactions', []))]\nwith open(sys.argv[-1], 'w') as f:\n json.dump({'rows': rows}, f)",
inputFiles: ["long_responses/stripe_result.txt"],
outputFile: "transactions.json"
})
Step 3: Output the datatable block using the absolute path from transform_data result:
```datatable
{
"src": "/absolute/path/from/transform_data/result",
"title": "Stripe Transactions — Last Month",
"columns": [
{ "key": "id", "label": "ID", "type": "text" },
{ "key": "date", "label": "Date", "type": "date" },
{ "key": "amount", "label": "Amount", "type": "currency" },
{ "key": "status", "label": "Status", "type": "badge" },
{ "key": "customer", "label": "Customer", "type": "text" }
]
}
```
Most common pattern. Extract fields from a JSON API response:
Python:
import json, sys
with open(sys.argv[1]) as f:
data = json.load(f)
# Handle common API response shapes
items = data.get('data', data.get('items', data.get('results', data)))
if not isinstance(items, list):
items = [items]
rows = [{
'id': item['id'],
'name': item.get('name', ''),
'created': item.get('created_at', ''),
} for item in items]
with open(sys.argv[-1], 'w') as f:
json.dump({'rows': rows}, f)
Parse CSV data into a spreadsheet for export:
Python:
import csv, json, sys
with open(sys.argv[1]) as f:
reader = csv.DictReader(f)
rows = list(reader)
# Auto-detect columns from CSV headers
columns = [{'key': k, 'label': k.replace('_', ' ').title(), 'type': 'text'} for k in rows[0].keys()] if rows else []
with open(sys.argv[-1], 'w') as f:
json.dump({'columns': columns, 'rows': rows}, f)
Combine data from multiple tool results:
Python:
import json, sys
# sys.argv[1:-1] are input files, sys.argv[-1] is output
with open(sys.argv[1]) as f:
users = {u['id']: u for u in json.load(f)['data']}
with open(sys.argv[2]) as f:
orders = json.load(f)['data']
rows = [{
'order_id': o['id'],
'customer': users.get(o['user_id'], {}).get('name', 'Unknown'),
'amount': o['total'],
'status': o['status'],
} for o in orders]
with open(sys.argv[-1], 'w') as f:
json.dump({'rows': rows}, f)
Call with:
transform_data({
language: "python3",
script: "...",
inputFiles: ["long_responses/users.txt", "long_responses/orders.txt"],
outputFile: "orders-with-customers.json"
})
Summarize data before display:
Python:
import json, sys
from collections import defaultdict
with open(sys.argv[1]) as f:
data = json.load(f)
# Group by category and sum
totals = defaultdict(lambda: {'count': 0, 'total': 0})
for item in data['transactions']:
cat = item.get('category', 'Other')
totals[cat]['count'] += 1
totals[cat]['total'] += item['amount']
rows = [{'category': k, 'count': v['count'], 'total': v['total']}
for k, v in sorted(totals.items(), key=lambda x: -x[1]['total'])]
with open(sys.argv[-1], 'w') as f:
json.dump({'rows': rows}, f)
When Python isn't available or you prefer JavaScript:
Node:
const fs = require('fs');
const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf-8'));
const rows = data.items.map(item => ({
id: item.id,
title: item.title,
status: item.state,
created: item.created_at,
}));
fs.writeFileSync(process.argv.at(-1), JSON.stringify({ rows }));
data/ directory. Path traversal attempts (e.g., ../) are blocked.AWS_*, GITHUB_TOKEN, GOOGLE_API_KEY, STRIPE_SECRET_KEY, NPM_TOKEN, and other configured secret variablesIs the data < 20 rows?
→ YES: Inline it directly in the datatable/spreadsheet block
→ NO: Use transform_data + "src" field
Is the data already structured JSON?
→ YES: Write a simple extraction script
→ NO: Use Python's csv, json, or string parsing to structure it
Does the user need to export/download?
→ YES: Use spreadsheet block (supports .xlsx export)
→ NO: Use datatable block (better sort/filter/group UX)
stripe-transactions.json, monthly-revenue.jsonq4-sales.jsontry/except (Python) or try/catch (Node) for JSON parsingpip install or external dependenciesjson.dump with default serialization — don't try to format numbers in the script; let column types handle renderingYYYY-MM-DD) — the date column type handles formattingsys.argv / process.argv correctlysys.argv[-1] / process.argv.at(-1) (the last argument)json.dump / fs.writeFileSync completed successfullylong_responses/ prefix for saved tool results, attachments/ for user-uploaded files"rows" key with an array, or be a bare array"key" fields exactly (case-sensitive)currency/percent, not strings)"src" path must be the absolute path returned by transform_data — do not use relative pathstransform_data (check the tool result message)