Fire-and-forget research tasks for the Gemini API. Submit a long-running prompt, get a task ID back in under a second, close your laptop, collect the answer later.
On July 7, 2026 Google added background execution to the Gemini Interactions API:
Holding an HTTP connection open for long-running tasks is fragile. Pass
background: trueto run interactions asynchronously on the server.
That solves the server half. The client half is still on you: you get back an interaction ID and now you own it — polling it, remembering which prompt it belonged to, not losing it when your shell dies, noticing that the free tier throws it away after 24 hours. gemcatch is that half. It passes background: true, stores the interaction ID in local SQLite next to the prompt that created it, gives you a handful of commands to get results back, and runs a daemon that collects them before the free tier drops them.
$ gemcatch research "compare the 2026 EU AI Act timelines against the UK approach"
Task 8f3a1c04 submitted. Run: gemcatch get 8f3a1c04 when ready.
$ # ...close your laptop, come back later...
$ gemcatch get 8f3a1c04
The EU AI Act's high-risk obligations phase in from August 2026, whereas...Needs Node.js 22+ and a Gemini API key. Getting a key needs no billing account and no card. gemini-3.1-flash-lite runs free within the free tier's daily quota; past that, paid rates apply.
- Get a key at https://aistudio.google.com/apikey
- Put it in your environment:
# PowerShell
$env:GEMINI_API_KEY = "your-key-here"# bash / zsh
export GEMINI_API_KEY=your-key-hereTo make it permanent, add that line to your shell profile ($PROFILE on PowerShell, ~/.bashrc or ~/.zshrc on Unix). GOOGLE_API_KEY works too.
npx gemcatch research "your question"Or install it once and use the short name:
npm install -g gemcatch
gemcatch research "your question"From a clone: npm install && node index.js research "your question".
# 1. Submit — returns immediately with a task ID
$ gemcatch research "summarize this week in AI"
Task 8f3a1c04 submitted. Run: gemcatch get 8f3a1c04 when ready.
# 2. Check on it — or `gemcatch list` to see everything
$ gemcatch status 8f3a1c04
Task 8f3a1c04: in_progress
# 3. Collect the answer (blocks and polls until done)
$ gemcatch watch 8f3a1c04
[10:52:31] 8f3a1c04: in_progress
[10:54:02] 8f3a1c04: completed
This week in AI: ...| Command | What it does |
|---|---|
gemcatch research "<prompt>" |
Submits with background: true, stores the interaction ID, exits immediately. |
gemcatch batch <file> |
Submits many prompts from a file at once, tagged as one collectable batch. |
gemcatch status <id> |
Polls the API and prints the current state. |
gemcatch get <id> |
Prints the full response if complete, otherwise the current status. |
gemcatch list |
All tasks, newest first: id, age, status, prompt. |
gemcatch export |
Concatenates finished results, each under its prompt, to stdout or a file (Markdown or JSON). |
gemcatch digest |
Feeds a tag's completed results through one Gemini call into a single summary. |
gemcatch watch <id> |
Polls until the task finishes, then prints the result. |
gemcatch sync |
Refreshes every in-flight task in one pass. |
gemcatch daemon |
Keeps polling in-flight tasks on a loop, so results are cached before they expire. |
gemcatch cancel <id> |
Asks the API to stop an in-flight task. |
gemcatch rm <ids...> |
Forgets tasks locally. --remote deletes them server-side too. |
gemcatch prune |
Drops finished tasks older than --days (default 30). |
gemcatch stats |
Where the store lives and what's in it. |
Useful flags:
| Flag | On | Does |
|---|---|---|
--json |
most commands | Machine-readable output. |
-m, --model <id> |
research, batch |
Override the model. |
-s, --system <text> |
research, batch |
Set a system instruction. |
-f, --file <path> |
research |
Read the prompt from a file. |
-t, --tag <tag> |
research, batch, list |
Label tasks and filter them. |
-w, --watch |
research, batch |
Submit and wait, in one command. |
--separator <str> |
batch |
Split the file on this delimiter line for multi-line prompts. |
-i, --interval <s> |
watch, daemon |
Poll rate in seconds; must be > 0. Default 10s for watch, 300s for daemon. |
--exit-when-idle |
daemon |
Stop once nothing is left in flight. |
--status <s> |
list, export |
Only tasks in this status. |
-n, --limit <n> |
list |
Cap the rows (non-negative; 0 shows none). |
--format <md|json> |
export |
Output format. Default md. |
-o, --out <file> |
export |
Write to a file instead of stdout. |
--dry-run |
batch, prune |
Show what would go; submit/delete nothing. |
--raw |
get |
Dump the raw interaction JSON. |
IDs are the first 8 characters of a UUID. Any unique prefix works, so gemcatch get 8f3a is fine.
Statuses come straight from the API: in_progress, requires_action, completed, failed, cancelled, incomplete, budget_exceeded. Plus pending, which is local: the row exists but the submit call hasn't returned yet.
# Fire off a whole file of prompts in one command, then collect later.
# Every task shares one auto-generated tag (batch-xxxxxx), printed on submit.
$ gemcatch batch questions.txt # one prompt per line; "# " and blanks skipped
$ gemcatch daemon --exit-when-idle # keep polling until they're all in
$ gemcatch list --tag batch-1a2b3c --status completed
# Collect a whole batch into one document (the "gather" for batch's "scatter").
$ gemcatch export --tag batch-1a2b3c -o results.md # Markdown, one section per prompt
$ gemcatch export --tag batch-1a2b3c --format json | jq -r '.[].result'
$ gemcatch digest --tag batch-1a2b3c # or synthesize them into one summary
# Multi-line prompts: split the file on a delimiter line instead of per-line
$ gemcatch batch briefs.md --separator ---
$ gemcatch batch - < questions.txt # or pipe the list in on stdin
# The same thing by hand, if you prefer a loop
$ for q in "topic A" "topic B" "topic C"; do gemcatch research "$q" -t batch1; done
$ gemcatch sync # one pass now...
$ gemcatch daemon --exit-when-idle # ...or keep polling until they're all in
$ gemcatch list --tag batch1 --status completed
# Long prompt from a file, result to a file.
# Progress goes to stderr, so the redirect captures only the answer.
$ gemcatch research -f brief.md -w > answer.md
# Pipe a prompt in
$ cat notes.txt | gemcatch research - -s "extract every open question"
# Script against it
$ id=$(gemcatch research "..." --json | jq -r .id)
$ gemcatch watch "$id" --json | jq -r .resultTasks live in SQLite at ~/.gemcatch/tasks.db (override with GEMCATCH_HOME):
CREATE TABLE tasks (id TEXT PRIMARY KEY, prompt TEXT, interaction_id TEXT,
status TEXT DEFAULT 'pending', result TEXT, created_at INTEGER);
-- plus model, system_instruction, tag, error, usage, updated_atresearch calls interactions.create({model, input, background: true}) via @google/genai and keeps the returned id. The polling commands call interactions.get(id) and write the status back. Once a task completes, the text is cached in the result column — gemcatch get then answers from disk without touching the network.
An older tasks.db upgrades in place; migrations are additive and never drop a row.
Free-tier results expire after 24 hours. The docs note the system retains interactions for 1 day on the free tier (55 days paid). Once gemcatch has seen a task complete, the text is cached locally and survives that expiry — but a task nobody polls inside that window is gone server-side. That is what gemcatch daemon is for.
Caching a result permanently is easy; noticing it is the hard part. If nothing polls a finished task within 24 hours, the answer is dropped server-side and no amount of local bookkeeping brings it back. gemcatch daemon is the something that looks:
$ gemcatch daemon
gemcatch daemon: polling every 300s. Store: ~/.gemcatch/tasks.db. Ctrl-C to stop.
[10:54:02] 8f3a1c04: completed
[11:31:20] c7b91e55: completedIt refreshes everything in flight on an interval, writes each result to SQLite the moment it lands, and stays quiet otherwise — only transitions and errors get a line. It's an ordinary foreground process: no forking, no pidfile. Leave it in a terminal, or hand it to whatever supervises long-running jobs on your machine (a systemd user unit, a launchd agent, Task Scheduler at log-on, nohup, pm2).
Nothing is lost if it dies. Every poll is committed to SQLite synchronously, so killing the daemon — Ctrl-C, kill -9, a reboot — stops the polling and nothing else. Results it already collected are on disk, and restarting picks up where it left off.
--exit-when-idle stops once nothing is in flight, which makes it the "collect this batch, then quit" one-liner:
$ for q in "topic A" "topic B" "topic C"; do gemcatch research "$q" -t batch1; done
$ gemcatch daemon --exit-when-idle -i 30
$ gemcatch list --tag batch1 --status completedIf a task's interaction has vanished server-side — the free tier dropped it after 24h, or it was deleted — polling it returns a 404. Rather than chase a task that can never resolve, gemcatch retires it locally to incomplete, so it leaves the in-flight set and --exit-when-idle still converges. watch and batch -w also give up after a bounded run of consecutive poll failures (GEMCATCH_WATCH_MAX_FAILS, default 10) instead of looping forever.
The free tier allows roughly 15 requests a minute, which a wide gemcatch sync or a busy daemon would otherwise blow straight through. Every outbound call is paced to GEMCATCH_RPM (default 15) — set it higher on a paid key, or 0 to disable pacing entirely.
Pacing is per process: each gemcatch invocation keeps its own counter, so two running at once (a daemon in one terminal and a one-off sync in another) can together exceed the ceiling. If you need the limit to hold, run a single daemon and let it do the polling.
Transient failures are retried with exponential backoff and full jitter, honouring Retry-After when the server sends it. A rate limit, a timeout or a 5xx gets GEMCATCH_MAX_RETRIES more attempts (default 4); a 4xx does not, because a bad key or a bad model id fails identically forever and retrying it only burns your quota.
| Variable | Purpose |
|---|---|
GEMINI_API_KEY |
Your API key. GOOGLE_API_KEY also works. |
GEMCATCH_HOME |
Where tasks.db lives. Default ~/.gemcatch. |
GEMCATCH_MODEL |
Default model. Default gemini-3.1-flash-lite. |
GEMCATCH_POLL_MS |
watch poll interval in ms. Default 10000. |
GEMCATCH_DAEMON_S |
daemon interval in seconds. Default 300. |
GEMCATCH_RPM |
Requests/minute ceiling. Default 15 (the free tier). 0 disables pacing. |
GEMCATCH_MAX_RETRIES |
Extra attempts on a transient failure. Default 4. 0 disables retries. |
GEMCATCH_WATCH_MAX_FAILS |
Consecutive poll failures before watch/batch -w give up. Default 10. |
GEMCATCH_BASE_URL |
Override the API endpoint (proxy/gateway/testing). |
GEMCATCH_FORCE_REST |
1 bypasses the SDK and uses raw fetch. |
NO_COLOR |
Disable colour output. |
git clone https://github.com/Booyaka101/gemcatch
cd gemcatch
npm install
npm testThe tests need no API key and no network. They run the real CLI as a subprocess against a mock Interactions API on localhost, covering every command, the failure paths, and schema migration. See CONTRIBUTING.md for the layout and for the API gotchas worth knowing before touching gemini.js.
Issues and PRs welcome — see CONTRIBUTING.md and the Code of Conduct. Security reports go through private disclosure, not public issues.