Install
$ agentstack add mcp-eliasstravik-rowbound ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ● Filesystem access Used
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Rowbound
A CLI for GTM Engineering in Google Sheets with Claude Code.
Demo
Install
Prerequisites
- Node.js 22+ —
node --versionmust be >= 22.0.0 - gws CLI — Google Workspace CLI for Sheets access
npm install -g @googleworkspace/cli
gws auth setup # first time: creates Cloud project, enables APIs, logs in
gws auth login # subsequent logins
Quick install
npm install -g github:eliasstravik/rowbound
Build from source
git clone https://github.com/eliasstravik/rowbound.git
cd rowbound
npm install
npm run dev --
Quick Start
# 1. Initialize a sheet
rowbound init
# 2. Add an action
rowbound config add-action --json '{
"id": "enrich_company",
"type": "http",
"target": "company_info",
"method": "GET",
"url": "https://api.example.com/company?domain={{row.domain}}",
"headers": { "Authorization": "Bearer {{env.API_KEY}}" },
"extract": "$.name"
}'
# 3. Store API keys and run
rowbound env set API_KEY=your_key_here
rowbound run
rowbound run --dry-run # preview first
Column names are automatically resolved to stable IDs when you run rowbound sync.
Features
Sources — Create rows from external data
- HTTP sources — fetch from any API, extract array from response, map columns via JSONPath
- Exec sources — run shell commands, parse JSON output into rows
- Script sources — run named scripts to generate rows from their output
- Webhook sources — accept inbound POST payloads, create rows in real-time
- Deduplication — skip or update existing rows based on a match column
- Scheduling — run sources manually, hourly, daily, or weekly
Actions — Enrich existing rows
- HTTP actions — call any REST API with templated URLs, headers, and bodies; extract values with JSONPath;
ifEmptyfallback when extract returns nothing - Waterfall actions — try multiple providers in order until one returns a result (e.g., Clearbit → Apollo → Hunter)
- Formula actions — compute derived values with JavaScript expressions using
{{Column Name}}references - AI actions — run headless Claude or Codex per row with configurable model, max turns, tools, and
baremode for faster startup - Exec actions — run shell commands and capture stdout
- Script actions — run reusable named scripts stored in config; supports bash, python3, and node runtimes
- Lookup actions — pull data from other tabs (boolean, count, or full row JSON)
- Write actions — push data to other tabs with column mapping; supports append, upsert, and array expansion via
expandPath - Per-action environment variables — inject env vars per action (e.g.,
PLAYWRIGHT_HEADLESS=true)
Pipeline
- Conditional execution — skip actions per-row with
whenexpressions - Smart skip — automatically skips rows where the target cell already has a value
- Watch mode — poll sheets on an interval or trigger runs via webhook
- Column tracking — stable column IDs that survive header renames and reordering
--columnsflag — target specific columns by letter (e.g.,--columns A-C,E,AP)--rowsflag — flexible row specs (e.g.,--rows 2-5,8,10-12)- Rate limiting — configurable seconds between requests (default: 1 per second)
- Timeouts in seconds — all user-facing timeouts in seconds, not milliseconds
- Retry with backoff — exponential, linear, or fixed backoff on failures
- Structured error handling — per-action
onErrorconfig maps status codes to actions (skip, write fallback) - MCP server — expose all operations as Model Context Protocol tools for Claude Desktop and other AI assistants
- Run history — track pipeline executions with per-action summaries, durations, and error logs
- Dry run — preview what would change without writing to the sheet
- Per-tab stop/start — enable or disable processing per tab; stops mid-run if toggled during execution
- Per-tab settings — override concurrency, rate limit, retries, and backoff per tab
- BYOK — bring your own API keys, pay only for the APIs you use
CLI Commands
| Command | Description | |---------|-------------| | rowbound init | Initialize a sheet with a default pipeline config | | rowbound run | Run the enrichment pipeline (--dry-run, --rows, --columns) | | rowbound status | Show pipeline status and enrichment rates | | rowbound watch | Watch for changes and run continuously (--interval, --port) | | rowbound sync | Reconcile columns, validate config, fix issues | | rowbound config show | Display the pipeline config as JSON | | rowbound config add-action | Add an action to the pipeline | | rowbound config remove-action | Remove an action by ID | | rowbound config update-action | Update an action (merge partial JSON) | | rowbound config list-actions | List configured actions (--json) | | rowbound config add-source | Add a source to the pipeline | | rowbound config remove-source | Remove a source by ID | | rowbound config update-source | Update a source (merge partial JSON) | | rowbound config set | Update pipeline settings (--enabled, --disabled, --concurrency, --rate-limit, etc.) | | rowbound config add-script | Add a script to the pipeline config | | rowbound config remove-script | Remove a script by name | | rowbound config update-script | Update a script (merge partial JSON) | | rowbound config validate | Validate the pipeline config | | rowbound runs [runId] | List recent runs or view a specific run | | rowbound runs clear | Delete all run history | | rowbound source run | Run a source to create rows (--source, --dry-run) | | rowbound source list | List configured sources | | rowbound env set | Store an API key globally | | rowbound env remove | Remove a stored key | | rowbound env list | List stored keys (values masked) | | rowbound mcp | Start the MCP server (stdio) |
MCP Server
Rowbound exposes all pipeline operations as MCP tools. Add this to your Claude Desktop config:
{
"mcpServers": {
"rowbound": {
"command": "rowbound",
"args": ["mcp"]
}
}
}
| Tool | Description | |------|-------------| | init_pipeline | Initialize a sheet with a default pipeline config | | run_pipeline | Run the enrichment pipeline | | add_action / remove_action / update_action | Manage pipeline actions | | add_source / remove_source / update_source | Manage data sources | | run_source | Execute a source to create rows | | update_settings | Update pipeline settings (concurrency, rate limit, retry) | | sync_columns | Sync the column registry with the current sheet state | | get_config / validate_config | Read or validate the pipeline config | | get_status | Return pipeline status with enrichment rates | | dry_run | Run in dry mode (no writes) | | start_watch / stop_watch | Manage watch mode | | preview_rows | Read and display rows from the sheet | | list_runs / get_run | View pipeline run history |
Source Types
Sources create rows from external data. They run before actions in the pipeline — new rows are created first, then actions enrich them on the next run.
http source
Fetch from an API and create rows from the response.
{
"id": "search_companies",
"type": "http",
"method": "POST",
"url": "https://api.blitz-api.ai/v2/search/company",
"headers": { "x-api-key": "{{env.BLITZ_API_KEY}}" },
"body": { "industry": "restaurants", "country_code": ["SE"] },
"extract": "$",
"extractPath": "$.results",
"columns": { "Title": "$.company_name", "Website": "$.website_url", "LinkedIn": "$.linkedin_url" },
"dedup": "Website",
"schedule": "daily"
}
exec source
Run a shell command and parse JSON output into rows.
{
"id": "import_leads",
"type": "exec",
"command": "curl -s https://api.example.com/leads",
"extract": "$.data",
"columns": { "Name": "$.name", "Email": "$.email" },
"dedup": "Email",
"updateExisting": true
}
webhook source
Accept inbound POST payloads and create rows. Used with rowbound watch.
{
"id": "form_submissions",
"type": "webhook",
"columns": { "Name": "$.name", "Email": "$.email", "Company": "$.company" },
"dedup": "Email"
}
Source options
| Field | Description | |-------|-------------| | columns | Maps sheet column headers to JSONPath per item: { "Name": "$.name" }. Use $.nested.field for nested data, or literal strings for static values. | | extract / extractPath | JSONPath to locate the array in the response. extractPath drills into a nested object first (e.g., $.results extracts from {"results": [...]}). | | dedup | Column header to deduplicate on. Existing rows with the same value are skipped. | | updateExisting | When true and dedup is set, update matched rows instead of skipping (default: false). | | schedule | "manual" (default), "hourly", "daily", or "weekly". Watch mode checks schedules automatically. |
script source
Run a named script (defined in the scripts config section) and parse its output into rows.
{
"id": "import_from_script",
"type": "script",
"script": "fetch_leads",
"args": ["--format", "json"],
"extract": "$.leads",
"columns": { "Name": "$.name", "Email": "$.email" },
"dedup": "Email"
}
Scripts
Scripts are reusable code blocks stored in your pipeline config. Define a script once, then reference it from multiple actions or sources by name. Each script has a runtime (the interpreter) and code (the script body).
Config section
Scripts are stored under the scripts key in your config (global or per-tab):
{
"scripts": {
"claude_json": {
"runtime": "bash",
"code": "#!/bin/bash\ncurl -s https://api.anthropic.com/v1/messages \\\n -H \"x-api-key: $ANTHROPIC_API_KEY\" \\\n -H \"content-type: application/json\" \\\n -d \"$1\""
},
"parse_csv": {
"runtime": "python3",
"code": "import csv, json, sys\nwith open(sys.argv[1]) as f:\n print(json.dumps(list(csv.DictReader(f))))"
}
}
}
Supported runtimes: bash, python3, node.
Referencing scripts from actions
Use "type": "script" in an action to run a named script per row. The script receives row data via template-expanded arguments and its stdout is captured as the result.
{
"id": "enrich_with_claude",
"type": "script",
"target": "ai_summary",
"script": "claude_json",
"args": ["{\"model\":\"claude-sonnet-4-20250514\",\"max_tokens\":256,\"messages\":[{\"role\":\"user\",\"content\":\"Summarize: {{row.company}}\"}]}"],
"extract": "$.content[0].text",
"timeout": 60000
}
Referencing scripts from sources
Use "type": "script" in a source to run a named script and create rows from its output.
{
"id": "load_leads",
"type": "script",
"script": "parse_csv",
"args": ["/tmp/leads.csv"],
"columns": { "Name": "$.name", "Email": "$.email" },
"dedup": "Email"
}
CLI commands
| Command | Description | |---------|-------------| | rowbound config add-script | Add a script to the config | | rowbound config remove-script | Remove a script by name | | rowbound config update-script | Update a script (merge partial JSON) |
Action Types
Templates use {{row.column}} for row data and {{env.KEY}} for environment variables. Actions support conditional execution with when expressions and structured error handling with onError.
http
Call a REST API and extract a value with JSONPath.
{
"id": "get_company",
"type": "http",
"target": "company_name",
"when": "row.domain !== ''",
"method": "GET",
"url": "https://api.clearbit.com/v2/companies/find?domain={{row.domain}}",
"headers": { "Authorization": "Bearer {{env.CLEARBIT_API_KEY}}" },
"extract": "$.name",
"ifEmpty": "❌",
"onError": { "404": "skip", "429": "skip", "default": { "write": "ERROR" } }
}
waterfall
Try multiple providers in order. First non-empty result wins.
{
"id": "find_email",
"type": "waterfall",
"target": "email",
"providers": [
{
"name": "hunter",
"method": "GET",
"url": "https://api.hunter.io/v2/email-finder?domain={{row.domain}}&first_name={{row.first_name}}&last_name={{row.last_name}}&api_key={{env.HUNTER_API_KEY}}",
"extract": "$.data.email"
},
{
"name": "apollo",
"method": "POST",
"url": "https://api.apollo.io/api/v1/people/match",
"headers": { "Content-Type": "application/json", "X-Api-Key": "{{env.APOLLO_API_KEY}}" },
"body": { "email": "{{row.personal_email}}", "domain": "{{row.domain}}" },
"extract": "$.person.email"
}
]
}
formula
Compute a value with a sandboxed JavaScript expression.
{
"id": "full_name",
"type": "formula",
"target": "full_name",
"expression": "`${row.first_name} ${row.last_name}`"
}
exec
Run a shell command and capture stdout. Template values are shell-escaped.
{
"id": "whois_lookup",
"type": "exec",
"target": "registrar",
"command": "whois {{row.domain}} | grep 'Registrar:' | head -1 | cut -d: -f2",
"timeout": 10000,
"onError": { "default": "skip" }
}
lookup
Pull data from another tab by matching a column value. Source tab data is cached per pipeline run for performance.
{
"id": "get_company_info",
"type": "lookup",
"target": "company_name",
"sourceTab": "Companies",
"matchColumn": "Domain",
"matchValue": "{{row.domain}}",
"matchOperator": "equals",
"returnColumn": "Name",
"matchMode": "first"
}
Use "matchMode": "all" to return all matches as a JSON array. Use "matchOperator": "contains" for substring matching.
write
Push data to another tab with column mapping. Supports append, upsert, and array expansion.
{
"id": "export_contacts",
"type": "write",
"target": "export_status",
"destTab": "Contacts",
"columns": {
"Company": "{{row.company}}",
"Name": "{{item.name}}",
"Title": "{{item.title}}",
"Email": "{{item.email}}"
},
"expand": "{{row.contacts_json}}",
"expandPath": "$.contacts"
}
- append (default) — always create new rows
- upsert — update existing rows if
upsertMatchcolumn matches, otherwise append - expand + expandPath — expand a JSON array into multiple rows; use
{{item.field}}in column templates to access element data
script
Run a named script and capture its output. Scripts are defined in the scripts config section and referenced by name.
{
"id": "ai_summary",
"type": "script",
"target": "summary",
"script": "claude_json",
"args": ["{\"prompt\":\"Summarize {{row.company}}\"}"],
"extract": "$.content[0].text",
"timeout": 60000
}
Error handling
Actions can define onError to map HTTP status codes (or exit codes for exec) to behaviors:
| Action | Effect | |--------|--------| | "skip" | Skip this action for the current row | | "stop_provider" | Stop the current waterfall provider, try the next | | {"write": "value"} | Write a fallback value to the target cell |
Google Sheets Sidebar
Rowbound includes an Apps Script sidebar that lets you configure actions directly in Google Sheets — no CLI needed. Click a column, edit the action config in a sidebar UI, and save. The sidebar reads and writes the same Developer Metadata config as the CLI, so both stay in sync.
Setup
- Open your Google Sheet → Extensions → Apps Script
- Replace the contents of
Code.gswith [apps-script/Code.gs](apps-script/Code.gs) - Click + next to Files → HTML → name it
Sidebar→ paste [apps-script/Sidebar.html](apps-script/Sidebar.html) - In the left panel, click + next to Services → select Google Sheets API → set Ident
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: eliasstravik
- Source: eliasstravik/rowbound
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.