> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tinyfish.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshoot Agent Setup and Failed Runs

> Find out why a request was rejected or a run came back without your data, and apply the fix for login walls, bot checks, step and time limits, and setup errors

> [!NOTE]
>
> **Capability:** Works with any API key. Fixing login walls needs a Browser Context Profile or Vault credentials, and the stealth fix uses `browser_profile` and `proxy_config`. `max_steps` is in beta: enroll at [agent.tinyfish.ai/beta](https://agent.tinyfish.ai/beta) first, or a request that sets it returns `403 FORBIDDEN`.

Use this when a request is rejected before it starts, or a run finishes without the data you asked for. A problem shows up in one of three places, and each needs a different fix:

| Where it shows up | What you see | Go to |
| - | - | - |
| The request is rejected | The SDK raises an error with an HTTP status (401, 400, 402, 403, 429) | [Setup failures](#setup-failures) |
| The run ends `FAILED` or `CANCELLED` | `run.error` has a `category` (a `code` too, if you called sync `/run`) | [Runs that fail](#runs-that-fail) |
| The run ends `COMPLETED` but the data is missing | `run.result` is empty, partial, or explains what went wrong | [Auth required](#auth-required), [Blocked](#blocked), [Budget exhausted](#budget-exhausted) |

The third case is the most common. `COMPLETED` means the run finished, not that it did what you asked. When the agent hits a login page, a bot check, or its step limit, it usually stops, writes what it found and why it stopped into `result`, and ends with `status: COMPLETED` and `error: null`.

## Make every run report how it ended

Add a `status` and a `reason` to your output schema, and tell the agent in the goal what each status means. Your code can then branch on a field instead of reading prose.

**Python**

```py theme={null}
from tinyfish import TinyFish, AgentConfig, RunStatus

client = TinyFish()

OUTCOME_SCHEMA = {
    "type": "object",
    "properties": {
        "status": {
            "type": "string",
            "enum": ["success", "auth_required", "blocked", "incomplete"],
        },
        "reason": {"type": "string"},
        "products": {"type": "array", "items": {"type": "string"}},
    },
    "required": ["status", "reason"],
}

run = client.agent.run(
    url="https://scrapeme.live/shop/",
    goal=(
        "List the name of every product on shop pages 1 to 5. "
        "Report how the run ended in status: success if you finished, "
        "auth_required if the site asks you to sign in, "
        "blocked if a CAPTCHA or bot check stops you, "
        "incomplete if you run out of steps or time before finishing. "
        "Put a one-sentence explanation in reason."
    ),
    output_schema=OUTCOME_SCHEMA,
    agent_config=AgentConfig(max_steps=30),
)

if run.status != RunStatus.COMPLETED:
    # FAILED or CANCELLED: the run itself didn't finish
    print(run.status, run.error.code if run.error else None)
else:
    print(run.result["status"], "-", run.result["reason"])
```

**TypeScript**

```ts theme={null}
import { TinyFish, RunStatus } from "@tiny-fish/sdk";

const client = new TinyFish();

const OUTCOME_SCHEMA = {
  type: "object",
  properties: {
    status: {
      type: "string",
      enum: ["success", "auth_required", "blocked", "incomplete"],
    },
    reason: { type: "string" },
    products: { type: "array", items: { type: "string" } },
  },
  required: ["status", "reason"],
};

const run = await client.agent.run({
  url: "https://scrapeme.live/shop/",
  goal:
    "List the name of every product on shop pages 1 to 5. " +
    "Report how the run ended in status: success if you finished, " +
    "auth_required if the site asks you to sign in, " +
    "blocked if a CAPTCHA or bot check stops you, " +
    "incomplete if you run out of steps or time before finishing. " +
    "Put a one-sentence explanation in reason.",
  output_schema: OUTCOME_SCHEMA,
  agent_config: { max_steps: 30 },
});

if (run.status !== RunStatus.COMPLETED) {
  // FAILED or CANCELLED: the run itself didn't finish
  console.log(run.status, run.error?.code);
} else {
  const outcome = run.result as { status: string; reason: string };
  console.log(outcome.status, "-", outcome.reason);
}
```

With a 3-step limit, this run couldn't reach page 5, and it said so:

```json theme={null}
{
  "status": "COMPLETED",
  "num_of_steps": 4,
  "error": null,
  "result": {
    "status": "incomplete",
    "reason": "Ran out of steps before reaching page 5; only completed pages 1 and 2 of the required five pages.",
    "products": [
      "Page 1: Bulbasaur, Ivysaur, Venusaur, Charmander, ...",
      "Page 2: Pidgeotto, Pidgeot, Rattata, Raticate, ..."
    ]
  }
}
```

Keep only `status` and `reason` in `required`. If a data field is required and the agent can't reach the data, it fills the field with a placeholder rather than leaving it out. See [Common mistakes](#common-mistakes).

## Auth required

The page asks the agent to sign in, and the run has no way to log in. On a page behind a login with no schema, the run completes with an empty value:

```json theme={null}
{
  "status": "COMPLETED",
  "num_of_steps": 3,
  "error": null,
  "result": { "name": null }
}
```

With the outcome schema above, the same page returns the reason directly:

```json theme={null}
{
  "status": "auth_required",
  "reason": "The page requires sign in to GitHub. No credentials are configured for this account, so login cannot succeed. The task cannot be completed without authentication."
}
```

**Fix:** give the run a way in. Use a Browser Context Profile, which reuses a browser session you already signed in to:

**Python**

```py theme={null}
from tinyfish import TinyFish

client = TinyFish()

run = client.agent.run(
    url="https://github.com/settings/profile",
    goal="Extract the display name shown in the Name field of the public profile settings form.",
    use_profile=True,
)

print(run.status, run.result)
```

**TypeScript**

```ts theme={null}
import { TinyFish } from "@tiny-fish/sdk";

const client = new TinyFish();

const run = await client.agent.run({
  url: "https://github.com/settings/profile",
  goal: "Extract the display name shown in the Name field of the public profile settings form.",
  use_profile: true,
});

console.log(run.status, run.result);
```

Or store the login in Vault and pass `use_vault: true`, with `credential_item_ids` to limit the run to specific items. Never put a password in the goal. See [Browser Context Profiles](https://docs.tinyfish.ai/key-concepts/browser-context-profiles.md) and [Vault setup](https://docs.tinyfish.ai/vault-setup.md).

If the run ends `FAILED` with `TASK_FAILED` and a message about authentication, the fix is the same. A failed run may also include `profile_hint` with `reason: "auth_wall"` and a `setup_url` that opens profile setup for that domain.

## Blocked

A CAPTCHA, a "verify you are human" page, or a bot-protection service stops the agent. With the default `lite` browser on a page behind DataDome, the run didn't fail.

The run ended `COMPLETED` after 5 steps with `error: null`. Its result, trimmed:

```txt theme={null}
Unable to extract the overall star rating. The G2.com page for Tinyfish reviews is blocked by a security challenge (DataDome)... shows a human verification form...
```

A run can also end `FAILED` with `SITE_BLOCKED`, or include `profile_hint` with `reason: "bot_challenge"`.

**Fix:** switch to the stealth browser and route through a proxy. Retrying with the same settings usually hits the same wall.

**Python**

```py theme={null}
from tinyfish import TinyFish, AgentConfig, BrowserProfile, ProxyConfig

client = TinyFish()

run = client.agent.run(
    url="https://www.g2.com/products/tinyfish/reviews",
    goal="Extract the overall star rating shown at the top of the page. Return JSON with key rating.",
    browser_profile=BrowserProfile.STEALTH,
    proxy_config=ProxyConfig(enabled=True, country_code="US"),
    agent_config=AgentConfig(max_steps=6),
)

print(run.status, run.result)
```

**TypeScript**

```ts theme={null}
import { TinyFish } from "@tiny-fish/sdk";

const client = new TinyFish();

const run = await client.agent.run({
  url: "https://www.g2.com/products/tinyfish/reviews",
  goal: "Extract the overall star rating shown at the top of the page. Return JSON with key rating.",
  browser_profile: "stealth",
  proxy_config: { enabled: true, country_code: "US" },
  agent_config: { max_steps: 6 },
});

console.log(run.status, run.result);
```

The same page and goal with these settings:

```json theme={null}
{
  "status": "COMPLETED",
  "num_of_steps": 6,
  "error": null,
  "result": { "rating": "4.3" }
}
```

If stealth and a proxy aren't enough, a Browser Context Profile carries real cookies and history, which some sites check for. See the [Anti-Bot Guide](https://docs.tinyfish.ai/anti-bot-guide.md).

## Budget exhausted

The run hit its step limit (`agent_config.max_steps`) or time limit (`agent_config.max_duration_seconds`) before finishing. The agent wraps up and returns what it collected so far. Here's a five-page goal with `max_steps: 2`.

The run ended `COMPLETED` after 3 steps with `error: null`. Its result listed the page 1 products, then:

```txt theme={null}
Note: Budget was exhausted after collecting only Page 1 data. Pages 2-5 were not completed due to step/time limits...
```

With `max_duration_seconds: 10` on the same goal, the run returned pages 1 and 2 with `"Note: Pages 3, 4, and 5 were not completed due to budget exhaustion."`

A run that stops at the step limit without producing any result ends `FAILED` with `MAX_STEPS_EXCEEDED`.

**Fix:** pick one.

* Raise `max_steps` or `max_duration_seconds` if the task needs more room.
* Split the goal into smaller runs, for example one run per page, and run them in parallel. See [Recover a Stuck or Long-Running Run](https://docs.tinyfish.ai/agent/how-to/recover-stuck-runs.md#split-the-task-into-smaller-runs).
* Remove steps the agent doesn't need: start `url` on the page with the data, and say exactly which fields you want. See the [Prompting Guide](https://docs.tinyfish.ai/prompting-guide.md).

**Out of credits** is a different budget. If your wallet can't cover a run, the request is rejected with HTTP 402 `INSUFFICIENT_CREDITS`, or the run ends `CANCELLED` with `BILLING_REJECTED` and no steps. Add money in your [wallet](https://docs.tinyfish.ai/wallet.md) and run it again.

## Runs that fail

When `status` is `FAILED` or `CANCELLED`, `result` is null and `error` says why. Start with `error.category`:

| `category` | Meaning | What to do |
| - | - | - |
| `SYSTEM_FAILURE` | A problem on the TinyFish side, such as `SERVICE_BUSY` or `TIMEOUT` | Retry. Wait `retry_after` seconds if it's set |
| `AGENT_FAILURE` | The run couldn't do the task, such as `SITE_BLOCKED`, `MAX_STEPS_EXCEEDED`, or `TASK_FAILED` | Change the input. Retrying the same request usually fails the same way |
| `BILLING_FAILURE` | Not enough credits (`BILLING_REJECTED`) | Add money to your wallet |
| `UNKNOWN` | Not classified | Treat as retryable |

`error.help_url` and `error.help_message`, when present, point to the page that covers the fix. The full list of codes is in [Error Codes](https://docs.tinyfish.ai/error-codes.md).

A run you started with `queue` or `stream` that is still `RUNNING` long after you expected can be stopped with `POST /v1/runs/{id}/cancel`. It ends `CANCELLED`. See [Runs](https://docs.tinyfish.ai/agent/api/runs.md).

## Setup failures

These errors come back before a run starts, so nothing is charged.

**Python**

```py theme={null}
from tinyfish import (
    TinyFish,
    AuthenticationError,
    BadRequestError,
    PaymentRequiredError,
)

client = TinyFish()

try:
    run = client.agent.run(
        url="https://scrapeme.live/shop/",
        goal="Extract the name of the first product.",
    )
    print(run.status, run.result)
except AuthenticationError as e:
    # 401: MISSING_API_KEY, INVALID_API_KEY, or VAULT_RECONNECT_REQUIRED
    print(e.status_code, e.code, e.message)
except BadRequestError as e:
    # 400: INVALID_INPUT, e.g. a bad url or an unsupported output_schema field
    print(e.status_code, e.code, e.message)
except PaymentRequiredError as e:
    # 402: INSUFFICIENT_CREDITS, add money at agent.tinyfish.ai/wallet
    print(e.status_code, e.code, e.message)
```

**TypeScript**

```ts theme={null}
import {
  TinyFish,
  AuthenticationError,
  BadRequestError,
  PaymentRequiredError,
} from "@tiny-fish/sdk";

const client = new TinyFish();

try {
  const run = await client.agent.run({
    url: "https://scrapeme.live/shop/",
    goal: "Extract the name of the first product.",
  });
  console.log(run.status, run.result);
} catch (e) {
  if (
    e instanceof AuthenticationError || // 401: MISSING_API_KEY, INVALID_API_KEY, VAULT_RECONNECT_REQUIRED
    e instanceof BadRequestError || // 400: INVALID_INPUT
    e instanceof PaymentRequiredError // 402: INSUFFICIENT_CREDITS
  ) {
    console.log(e.statusCode, e.code, e.message);
  } else {
    throw e;
  }
}
```

**No API key.** If `TINYFISH_API_KEY` isn't set and you don't pass a key, the client fails before sending anything:

```txt theme={null}
Python:     ValueError: No API key provided. Pass api_key= to the client or set the TINYFISH_API_KEY environment variable.
TypeScript: SDKError: Missing API key. Pass `apiKey` to the constructor or set the TINYFISH_API_KEY environment variable.
```

**401, missing or wrong key.** A request with no `X-API-Key` header:

```json theme={null}
{
  "error": {
    "code": "MISSING_API_KEY",
    "message": "X-API-Key header is required"
  },
  "request_id": "6096a915-..."
}
```

A key that's wrong, revoked, or expired comes back from both SDKs as `AuthenticationError`, status `401`, code `INVALID_API_KEY`, message `Invalid or expired API key`. Copy a fresh key from your [API keys page](https://agent.tinyfish.ai/api-keys) and check for spaces or line breaks. The key is checked before the request body, so fix a 401 first: a request with a bad key and a bad body only shows the 401.

A 401 with `VAULT_RECONNECT_REQUIRED` means a password manager connected to Vault needs to be reconnected. `details.providers` lists which ones.

**400, invalid input.** `INVALID_INPUT` means the request body didn't pass validation. `details` lists each problem with its `path`. Common causes: a `url` without `https://`, an empty `goal`, a `browser_profile` other than `lite` or `stealth`, or a lowercase `country_code`.

**Unsupported output schema.** Output schemas support a subset of JSON Schema. A keyword outside it is rejected before the run starts, with the keyword and where it is:

```txt theme={null}
output_schema field "patternProperties" is not supported at #.
```

Remove the keyword or restructure the schema. See [Structured output](https://docs.tinyfish.ai/agent/how-to/structured-output.md).

**402, 403, 429.**

* `402 INSUFFICIENT_CREDITS`: your wallet is empty. `details.wallet_url` links to it.
* `403 FORBIDDEN`: your key or plan can't use that feature. Check the message for which one.
* `429 RATE_LIMIT_EXCEEDED`: too many requests or too many pending runs. Wait and retry, honoring `Retry-After` when it's sent.

Every error body has a `request_id`, which matches the `X-Request-ID` response header. Include it when you contact support.

## Common Mistakes

> [accordion] Treating COMPLETED as success
>
> Your code checks `status == "COMPLETED"` and stores whatever is in `result`, including a note that the page needed a login. Check the data too. Add a `status` and `reason` to your output schema, as in [Make every run report how it ended](#make-every-run-report-how-it-ended), and only store results where `status` is `success`.

> [accordion] Required fields come back with made-up values
>
> A schema that requires `name` sent to a page behind a login returned `"name": "GitHub Profile Name"`, a placeholder, because the field had to be filled. Require only `status` and `reason`. Leave data fields optional so the agent can leave them out when it can't find them.

> [accordion] A typo in agent\_config is ignored in Python
>
> In Python, `AgentConfig(max_step=5)` is accepted and the misspelled field is dropped, so the run uses the default limit. Passing a plain dict to `agent_config` raises `AttributeError: 'dict' object has no attribute 'model_dump'`. Use `AgentConfig(max_steps=...)` with the exact field names. The TypeScript SDK rejects unknown keys before sending, with `Unrecognized key: "max_step"`.

> [accordion] Setting max\_steps right at the number you expect
>
> A run that stops at its step limit takes one more step to write its result, so `max_steps: 2` shows `num_of_steps: 3`. A time limit isn't a hard cut either: a run with `max_duration_seconds: 10` ended about 20 seconds after it started, once the agent wrapped up. Leave headroom in both limits, and budget for one extra step on runs that hit the cap.

> [accordion] Retrying a blocked run with the same settings
>
> A run blocked by a bot check is retried with the default browser and fails the same way. Retry with `browser_profile: "stealth"` and `proxy_config: { enabled: true }`. If it's still blocked, use a Browser Context Profile.

> [accordion] Putting a password in the goal
>
> The goal says "log in with [user@example.com](mailto:user@example.com) and password ...". The password ends up in run logs, and the run is harder to reuse. Use a Browser Context Profile (`use_profile`) or Vault (`use_vault`).

> [accordion] Looking up an MCP run from the CLI or API
>
> A run started from an MCP client returns `404 NOT_FOUND` when you fetch it from the CLI or REST API. Look up runs from the same place you started them. MCP runs and API or CLI runs are listed separately.

## Related

> [card] [Error Codes](https://docs.tinyfish.ai/error-codes.md)
>
> Full list of API error codes

> [card] [Runs](https://docs.tinyfish.ai/agent/explanation/runs.md)
>
> Run lifecycle, statuses, and finish reasons

> [card] [Agent Mode, Max Steps, and Max Duration](https://docs.tinyfish.ai/agent/explanation/agent-modes.md)
>
> What each limit is for

> [card] [Browser Context Profiles](https://docs.tinyfish.ai/key-concepts/browser-context-profiles.md)
>
> Reuse a signed-in browser session

> [card] [Anti-Bot Guide](https://docs.tinyfish.ai/anti-bot-guide.md)
>
> Get past bot detection

> [card] [Get a TinyAgent Run as Structured JSON](https://docs.tinyfish.ai/agent/how-to/structured-output.md)
>
> Pin down an exact, predictable result shape


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.