tinyfish-cli is an agent-friendly command-line interface for the TinyFish browser automation API.
It is designed to work well for LLM agents like OpenClaw while still being comfortable for humans:
- compact JSON output by default
--prettyoutput for interactive use- stdin/file-based JSON inputs
- async, SSE, batch, fanout, and suite workflows
- stable input and output shapes for orchestration
The implementation is based on TinyFish's public docs and OpenAPI spec:
- Authentication
- Run browser automation with SSE streaming
- Start automation asynchronously
- Start multiple automations asynchronously
- Runs
- OpenAPI spec
authcommands for saving and inspecting your API keyrun,run-async,run-sse, andrun-batchruns list|get|get-many|wait|cancel|cancel-manybrowser create|usagefanoutfor bounded-concurrency orchestrationsuitefor built-in and custom integration testsagent run ...aliases for agent-oriented callers
- Python
3.9+ - a TinyFish API key
This project uses only the Python standard library at runtime.
No installation step is required:
./bin/tinyfish --helpYou can also invoke it explicitly with Python:
python3 bin/tinyfish --helppython3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -e .
tinyfish --helpOnce the package is published to PyPI, installation becomes:
python3 -m pip install tinyfish-cliThe CLI resolves the API key in this order:
--api-keyTINYFISH_API_KEY~/.tinyfish/config.json
Default config path:
~/.tinyfish/config.json
Store a key interactively:
./bin/tinyfish auth loginStore a key directly:
./bin/tinyfish auth set tf_your_key_hereStore a key from stdin:
printf '%s' "$TINYFISH_API_KEY" | ./bin/tinyfish auth setCheck auth status:
./bin/tinyfish auth status --prettyRemove the saved key:
./bin/tinyfish auth logout --prettyauth status exits with code 0 when authenticated and 1 when no key is available.
Set your key for the current shell:
export TINYFISH_API_KEY="tf_your_real_key"Verify the CLI sees it:
./bin/tinyfish auth status --prettyTry a read-only API call:
./bin/tinyfish runs list --limit 1 --prettyRun a simple automation:
./bin/tinyfish run \
--url https://example.com \
--goal "Return JSON only with {\"title\":\"...\"}" \
--prettyFor longer jobs, prefer run-async or run-sse instead of synchronous run.
By default, commands emit compact JSON on stdout. Add --pretty to indent output for human use.
Errors are emitted as JSON on stderr in this shape:
{
"error": {
"code": "SOME_CODE",
"message": "Human-readable error",
"status": 401,
"details": {
"any": "extra context"
}
}
}When TinyFish itself returns a structured error object, the CLI preserves that shape where possible.
Top-level commands:
./bin/tinyfish --helpauth
run
run-async
run-sse
run-batch
runs
browser
fanout
suite
Runs a synchronous TinyFish automation against POST /v1/automation/run.
Example:
./bin/tinyfish run \
--url https://example.com \
--goal "Return JSON only with {\"title\":\"...\",\"links\":[]}" \
--browser-profile lite \
--api-integration openclaw \
--prettySupported flag-driven request fields:
--url--goal--browser-profile lite|stealth--proxy-enabledor--no-proxy-enabled--proxy-country US|GB|CA|DE|FR|JP|AU--api-integration--enable-agent-memoryor--no-enable-agent-memory--use-vaultor--no-use-vault--credential-item-idrepeated
You can also provide a full JSON object with --input and optionally override fields with flags:
cat <<'JSON' | ./bin/tinyfish run --input - --pretty
{
"url": "https://example.com",
"goal": "Return JSON only with {\"title\":\"...\"}",
"browser_profile": "lite",
"proxy_config": {
"enabled": true,
"country_code": "US"
},
"api_integration": "openclaw",
"feature_flags": {
"enable_agent_memory": true
},
"use_vault": true,
"credential_item_ids": ["cred_123"]
}
JSONStarts an async TinyFish automation with POST /v1/automation/run-async.
Example:
./bin/tinyfish run-async \
--url https://example.com \
--goal "Return JSON only with {\"title\":\"...\"}" \
--prettyTypical follow-up:
./bin/tinyfish runs wait RUN_ID --prettyRuns a TinyFish automation via SSE streaming.
Example:
./bin/tinyfish run-sse \
--url https://example.com \
--goal "Return JSON only with {\"title\":\"...\"}" \
--prettyIn --pretty mode, the CLI prints readable progress lines such as:
[started][live][progress][complete]
Show heartbeat events too:
./bin/tinyfish run-sse \
--url https://example.com \
--goal "Return JSON only with {\"title\":\"...\"}" \
--show-heartbeats \
--prettyWithout --pretty, each SSE event is emitted as compact JSON.
Starts multiple async runs at once with POST /v1/automation/run-batch.
Input can be either:
- a JSON array of run request objects
- an object with a top-level
runsarray
Example array input:
cat <<'JSON' | ./bin/tinyfish run-batch --input - --pretty
[
{
"url": "https://example.com/a",
"goal": "Return JSON only with {\"site\":\"a\"}"
},
{
"url": "https://example.com/b",
"goal": "Return JSON only with {\"site\":\"b\"}"
}
]
JSONInspect and manage existing runs.
List runs:
./bin/tinyfish runs list --limit 10 --prettyFilter runs:
./bin/tinyfish runs list \
--status COMPLETED \
--goal "Return JSON only" \
--sort-direction desc \
--limit 5 \
--prettyGet one run:
./bin/tinyfish runs get RUN_ID --prettyGet many runs:
./bin/tinyfish runs get-many RUN_1 RUN_2 RUN_3 --prettyOr from JSON:
cat <<'JSON' | ./bin/tinyfish runs get-many --input - --pretty
{
"run_ids": ["RUN_1", "RUN_2"]
}
JSONWait for a run to finish:
./bin/tinyfish runs wait RUN_ID --interval 2 --wait-timeout 300 --prettyCancel one run:
./bin/tinyfish runs cancel RUN_ID --prettyCancel many runs:
./bin/tinyfish runs cancel-many RUN_1 RUN_2 --prettyruns wait exits non-zero if the final status is FAILED or CANCELLED.
Remote browser session utilities.
Create a browser session:
./bin/tinyfish browser create --prettyList browser usage:
./bin/tinyfish browser usage --limit 20 --prettyFilter browser usage:
./bin/tinyfish browser usage \
--session-id SESSION_ID \
--status running \
--start-after 2026-03-01T00:00:00Z \
--end-before 2026-03-31T23:59:59Z \
--limit 20 \
--page 1 \
--prettyfanout is a generic bounded-concurrency executor built for independent TinyFish tasks.
It is not pricing-specific, scraping-specific, or ticketing-specific. It lets an external agent decide what the tasks are while this CLI handles:
- async run creation
- client-side concurrency limits
- polling until terminal status
- aggregation into one JSON envelope
Print machine-readable schemas:
./bin/tinyfish fanout schema input
./bin/tinyfish fanout schema output
./bin/tinyfish fanout schema exampleValidate a plan:
./bin/tinyfish fanout validate --input ./examples/fanout-template.json --prettyRun a plan with up to 5 active tasks:
./bin/tinyfish fanout run \
--input ./examples/fanout-template.json \
--max-concurrency 5 \
--prettyRun selected tasks only:
./bin/tinyfish fanout run \
--input ./examples/fanout-template.json \
--task site-a \
--task site-b \
--max-concurrency 5 \
--prettyDetailed fanout docs live in docs/FANOUT.md.
Suites are live TinyFish smoke tests with assertions on the final run payload.
List built-in suites:
./bin/tinyfish suite list --prettyShow a suite definition:
./bin/tinyfish suite show common-web --prettyRun the built-in suite:
./bin/tinyfish suite run common-web --prettyRun a single scenario:
./bin/tinyfish suite run common-web --scenario cart-addition --prettyRun the built-in suite in fanout mode:
./bin/tinyfish suite run common-web \
--fanout \
--fanout-duplicates 2 \
--fanout-max-concurrency 5 \
--prettyThe built-in common-web suite covers:
- multi-page research on Books to Scrape
- cart interaction on Sauce Demo
- form fill and submit on Selenium Web Form
In fanout mode, each selected scenario is duplicated into task IDs like cart-addition--1 and cart-addition--2, executed concurrently through the generic fanout executor, and then validated with the original suite assertions.
For agent-oriented integrations, the CLI also accepts agent run ... aliases.
These map as follows:
tinyfish agent run ...->tinyfish run ...tinyfish agent run async ...->tinyfish run-async ...tinyfish agent run sse ...->tinyfish run-sse ...tinyfish agent run batch ...->tinyfish run-batch ...tinyfish agent run list ...->tinyfish runs list ...tinyfish agent run get ...->tinyfish runs get ...tinyfish agent run get-many ...->tinyfish runs get-many ...tinyfish agent run wait ...->tinyfish runs wait ...tinyfish agent run cancel ...->tinyfish runs cancel ...tinyfish agent run cancel-many ...->tinyfish runs cancel-many ...
Examples:
./bin/tinyfish agent run \
--url https://example.com \
--goal "Return JSON only with {\"title\":\"...\"}" \
--pretty./bin/tinyfish agent run async \
--url https://example.com \
--goal "Return JSON only with {\"title\":\"...\"}" \
--pretty./bin/tinyfish agent run list --limit 5 --prettySingle-run commands accept a JSON object with TinyFish request fields.
Minimum required fields:
{
"url": "https://example.com",
"goal": "Return JSON only with {\"title\":\"...\"}"
}Common optional fields:
{
"url": "https://example.com",
"goal": "Return JSON only with {\"title\":\"...\"}",
"browser_profile": "lite",
"proxy_config": {
"enabled": true,
"country_code": "US"
},
"api_integration": "openclaw",
"feature_flags": {
"enable_agent_memory": true
},
"use_vault": true,
"credential_item_ids": ["cred_123"]
}Accepted by run-batch:
{
"runs": [
{
"url": "https://example.com/a",
"goal": "Return JSON only with {\"site\":\"a\"}"
},
{
"url": "https://example.com/b",
"goal": "Return JSON only with {\"site\":\"b\"}"
}
]
}or:
[
{
"url": "https://example.com/a",
"goal": "Return JSON only with {\"site\":\"a\"}"
}
]Accepted by fanout validate and fanout run:
{
"name": "multi-site-checks",
"description": "Generic concurrent TinyFish task plan.",
"request_defaults": {
"browser_profile": "lite",
"api_integration": "openclaw"
},
"tasks": [
{
"id": "site-a",
"meta": {
"site": "site-a",
"kind": "lookup"
},
"request": {
"url": "https://example.com/a",
"goal": "Return JSON only with {\"title\":\"...\",\"price\":\"...\"}"
}
}
]
}Top-level fields:
name: optional stringdescription: optional stringrequest_defaults: optional object merged into each task requesttasks: required non-empty array
Task fields:
id: required stable identifiermeta: optional object carried through to outputrequest: required TinyFish async request object with at leasturlandgoal
Array shorthand is also accepted:
[
{
"id": "task-1",
"request": {
"url": "https://example.com/1",
"goal": "Return JSON only with {\"title\":\"...\"}"
}
}
]fanout run returns one aggregated JSON object:
{
"job": {
"name": "multi-site-checks",
"description": "Generic concurrent TinyFish task plan.",
"started_at": "2026-03-24T12:00:00+00:00",
"finished_at": "2026-03-24T12:00:21+00:00",
"max_concurrency": 5,
"interval_seconds": 2.0,
"wait_timeout_seconds": 300.0,
"fail_fast": false,
"requested_tasks": 2,
"executed_task_ids": ["site-a", "site-b"]
},
"summary": {
"total": 2,
"completed": 2,
"failed": 0,
"cancelled": 0,
"run_creation_failed": 0,
"wait_timeout": 0,
"polling_error": 0
},
"results": [
{
"id": "site-a",
"meta": {
"site": "site-a",
"kind": "lookup"
},
"request": {
"url": "https://example.com/a",
"goal": "Return JSON only with {\"title\":\"...\",\"price\":\"...\"}",
"browser_profile": "lite",
"api_integration": "openclaw"
},
"run_id": "run_abc123",
"outcome": "COMPLETED",
"run_status": "COMPLETED",
"result": {
"title": "Example Product",
"price": "$349.99"
},
"error": null,
"duration_seconds": 12.418,
"run_response": {
"...": "..."
}
}
]
}Result semantics:
outcomeis the executor-level outcomerun_statusis the TinyFish terminal status when a run existsresultis the TinyFishresult, with JSON strings parsed into objects or arrays when possibleerroris either a TinyFish error object or a CLI-generated orchestration error
Possible outcome values:
COMPLETEDFAILEDCANCELLEDRUN_CREATION_FAILEDWAIT_TIMEOUTPOLLING_ERROR
A custom suite is a JSON object with a scenarios array:
{
"name": "my-suite",
"description": "Custom TinyFish smoke tests",
"scenarios": [
{
"id": "form-check",
"description": "Fill a form and confirm success",
"request": {
"url": "https://example.com/form",
"goal": "Return JSON only with {\"submitted\":true,\"message\":\"...\"}"
},
"assertions": [
{
"type": "equals",
"path": "status",
"value": "COMPLETED"
},
{
"type": "truthy",
"path": "result.submitted"
},
{
"type": "contains",
"path": "result.message",
"value": "Success"
}
]
}
]
}Custom suite requirements:
- top-level object
- non-empty
scenariosarray - each scenario must have a unique string
id - each scenario must include a
requestobject withurlandgoal assertionsmust be an array if present
Supported assertion types:
equalstruthycontainscontains_allmin_itemstypeall_items_have_keys
Supported path syntax:
- dotted object paths like
result.message - list indexes like
result.items.0.title
Run a custom suite:
./bin/tinyfish suite run --file ./my-suite.json --prettyFor LLM agents, these patterns work best:
- ask TinyFish to return JSON only
- define the expected object shape explicitly in the
goal - put orchestration metadata in
meta, not inside the prompt text - use
api_integrationto identify the caller, for exampleopenclaw - use
run-asyncorfanout runfor long or concurrent jobs - omit
--prettywhen another program is parsing stdout
Recommended pattern for a single long-running job:
./bin/tinyfish run-async \
--url https://example.com \
--goal "Return JSON only with {\"title\":\"...\"}" \
--pretty
./bin/tinyfish runs wait RUN_ID --prettyRecommended pattern for many independent jobs:
./bin/tinyfish fanout run \
--input ./examples/fanout-template.json \
--max-concurrency 5 \
--prettyIf you need application-specific workflow logic such as approvals, deduplication, dependencies, or post-run reconciliation, keep that logic outside this CLI and treat tinyfish-cli as the execution layer.
runreturns the final TinyFish response when the HTTP request completes normally.run-asyncreturns the run creation response.run-sseexits non-zero if the stream ends beforeCOMPLETE, or if the final status isFAILEDorCANCELLED.runs waitexits non-zero if the final status isFAILEDorCANCELLED.fanout runexits non-zero if any task ends inFAILED,CANCELLED,RUN_CREATION_FAILED,WAIT_TIMEOUT, orPOLLING_ERROR.suite runexits non-zero if any scenario fails validation or cannot complete successfully.
The synchronous run command can encounter network disconnects on long jobs even when the run succeeded server-side. This CLI wraps that case as a REMOTE_DISCONNECTED error and includes guidance to prefer run-async or run-sse.
Run the test suite:
python3 -m unittest discover -s tests -vHelpful smoke checks:
python3 bin/tinyfish --help
python3 bin/tinyfish suite run --help
python3 bin/tinyfish fanout schema inputAs of March 24, 2026, the package name tinyfish-cli was available on PyPI.
Build distributions:
python3 -m venv .venv-publish
source .venv-publish/bin/activate
python3 -m pip install --upgrade pip build twine
python3 -m build
python3 -m twine check dist/*Upload to PyPI:
python3 -m twine upload dist/*Upload to TestPyPI first if you want a dry run:
python3 -m twine upload --repository testpypi dist/*You will need a PyPI account and either an API token or Trusted Publishing configured for the repository.
bin/tinyfish repo-local runner
src/tinyfish_cli/cli.py command parsing and handlers
src/tinyfish_cli/client.py HTTP and SSE client
src/tinyfish_cli/fanout.py bounded-concurrency executor
src/tinyfish_cli/suite_runner.py suite loading and execution
src/tinyfish_cli/builtin_suites.py built-in live smoke suites
docs/FANOUT.md detailed fanout documentation
examples/fanout-template.json starter fanout plan
LICENSE package license
MANIFEST.in source distribution include rules
tests/ unit tests
MIT