Skip to main content
Capability: Watching, cancelling, and splitting runs works with any API key. max_duration_seconds is generally available. max_steps is in beta: enroll at agent.tinyfish.ai/beta first, or a request that sets it returns 403 FORBIDDEN.
Use this when a run has been RUNNING longer than you expected, or when a big goal keeps running out of steps or time. A run usually takes this long because the goal is too big for one browser session, not because something broke. The fix is to watch it, stop it if you need to, and send the work again as smaller runs.

Check whether it’s stuck or just slow

Start the run with queue so you get a run_id straight away, then poll it with runs.get. While the run is going, status is RUNNING, num_of_steps is null, and streaming_url is a live view of the browser you can open. Python
TypeScript
This is a run 20 seconds in, from GET /v1/runs/{id}:
num_of_steps is on the TypeScript SDK’s Run type (run.num_of_steps), but it isn’t exposed on the Python SDK’s Run model yet — read it from the raw REST response (as above) if you need it in Python.
Polling tells you the run is alive, not what it’s doing. To see that, either open streaming_url or use stream instead of queue. Every PROGRESS event has a purpose that says what the agent is doing, so a run that’s paging through results looks different from one that keeps retrying the same click. Python
TypeScript
How to read what you see: After a run ends, streaming_url no longer shows the browser. To see what happened, read the run’s step timeline. Each entry in steps has a timestamp, a status, and an action describing what the agent did, and video_url is a recording of the whole run. Neither SDK returns steps or video_url yet, so read them from the REST endpoint. video_url is a signed link that expires after 15 minutes, so fetch it when you need it rather than storing it. Python
TypeScript

Set limits that fit the task

Two fields in agent_config bound a run. Set both on anything that pages through results. Neither limit makes the run fail. The run ends COMPLETED with whatever the agent wrote into result, and error is null. To tell a finished run from one that was cut short, add a status field to your output schema, as shown in Troubleshoot Runs. To pick values, run the goal once on a small piece (one page, one item), look at num_of_steps, and multiply by the number of pieces. Leave headroom: one shop page took 2 steps, but a 20-page goal had used 14 steps by page 5. Set max_duration_seconds to the longest you’re willing to wait, and allow a few extra seconds for the wrap-up.

Cancel the run

Cancel a run with POST /v1/runs/{id}/cancel. The Python and TypeScript SDKs don’t have a cancel method yet, so call the endpoint directly with your API key. This works on runs started with queue, stream, or /v1/automation/run-batch. A run started with run (/v1/automation/run) can’t be cancelled, so use queue for anything that might run long. Python
TypeScript
Cancelling is safe to repeat. Calling it again on the same run returns the current status and says why nothing changed:
A run that already finished returns COMPLETED or FAILED with message: "Run already finished". A 404 means the run ID doesn’t exist, and a 403 means your key can see the run but isn’t allowed to cancel it. To stop several runs at once, send up to 100 IDs to POST /v1/runs/batch/cancel:
The response has a results entry for each run it found and a not_found list for IDs it didn’t.

Recover what the run found

Cancelling throws away the run’s work. After the cancel above, the run had been going for almost a minute, and this is what GET /v1/runs/{id} returned:
result is null. If you want partial data back, let a limit stop the run instead of cancelling it. When max_steps or max_duration_seconds runs out, the agent writes up what it has before it ends. Ask for a field that says where it got to, so you know where to pick up:
That’s a 20-page goal with max_duration_seconds: 60. It reports how far it got (last_page: 5), but products came back empty even though the reason says it extracted 80. A long run’s wrap-up is a summary, and it can drop the data itself. Use it to find out where to resume, not as the data. The reliable way to keep what you’ve collected is to make each run small enough to finish, which is the next section.

Split the task into smaller runs

Instead of one run that pages through everything, start one run per page (or per item) and run them at the same time. Each run is short, finishes on its own, and returns its own result, so a problem on one page doesn’t cost you the rest. Python
TypeScript
All three runs were queued together, and each one finished in 25 to 37 seconds using 2 steps. This is page 1:
Together, the three pages took about 45 seconds from the first queue call to the last result, and 6 steps in total. The single 20-page run used 14 steps to cover 5 pages and returned no products. The same split works for any goal with independent parts: one run per URL, per search term, or per record. If the parts depend on each other (you need page 1 to find the link for page 2), keep them in one run and give it enough max_steps. To send many runs in one request, use POST /v1/automation/run-batch with 1 to 100 runs. The SDKs don’t have a batch method, so call it with cURL or the CLI (tinyfish agent batch run --input runs.csv). It returns all the run IDs at once, and you can poll them together with POST /v1/runs/batch:
In a batch, output_schema goes at the top level of the request and applies to every run. See Multiple URLs for more on running work in parallel.

Retry the parts that didn’t finish

Once the work is split, retry only the pieces that need it, and decide based on how the run ended: Check error.category before treating CANCELLED as “stop retrying” — a billing-rejected run also comes back CANCELLED, not FAILED, and that one still needs credits added. A run you (or someone else) cancelled on purpose is also CANCELLED, with error.category: "UNKNOWN", so a retry loop that reads only status will restart runs you meant to stop. Python
TypeScript
Before you retry, make sure the first run really ended. Requests to run-batch aren’t idempotent: if a batch request times out on your side and you send it again, you can end up with two copies of every run. Call runs.list or POST /v1/runs/batch first to see whether the runs exist.

Common Mistakes

A cancelled run returns result: null. If you want partial data, let max_steps or max_duration_seconds stop it, or split the work so each finished piece is already saved.
run waits for the result and can’t be cancelled. Use queue or stream for anything that might take more than a minute or two.
When a long run hits a limit, the wrap-up says how far it got, but the data fields can be empty. Use it to find where to resume, then get the rest with smaller runs.
The agent finishes its current step and writes up its result after the limit, so the run ends a bit later. Set the limit a little below the longest you’re willing to wait.
A run that hits the cap uses one extra step to wrap up, and the same goal can take more steps on a slower page. Leave headroom.
Cancelled runs report category: "UNKNOWN", except a run the billing check rejected, which reports BILLING_FAILURE. Check error.category before deciding whether to retry.
run-batch isn’t idempotent. Check whether the runs were created before you send it again.

Troubleshoot Runs

Fix each finish reason

Runs

Run lifecycle, statuses, and finish reasons

Runs Reference

List, get, cancel, batch, and per-step screenshots and HTML

Agent Mode, Max Steps, and Max Duration

Default vs Strict mode, step and time limits

Run TinyAgent Across Multiple URLs

Fire the same goal at many URLs at once

Error Codes

Every code and how to handle it