# How to use TinyFish

> Learn TinyFish with JavaScript: search the web, extract data, automate browser tasks, reuse logins, research topics, and monitor websites.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-10-29 | Topics: [AI](https://flaviocopes.com/tags/ai/) | Canonical: https://flaviocopes.com/tinyfish/

**TinyFish** lets your code search the web, read pages, and complete tasks in a remote browser.

You can ask it to collect products from a store, compare hotel availability, check a supplier portal, or fill a form. You give its browser agent a URL and instructions. It works through the website and returns a result your application can use.

There are also simpler APIs for searching and reading pages, plus direct browser control when you want to write the automation yourself.

In this tutorial we'll start with a small product catalog. We'll extract its contents with JavaScript, add a predictable output format, and handle longer tasks. Then we'll look at the other workflows people build with TinyFish, including research, monitoring, and authenticated automations.

I haven't used TinyFish in production. This guide follows its official documentation and public examples. The code shows how to build the integrations; the live service still needs testing with your API key and the websites you plan to use.

## What TinyFish does

A normal HTTP request can download a page. Sometimes that gives you everything you need.

Other pages load their contents with JavaScript. A price might appear only after selecting a country. A booking website might require dates, a location, and several clicks before showing any results.

TinyFish provides several ways to work with those sites:

| Tool | What you give it | What you get |
| --- | --- | --- |
| Search | A search query | Ranked URLs, titles, and snippets |
| Fetch | One or more URLs | Extracted page contents |
| Web Agent | A starting URL and a goal | A browser workflow and its result |
| Browser | Session settings | A remote Chromium browser you control |
| Research | A research question | A report with citations |
| Monitor | A page or search topic and a schedule | Recurring checks and their results |

Suppose we want the current price of a particular product.

Search can find the product page. Fetch can read the page. If the price requires selecting a size or checking delivery, Web Agent can interact with the site. Browser lets us write those interactions ourselves with Playwright.

Start with the simplest tool that can finish the task. Asking a browser agent to read one paragraph usually adds work you don't need. The [official overview](https://docs.tinyfish.ai/) links each API's documentation.

A product research workflow can combine these tools. Use Agent only for the pages that need interaction:

```mermaid
flowchart TD
  accTitle: From product search to saved data
  accDescr: Search finds product URLs, Fetch reads their pages, Agent handles interaction when needed, and your application validates and saves the results.
  A["Search: find product URLs"] --> B["Fetch: read product pages"]
  B --> C["Agent: interact when needed"]
  C --> D["Validate and save results"]
```

## Create an account and try the playground

Create an account at the [TinyFish dashboard](https://agent.tinyfish.ai/). The playground lets you try a browser task before writing code.

For our first task, use this public practice store:

```text
https://scrapeme.live/shop/
```

Give it this goal:

```text
Read the first 3 products on the shop page.
Return each product's name, displayed price, and product URL.
Do not add products to the cart or open checkout.
If a field is missing, return null for that field.
```

The goal explains where to stop and what to return. We only want to read the catalog.

Watch the run, then inspect its result. Check that each URL opens the corresponding product and that the prices match the page. Keep this habit when you move to code. Valid JSON can still contain the wrong information.

Create an API key on the dashboard's [API keys page](https://agent.tinyfish.ai/api-keys). Keep it on your server or in your local environment. A browser application should call your backend, which calls TinyFish. Putting the key in frontend JavaScript exposes it to visitors.

## Set up the JavaScript project

We'll use a recent Node.js release, its built-in file APIs, and TinyFish's official SDK. If these pieces are new, the free [Node.js course](https://flaviocopes.com/courses/nodejs/) covers modules, files, and environment variables.

Create a directory and install the SDK:

```bash
mkdir tinyfish-guide
cd tinyfish-guide
npm init -y
npm pkg set type=module
npm install @tiny-fish/sdk
```

Create a `.env` file containing your API key:

```dotenv
TINYFISH_API_KEY=replace-with-your-tinyfish-api-key
```

Exclude that file and our dependencies from Git. Create `.gitignore`:

```gitignore
.env
node_modules/
```

We'll use `.mjs` files, which Node treats as JavaScript modules. Run each example with `node --env-file=.env` so the SDK can read `TINYFISH_API_KEY`.

The [authentication guide](https://docs.tinyfish.ai/authentication) explains key management and organization keys. The examples below use a personal API key, which also matters later when we use saved login profiles.

## Search for pages

Search is useful when you know the question but haven't chosen a URL.

Let's find official documentation about browser automation. Save this as `search.mjs`:

```js
import { TinyFish } from '@tiny-fish/sdk'

const client = new TinyFish()

const response = await client.search.query({
  query: 'Playwright browser automation locators',
  include_domains: 'playwright.dev',
  language: 'en',
})

for (const page of response.results) {
  console.log(page.title)
  console.log(page.url)
  console.log(page.snippet)
}
```

Run it:

```bash
node --env-file=.env search.mjs
```

`include_domains` limits the search to the documentation site. You can pass a comma-separated string of domains. There is also `exclude_domains` for sources you want to leave out.

A search snippet helps you choose which page to read. Fetch the page before treating the snippet as evidence for a detailed answer.

Search also supports news and academic papers. For example, replace the request above with this one to find recent news:

```js
const response = await client.search.query({
  query: 'browser automation',
  domain_type: 'news',
  recency_minutes: 1440,
  language: 'en',
})
```

`1440` means the last 24 hours. Use `after_date` and `before_date` for calendar boundaries instead, without combining them with `recency_minutes`.

For academic papers, use `domain_type: 'research_paper'`. That mode uses `pub_year_min` and `pub_year_max` for publication years, rather than the web and news date filters. Its results can include authors, venue, and a PDF URL.

The [Search reference](https://docs.tinyfish.ai/search-api/reference) documents these filters, country targeting, and pagination.

## Read a page with Fetch

Fetch turns a URL into content your program can read. It can return Markdown, semantic HTML, or a structured document tree.

We'll fetch the practice store as Markdown. Create `fetch.mjs`:

```js
import { writeFile } from 'node:fs/promises'
import { TinyFish } from '@tiny-fish/sdk'

const client = new TinyFish()

const response = await client.fetch.getContents({
  urls: ['https://scrapeme.live/shop/'],
  format: 'markdown',
  links: true,
  ttl: 0,
})

for (const error of response.errors) {
  console.error(error)
}

const page = response.results[0]

if (!page || typeof page.text !== 'string') {
  throw new Error('The shop page did not return Markdown')
}

await writeFile('shop.md', page.text)
console.log(page.title)
console.log(page.links)
```

Run it:

```bash
node --env-file=.env fetch.mjs
```

Open `shop.md`. You should see the extracted content rather than the original page's scripts and surrounding markup.

`links: true` returns discovered links. Your code chooses which ones to fetch next.

`ttl: 0` asks TinyFish to prefer a live fetch. Origin cache rules can still affect freshness, so treat it as a request for fresh content rather than proof of a new observation. A positive value sets a cache freshness tolerance in seconds. If you omit `ttl`, the documented behavior accepts any available cached entry. Be explicit when checking prices or availability.

### Handle partial failures

Fetch accepts up to 10 URLs per request. Some can succeed while others fail, so inspect both `results` and `errors`.

For a documentation collection, keep the successful pages and retry failed URLs separately. For a comparison of five suppliers, tell the reader when only four suppliers were reachable.

An empty result is different from a page that explicitly says a product is unavailable. Preserve that distinction in your application.

### Extract only part of a page

A large page might contain the article you need alongside a menu and a long footer. Fetch supports CSS selectors to narrow the content.

These selector options are documented in the REST API, but aren't exposed by the published SDK used for this guide. For this request, use Node's built-in `fetch` directly:

```js
const response = await fetch('https://api.fetch.tinyfish.ai', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.TINYFISH_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    urls: ['https://playwright.dev/docs/locators'],
    format: 'markdown',
    include_selectors: ['main'],
    exclude_selectors: ['nav'],
  }),
})

if (!response.ok) throw new Error(`Fetch failed: ${response.status}`)
console.log(await response.json())
```

These are ordinary CSS selectors. If none of the include selectors matches, that URL fails with `selector_not_matched`. It doesn't silently return the whole page.

Also be careful with `format: 'json'`. Fetch's JSON format describes the document. It doesn't automatically create a custom product object with fields such as `price` and `in_stock`. Use Web Agent's structured output when you need that kind of extraction.

See the [Fetch reference](https://docs.tinyfish.ai/fetch-api/reference) for selector behavior, conditional requests, and document handling.

## Extract products with Web Agent

Web Agent takes instructions in natural language. It can inspect a page, follow links, select filters, and decide what to do next.

We'll use it to return three products from the same practice store. Save this as `products.mjs`:

```js
import { TinyFish } from '@tiny-fish/sdk'

const client = new TinyFish()

const run = await client.agent.run({
  url: 'https://scrapeme.live/shop/',
  goal: `Read the first 3 products on the shop page.
    Return JSON with a products array.
    Each product needs name, price_text, and url.
    Copy the price exactly as displayed.
    Do not add anything to the cart or open checkout.`,
})

if (run.status !== 'COMPLETED') {
  throw new Error(JSON.stringify(run.error))
}

console.log(JSON.stringify(run.result, null, 2))
```

Run it:

```bash
node --env-file=.env products.mjs
```

The SDK waits for the synchronous run to finish. A browser task can take much longer than a normal API request, particularly when it crosses several pages.

`COMPLETED` tells us the run ended normally. We still need to check whether its result contains the requested products. The agent could have reached a blocked page and returned an explanation.

The [Agent reference](https://docs.tinyfish.ai/agent-api/reference) describes the request fields and run results.

## Give the result a predictable shape

Prompting for JSON is convenient while experimenting. When another part of your application depends on the output, add `output_schema`.

We'll keep prices as text so we preserve the currency symbol and formatting. Converting prices into numbers comes later, when we know the currency and decimal convention.

Replace the complete contents of `products.mjs` with:

```js
import { writeFile } from 'node:fs/promises'
import { TinyFish } from '@tiny-fish/sdk'

const client = new TinyFish()

const run = await client.agent.run({
  url: 'https://scrapeme.live/shop/',
  goal: `Read the first 3 products on the shop page.
    Return their names, displayed prices, and product URLs.
    Copy prices exactly. Use null for a missing price.
    If the page is blocked or unreadable, explain that in problem
    and return an empty products array.
    Do not add anything to the cart or open checkout.`,
  output_schema: {
    type: 'object',
    properties: {
      products: {
        type: 'array',
        maxItems: 3,
        items: {
          type: 'object',
          properties: {
            name: { type: 'string' },
            price_text: { type: 'string', nullable: true },
            url: { type: 'string' },
          },
          required: ['name', 'price_text', 'url'],
        },
      },
      problem: { type: 'string', nullable: true },
    },
    required: ['products', 'problem'],
  },
})

if (run.status !== 'COMPLETED') {
  throw new Error(JSON.stringify(run.error))
}

const result = run.result

if (!result || typeof result !== 'object' || Array.isArray(result)) {
  throw new Error('Expected a product result object')
}

if (result.problem || !Array.isArray(result.products)) {
  throw new Error(JSON.stringify(result))
}

if (result.products.length !== 3) {
  throw new Error('Expected 3 products; inspect the run before continuing')
}

await writeFile('products.json', JSON.stringify({
  checked_at: new Date().toISOString(),
  run_id: run.run_id,
  ...result,
}, null, 2))
```

The result now has a defined shape, including a place to explain a task failure. We also reject an incomplete list before saving it.

For a larger application, validate every product's fields before using them. Check URLs against the sites you intended to visit, and reject impossible values. A schema controls structure; it doesn't prove that a price matches the website.

TinyFish supports a subset of JSON Schema. In particular:

| Requirement | TinyFish syntax |
| --- | --- |
| Return several items | Put an array inside a top-level object |
| Allow a missing value | Use `nullable: true` on the field |
| Require fields | List them in `required` |
| Limit the list size | Use `maxItems` |
| Restrict string values | Use `enum` |

Common JSON Schema fields such as `additionalProperties` and `oneOf` aren't accepted. Don't paste a generated schema without checking it. The [structured output guide](https://docs.tinyfish.ai/key-concepts/structured-output) lists the supported keywords.

## Write goals that are easy to verify

A useful goal says what the agent should do and how your program will recognize success.

Compare “find cheap hotels” with:

```text
Find hotels in Bologna for 2 adults, checking in on
15 February 2027 and checking out on 17 February 2027.
Keep options with a private bathroom and free cancellation.
Return up to 5 hotels, with the total stay price, currency,
room type, cancellation deadline, and source URL.
If taxes or fees are excluded, record that separately.
Do not reserve a room or enter payment details.
```

The second goal gives dates, constraints, and a stopping point. It also asks for the total stay price, which is much easier to compare than an unlabeled number.

For extraction tasks, specify what to do with missing fields. For comparisons, ask for source URLs and the conditions attached to each offer.

For long catalogs, set a limit. “Collect every product” can turn into a large, expensive job. Start with one category and a maximum number of pages, then inspect the coverage.

TinyFish's [prompting guide](https://docs.tinyfish.ai/prompting-guide) has more examples. Website content can contain instructions too. Treat it as untrusted input, and give the agent a narrow task with only the credentials and downstream access it needs.

## Watch a run while it works

Streaming is useful when you want progress messages or a live browser preview.

Create `stream.mjs`:

```js
import { TinyFish } from '@tiny-fish/sdk'

const client = new TinyFish()
let runId
let completed = false

const stream = await client.agent.stream({
  url: 'https://scrapeme.live/shop/',
  goal: 'Read the first 3 product names and prices. Return JSON.',
})

try {
  for await (const event of stream) {
    if (event.type === 'STARTED') {
      runId = event.run_id
      console.log('Run:', runId)
    }

    if (event.type === 'PROGRESS') {
      console.log(event.purpose)
    }

    if (event.type === 'STREAMING_URL') {
      console.log('Live browser:', event.streaming_url)
    }

    if (event.type === 'COMPLETE') {
      completed = true
      if (event.status !== 'COMPLETED') {
        throw new Error(JSON.stringify(event.error))
      }
      console.log(event.result)
    }
  }

  if (!completed) {
    throw new Error(`Stream ended before completion. Recover run ${runId}`)
  }
} finally {
  await stream.close()
}
```

Run it with `node --env-file=.env stream.mjs`.

Save the run ID as soon as you receive it. If the connection drops, retrieve that run instead of immediately starting another browser job.

The Agent stream doesn't support `Last-Event-ID` reconnection. Recover through `GET /v1/runs/{id}`, or `client.runs.get(id)`. Closing the HTTP stream isn't a substitute for explicitly cancelling the remote task.

If you embed a live preview in your own app, keep it behind the same access controls as the task. An authenticated run can display account information. The [live preview guide](https://docs.tinyfish.ai/live-preview) explains the stream URL.

## Run longer jobs in the background

A user shouldn't have to keep a page open while a browser agent visits several sites.

Use asynchronous runs for background jobs. TinyFish returns an ID immediately, and your worker retrieves the result later.

Create `background.mjs`:

```js
import { setTimeout } from 'node:timers/promises'
import { TinyFish } from '@tiny-fish/sdk'

const client = new TinyFish()

const queued = await client.agent.queue({
  url: 'https://scrapeme.live/shop/',
  goal: 'Read the first 3 product names and prices. Return JSON.',
  agent_config: { max_duration_seconds: 120 },
})

if (!queued.run_id) {
  throw new Error(JSON.stringify(queued.error))
}

console.log('Save this run ID:', queued.run_id)

const deadline = Date.now() + 180_000
let completed = false

while (Date.now() < deadline) {
  const run = await client.runs.get(queued.run_id)

  if (run.status === 'COMPLETED') {
    completed = true
    console.log(run.result)
    break
  }

  if (run.status === 'FAILED' || run.status === 'CANCELLED') {
    throw new Error(JSON.stringify(run.error))
  }

  await setTimeout(3000)
}

if (!completed) {
  throw new Error(`Local polling stopped. Check run ${queued.run_id}`)
}
```

The local deadline limits how long this script polls. The server's `max_duration_seconds` limits the browser task itself. Neither should be confused with an HTTP client timeout.

For a production worker, save the ID in your database before continuing. If a creation request times out before you receive an ID, inspect the run history before retrying. A second request can start a duplicate job.

The REST alternatives are:

| Action | Endpoint on `https://agent.tinyfish.ai` |
| --- | --- |
| Start synchronously | `POST /v1/automation/run` |
| Start in the background | `POST /v1/automation/run-async` |
| Start with events | `POST /v1/automation/run-sse` |
| Read a run | `GET /v1/runs/{id}` |
| Cancel a run | `POST /v1/runs/{id}/cancel` |

Synchronous `/run` jobs cannot be cancelled through the cancellation endpoint. Choose async or streaming when your application needs cancellation.

### Use webhooks instead of constant polling

You can pass `webhook_url` when creating a run. TinyFish calls your HTTPS endpoint when it completes, fails, or is cancelled.

Store the run ID and process each terminal result once. Webhook deliveries can be retried, so receiving the same notification twice mustn't create two records or send two messages.

Before trusting a callback, retrieve the run through your authenticated TinyFish API connection and confirm it belongs to a job your application created. Don't let arbitrary callback JSON trigger account changes.

The [webhook guide](https://docs.tinyfish.ai/webhooks) describes delivery and payloads. A callback delivery failure doesn't mean the browser task failed; the API remains available to retrieve its result.

```mermaid
flowchart TD
  accTitle: Turn a browser run into a background job
  accDescr: A worker starts a run, saves its ID, retrieves the terminal result, validates it, and stores data or records a failure for review.
  A["Start a run"] --> B["Save the run ID"]
  B --> C["Wait for callback or poll"]
  C --> D["Retrieve run through the API"]
  D --> E{"Finished successfully?"}
  E -->|"No"| F["Record error for review"]
  E -->|"Yes"| G{"Result passes validation?"}
  G -->|"No"| F
  G -->|"Yes"| H["Store data with source and check time"]
```

## Control the browser yourself with Playwright

Use the Browser API when you already know the interactions and want to write them in code.

You create a remote Chromium session, then connect using CDP, the Chrome DevTools Protocol. Playwright runs on your machine or server. The browser runs on TinyFish's infrastructure.

Install the browser client:

```bash
npm install playwright-core
```

`playwright-core` is enough because we're connecting to an existing browser. We don't need to download a local Chromium build for this example.

We'll read product names and save a screenshot. Create `browser.mjs`:

```js
import { TinyFish } from '@tiny-fish/sdk'
import { chromium } from 'playwright-core'

const client = new TinyFish()
const session = await client.browser.sessions.create({
  timeout_seconds: 120,
})

let browser

try {
  browser = await chromium.connectOverCDP(session.cdp_url)
  const context = browser.contexts()[0]

  if (!context) throw new Error('No browser context was returned')

  const page = context.pages()[0] || await context.newPage()
  await page.goto('https://scrapeme.live/shop/')

  const names = page.locator('.woocommerce-loop-product__title')
  await names.first().waitFor()
  console.log(await names.allTextContents())
  await page.screenshot({ path: 'shop.png', fullPage: true })
} finally {
  try {
    await browser?.close()
  } finally {
    const response = await fetch(
      `https://api.browser.tinyfish.ai/${session.session_id}`,
      {
        method: 'DELETE',
        headers: { 'X-API-Key': process.env.TINYFISH_API_KEY },
      },
    )

    if (!response.ok) {
      throw new Error(`Browser cleanup failed: ${response.status}`)
    }
  }
}
```

Run it with `node --env-file=.env browser.mjs`.

The CSS selector is specific to this practice store. If its markup changes, update the script. That's the tradeoff when you control the steps yourself.

The explicit `DELETE` ends the remote session. Don't rely on disconnecting Playwright to release a billed browser session. If termination fails, retry cleanup and inspect the session's status.

For repeatable test assertions, see the [Playwright tutorial](https://flaviocopes.com/playwright-e2e-testing/). The [TinyFish Browser reference](https://docs.tinyfish.ai/browser-api/reference) covers session settings and lifecycle.

## Work with sites that require login

A supplier portal, CRM, or billing dashboard usually needs authentication.

TinyFish has **Browser Context Profiles** for saved login state. These preserve cookies and browser storage so a later run can start signed in.

Set one up in the dashboard:

1. Open **Browser Context Profiles** and create a profile.
2. Give it a name identifying the account and environment.
3. Start its setup browser and sign in to your site.
4. Complete any required login verification.
5. Save the setup session and copy the profile ID.

Use separate profiles for separate accounts. A production account and a test account shouldn't share saved browser state.

To try an authenticated extraction, set `TINYFISH_PROFILE_ID` and `PORTAL_URL` in your `.env` file to your profile ID and the portal's dashboard URL. Create `portal.mjs`:

```js
import { TinyFish } from '@tiny-fish/sdk'

const client = new TinyFish()
const url = process.env.PORTAL_URL
const profileId = process.env.TINYFISH_PROFILE_ID

if (!url || !profileId) {
  throw new Error('Set PORTAL_URL and TINYFISH_PROFILE_ID first')
}

const run = await client.agent.run({
  url,
  use_profile: true,
  profile_id: profileId,
  goal: `Read the latest invoice summary.
    Return the invoice number, displayed amount, currency,
    payment status, and invoice page URL.
    Do not pay, edit, or download anything.`,
})

if (run.status !== 'COMPLETED') {
  throw new Error(JSON.stringify(run.error))
}

console.log(run.result)
```

Adapt the goal to the actual portal. This example assumes you have an invoice page and permission to read it.

`use_profile: true` uses saved state. Adding `profile_id` chooses a particular profile; omitting it uses your default profile.

A Browser Context Profile is different from `browser_profile: 'stealth'`. The former stores login state. The latter chooses a browser runtime mode.

The [profile guide](https://docs.tinyfish.ai/key-concepts/browser-context-profiles) also explains programmatic setup. Profiles require a personal key; organization workflow keys can't use them.

### Restore expired sessions with Vault

Saved sessions expire. TinyFish's Vault integration can provide credentials when the login page appears again.

Connect a supported password manager, such as 1Password or Bitwarden, through the dashboard's Vault settings. Select only the credentials needed for the task.

The request options are `use_vault: true` and, for a restricted set of credentials, `credential_item_ids`. Get those IDs from TinyFish's vault item listing rather than inventing them.

According to TinyFish's [credential documentation](https://docs.tinyfish.ai/key-concepts/credentials), the browser agent identifies login fields while TinyFish fills the secrets without exposing their values to the model. That still gives the browser access to the account. Use a dedicated account with appropriate permissions.

MFA prompts, expired sessions, and CAPTCHAs can interrupt automation. Build a way to flag those runs for manual attention.

## Choose a location and browser mode

Some sites show different offers by country. Others block an ordinary automation browser.

For an authorized task on one of those sites, the Agent API supports a proxy country and a `stealth` browser runtime. For example, replace the request in `products.mjs` with:

```js
const run = await client.agent.run({
  url: 'https://scrapeme.live/shop/',
  goal: 'Read the first 3 product names and displayed prices. Return JSON.',
  browser_profile: 'stealth',
  proxy_config: {
    enabled: true,
    country_code: 'GB',
  },
})
```

The practice store doesn't require these options; it gives us a harmless place to see the request shape. Use them on your real workflow only when needed.

A British exit IP doesn't guarantee prices in pounds. Sites can also use account settings, selected currency, and delivery address. Ask for the displayed currency and record the location used for the check.

Keep the country consistent for a saved login profile. Sudden changes can trigger account verification. The [proxy guide](https://docs.tinyfish.ai/key-concepts/proxies) and [browser mode guide](https://docs.tinyfish.ai/key-concepts/browser-profiles) explain these settings.

## Produce a report with Research

Search returns candidate pages. Research investigates a question and produces a report with citations.

Use it for questions that need several sources: comparing software providers, investigating a market, or collecting technical evidence before a decision.

The Research API is in beta. The documented modes are `deep` and `max`, and access can depend on your account.

For a first report, use the streaming REST endpoint:

```bash
curl --fail-with-body -N \
  https://agent.tinyfish.ai/v1/automation/run-research \
  -H "X-API-Key: $TINYFISH_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "Compare Playwright and Puppeteer for a small JavaScript team maintaining browser tests. Use official documentation. Explain browser support, debugging, and maintenance tradeoffs, with source URLs.",
    "mode": "deep",
    "output_language": "en",
    "stream": true
  }'
```

Unlike `node --env-file=.env`, curl doesn't automatically load `.env`. Set `TINYFISH_API_KEY` in the terminal environment before running this command. Avoid typing an actual key into a command you plan to share.

The response is an SSE event stream. Research uses events such as `created`, `source_fetched`, and `final_result`; these differ from the Agent API's uppercase event types.

Also, `stream: false` doesn't turn this endpoint into a normal JSON response. It controls streamed synthesis text. Accepted requests still return SSE.

For background research, use `POST /v1/automation/run-research-async`, then retrieve `/v1/research-run/{id}`. Unlike Agent streaming, Research progress supports reconnecting with `Last-Event-ID`. Read the report text from `deep_result.result` on saved runs.

Always inspect the citations. A generated report can misunderstand a source, miss a condition, or cite an outdated page. For a price comparison, open the official pricing pages before spending money.

The [Research reference](https://docs.tinyfish.ai/research-api/reference) covers filters, continuations, cancellation, and beta limits.

## Monitor a page or topic

A Monitor repeats a check on a schedule.

A **Page Monitor** watches a known public page through Fetch. A **Topic Monitor** repeats a Search query to discover new results.

We'll create a daily check of Playwright's release notes. This creates a persistent schedule in your TinyFish account; pause or delete it when you're done experimenting.

Save this as `monitor.mjs`:

```js
import { TinyFish } from '@tiny-fish/sdk'

const client = new TinyFish()

const { monitor, run } = await client.monitors.create({
  type: 'fetch',
  name: 'Playwright release notes',
  config: {
    url: 'https://playwright.dev/docs/release-notes',
    format: 'markdown',
  },
  schedule_cron: 'CRON_TZ=Europe/Rome 0 9 * * *',
  purpose: 'A new release adds a feature or changes browser support',
})

console.log('Monitor ID:', monitor.id)
console.log('Baseline:', run)
```

Run it with `node --env-file=.env monitor.mjs`.

Creation captures the first result as a baseline. The schedule then checks at 09:00 in the named time zone. Without a `CRON_TZ` prefix, schedules use UTC.

Use `type: 'search'` and a query in `config` for a Topic Monitor. Topic checks must be at least 30 minutes apart.

You can configure email delivery in the dashboard. If you add a webhook, it receives every scheduled run, including checks with no changes. Don't treat the arrival of a webhook as proof of a meaningful update.

A manual run through `client.monitors.run(id)` records a check but doesn't send the scheduled webhook or email notification. It won't test your notification delivery.

To manage an existing monitor, put its ID in `TINYFISH_MONITOR_ID` in `.env`. Create `pause-monitor.mjs`:

```js
import { TinyFish } from '@tiny-fish/sdk'

const client = new TinyFish()
const id = process.env.TINYFISH_MONITOR_ID

if (!id) throw new Error('Set TINYFISH_MONITOR_ID first')

await client.monitors.pause(id)
console.log('Monitor paused')
```

Use `resume(id)` to start future scheduled checks again, or `cancel(id)` to delete the Monitor. Don't rerun the creation script every morning, since each invocation creates another schedule.

The [Monitor reference](https://docs.tinyfish.ai/monitor/api-reference) documents the available settings. Built-in Page Monitors watch public URLs; use your own scheduled Agent job when a check requires logging in or interacting with the site.

## Use TinyFish from the terminal

You can try the same workflows without creating a JavaScript project.

Install the [TinyFish CLI](https://docs.tinyfish.ai/cli/index) and sign in:

```bash
npm install -g @tiny-fish/cli
tinyfish auth login
tinyfish auth status
```

The login command opens the API keys page and asks you to paste a key. The CLI saves it locally. An environment variable named `TINYFISH_API_KEY` takes priority over the saved key.

Search and read a page:

```bash
tinyfish search query "Playwright browser automation"
tinyfish fetch content get https://scrapeme.live/shop/ --format markdown
```

Run the product extraction:

```bash
tinyfish agent run \
  "Read the first 3 product names and prices. Return JSON. Do not buy anything." \
  --url https://scrapeme.live/shop/ \
  --sync
```

The default Agent command streams events. `--sync` waits for the result, while `--async` returns a run ID for later retrieval.

The [CLI command reference](https://docs.tinyfish.ai/cli/commands) also covers run history, profiles, Vault, monitors, and wallet information. The CLI is useful when you want to inspect a task before building it into an application.

## Connect TinyFish to an AI assistant

TinyFish exposes a hosted MCP server. An MCP-compatible assistant can discover its tools and call them during a conversation.

The official [MCP integration guide](https://docs.tinyfish.ai/mcp-integration/index) provides setup instructions for supported clients. The general remote server URL is:

```text
https://agent.tinyfish.ai/mcp
```

Use your client's remote MCP setup and complete the sign-in flow. Some clients need a dedicated endpoint, so follow the instructions for the client you use.

If MCP is new to you, [this introduction](https://flaviocopes.com/what-is-mcp/) explains how the assistant reaches external tools.

After connecting, give the assistant a task with a clear scope:

```text
Use TinyFish to compare the publicly listed plans on Railway
and Render. Include the source URLs, usage charges, and conditions
attached to any free allowance. Report missing information explicitly.
Do not create accounts or purchase anything.
```

Ask it to search and fetch first. A browser agent is useful if it needs to change a pricing selector or work through a calculator.

Your assistant's subscription and TinyFish usage are separate. Connecting an MCP server doesn't remove TinyFish's allowances or paid usage.

## Put TinyFish into an n8n or Dify workflow

In n8n, TinyFish supplies a community node named `n8n-nodes-tinyfish`. Whether you can install community nodes depends on your n8n environment.

A small first workflow is:

1. Add a Manual Trigger.
2. Add TinyFish Web Agent and configure its API key credential.
3. Set the practice store URL and ask for the first three products.
4. Check the returned status and product data.
5. Split the products array into individual items.
6. Write those items to a spreadsheet or database.

Confirm the actual result shape in the execution viewer before configuring the split. An error result shouldn't become a spreadsheet row pretending to be a product.

If the community node isn't available, use n8n's HTTP Request node with an `X-API-Key` credential. For a job that can exceed the workflow's request timeout, start asynchronously and poll by run ID.

The [n8n guide](https://docs.tinyfish.ai/integrations/n8n) walks through extraction into Google Sheets.

Dify has a TinyFish Web Agent plugin in its marketplace. Install it, authorize it with your key, and add its tools to a workflow or agent. For background jobs, pair **Run Asynchronously** with **Get Run** until the task reaches a terminal state. The [Dify guide](https://docs.tinyfish.ai/integrations/dify) lists its tools.

## What people build with TinyFish

TinyFish's [public cookbook](https://github.com/tinyfish-io/tinyfish-cookbook) contains sample applications across shopping, travel, research, education, logistics, and developer tools.

These are examples of what people have built. A sample repository doesn't prove a workflow is reliable enough for production, or that it works with every website in that category.

Here are the main patterns, with a way to approach each one.

### Web scraping and data collection

Collect product catalogs, article lists, business directories, event calendars, or public records.

Use Fetch when the content is already on the page. Use Web Agent when the site needs filters, pagination, or navigation into detail pages.

A bounded extraction goal could be:

```text
Open the first 5 product detail pages in this category.
Return name, displayed price, availability text, and source URL.
Stop after 5 products even if there are more pages.
Use null for missing fields. Do not infer stock from an enabled button.
```

Save the source URL and check time with every record. When a site changes, you need to know which observations need refreshing.

For a predictable page with stable markup, a conventional scraper can be easier to maintain. The [web scraping guide](https://flaviocopes.com/web-scraping/) explains that approach.

### Shopping, price comparison, and stock alerts

The cookbook includes refurbished electronics, game prices, Lego availability, and collectible shopping tools.

The reusable workflow is to find matching products, extract their offers, normalize them, and compare them in your own code.

Match the exact model, size, condition, and seller. A refurbished laptop and a new laptop are different offers. Record shipping and whether taxes are included. Preserve the original price text alongside any normalized amount.

For an alert, compare equivalent offers against the previous observation. A failed fetch shouldn't turn into a “price dropped to zero” notification.

Research can help discover sellers. Use a direct product-page check for the current offer before making a purchase.

### Travel and accommodation

The cookbook's lodging and motorbike rental examples show how agents can search several sites with the same constraints.

Dates and location belong in the request. So do guest counts, pickup and return times, room requirements, and currency when relevant.

Return a short list with source links and the conditions attached to each offer. Have your code distinguish a nightly rate from the total trip price.

Availability can change between the check and checkout. Keep the reservation step separate, and verify the final offer before booking.

### Local discovery and restaurant comparisons

Local search can find restaurants, tutors, services, and nearby offers. Search handles discovery; Fetch or Agent can read the details on relevant pages.

TinyFish's examples include restaurant comparisons and happy-hour discovery. A useful restaurant result records the address, opening hours, menu URL, and when the information was checked.

Location settings help make a search relevant. They don't prove a venue is open or that a menu is current.

For allergies, website extraction can help you find the restaurant's published information. Confirm requirements directly with the venue before relying on it.

### Competitor and market research

The cookbook contains competitor-analysis tools that collect prices and product information.

Define the fields before visiting each site. For software plans, useful fields include billing period, included usage, overage rates, seat minimums, and limits attached to a free allowance.

Store missing values explicitly. “Contact sales” isn't a numeric price, and “starting at” doesn't describe every customer.

For a recurring comparison, Monitor can watch public pricing pages. An Agent job can inspect a pricing calculator that requires selections. Your application should flag changes for review before updating a public comparison table.

### Academic research and technical investigation

Use academic Search to discover papers, then read the originals. Research can group findings into a cited report or investigate a focused technical question.

The cookbook's research assistant is an example of building discovery around academic sources.

Ask for publication dates and links to the paper itself. Distinguish a preprint from a published paper and a source's own findings from a later interpretation.

For technical research, domain filters can help prioritize official documentation. If you're comparing an API feature, verify the relevant request and response rather than relying on a summary.

### Lead discovery and business enrichment

You can build a workflow that finds companies matching public criteria, visits their websites, and records relevant facts.

For example, find software companies with a public integrations page, then record the company name, website, supported integration, and evidence URL.

This is an application pattern using Search, Fetch, and Agent. It doesn't require a separate lead-generation API.

Stick to published business information. Don't guess email addresses or turn a missing contact into invented data. Discovery and sending outreach are separate tasks, with separate permissions and delivery requirements.

### Jobs, scholarships, courses, and tenders

The cookbook includes scholarship discovery, summer-school comparison, tutor discovery, tender searches, and job-finding examples.

These workflows usually combine search with extraction from official detail pages.

For a scholarship, collect the eligibility rules, deadline, funding details, application link, and source date. For a job, collect the location, remote policy, employment type, and official application URL.

A search result can keep appearing after an opportunity closes. Open its detail page and check the status. Include “deadline not found” as a possible result instead of inventing a date.

A recurring Topic Monitor can discover new opportunities. A separate page check confirms whether an existing opportunity is still available.

### Supplier, logistics, and procurement checks

Public examples include semiconductor supply signals and logistics tracking.

A procurement workflow might check the exact part number across suppliers and return stock, minimum order quantity, lead time, and quote conditions.

Use a saved login profile when the relevant information lives behind a supplier account. For authenticated multi-step checks, schedule your own Agent jobs rather than using a public Page Monitor.

Keep status labels close to the supplier's wording. “Available to order” and “in stock” can mean different things. A logistics status should include its source and observation time so old data doesn't look current.

### Internal dashboards and legacy systems

Browser automation can connect systems that have no useful API.

You might read an order status from a vendor portal, check account usage, or collect invoice summaries for reconciliation. TinyFish handles the website interactions, and your code moves validated results into your database.

Start with a read-only workflow and a narrowly scoped account. Add writes after you can reliably identify the correct record and confirm the result.

If the application has a suitable API, use it. APIs usually provide clearer validation and error handling than interacting with a user interface.

### Form filling and repetitive administration

Web Agent can fill forms and move through multi-step interfaces.

A useful first experiment is preparing a draft on a form you own. Give it the exact field values and tell it to stop before submission. Inspect the populated fields yourself.

If you later automate submission, specify what counts as confirmation. Save the resulting record ID or confirmation page. When a connection fails, check whether the form was already submitted before trying again.

A retry that creates another inquiry or order is a data problem, even if both runs completed successfully.

### QA and exploratory browser testing

The cookbook includes a QA application with parallel test runs and live browser previews.

A browser agent can explore a staging site, try a navigation path, and report unexpected results. Give it a test account, a staging URL, and the expected behavior.

For example, ask it to find a product, add it to a test cart, change the quantity, and report whether the displayed total updates. Keep real payments and production accounts out of that experiment.

Use Playwright assertions for behavior that must be checked the same way on every commit. Browser Agent is useful for exploration; deterministic tests are useful for preventing a known bug from returning.

TinyFish Browser can host the remote Chromium session for code you control. Check your required browser coverage before choosing it as the infrastructure for an entire test suite.

### Knowledge bases and research assistants

Search and Fetch can supply public sources for an assistant that answers questions about a topic.

A typical pipeline discovers URLs, fetches their text, splits it into sections, and stores those sections with their source metadata. The assistant retrieves relevant sections before answering.

TinyFish handles retrieval from the web. Your application still needs indexing, access rules, citation handling, and refresh policies. The [RAG chunking guide](https://flaviocopes.com/rag-chunking-explained/) explains how to divide documents for retrieval.

Don't assume that information found on the web is safe to execute. Imported text is source material, including when it contains code or instructions directed at an agent.

### Specialized comparison and discovery apps

The cookbook also includes loan comparisons, pharmacy price comparisons, anime discovery, sports odds comparisons, and football video discovery.

The extraction pattern is similar: find matching items, read the relevant conditions, and return source links. The domain determines what makes two results comparable. A medicine listing needs the exact product and strength; a loan offer needs its fees and eligibility conditions as well as its headline rate.

Collecting published information doesn't establish medical suitability, credit eligibility, or what someone should buy. Those decisions need their own checks. Treat these cookbook projects as examples of data retrieval.

For entertainment discovery, verify the source and availability. A video search result doesn't grant permission to download or redistribute the clip. The cookbook's football project uses TinyFish for discovery and a separate video service for later processing.

### Chatbots and notification tools

Several cookbook projects wrap a narrow task in a chat interface or bot.

A user asks for a deal, a place to stay, or a research summary. The bot runs the appropriate TinyFish workflow and formats the validated result.

Keep a job ID so the conversation can report progress and recover after a disconnect. Deduplicate notifications and let the user stop recurring checks.

TinyFish provides the web access. Your bot still needs the messaging integration, user permissions, storage, and notification rules.

## Scale up without losing track of results

Once a single run works, you can process several sites concurrently.

Start with a small group. The Agent API supports bulk asynchronous submission through `/v1/automation/run-batch`, with a maximum of 100 runs per request. That request limit doesn't promise 100 simultaneous browsers on your account.

Use a queue to control concurrency. Keep one job record per site or account, with its run ID, goal, check time, status, and result.

If four sites succeed and one fails, retain the four results and flag the missing source. Don't restart all five jobs because one source was blocked.

Account API limits and target-site limits are different. Respect both. A proxy doesn't give you unlimited access to another service.

The [async bulk guide](https://docs.tinyfish.ai/examples/bulk-requests-async) explains submission and retrieval. For a small script, a few independent `agent.queue()` calls can be enough; larger jobs need durable queue state.

## Cost and limits

Check usage before leaving a workflow running every day. A short request can start a browser job with many steps.

As checked on 23 October 2026, the official references list these allowances and public rates:

| Tool | Published billing or allowance |
| --- | --- |
| Search | 12,000 free requests per day |
| Fetch | 1,000 successfully fetched URLs per day |
| Web Agent | Public product page lists $0.016 per step |
| Browser | Public product page lists $0.002 per browser minute |
| Research | Beta reference lists a default allowance of 5 completed runs per day, with wallet charging disabled during beta |
| Monitor | Check the dashboard's monitor rate; don't assume scheduled checks use the direct APIs' free allowances |

The free daily allowances reset at 00:00 UTC. Daily allowances don't remove rate limits. The Search reference lists a default of 30 requests per minute; Fetch lists 150 URLs per minute.

After the Search or Fetch free allowance is used, the documented behavior requires a positive wallet balance for continued paid use, or waiting for the reset. Actual account rates and legacy billing can differ.

Check the [Search](https://docs.tinyfish.ai/search-api/reference), [Fetch](https://docs.tinyfish.ai/fetch-api/reference), and [Research](https://docs.tinyfish.ai/research-api/reference) references alongside the public [Agent](https://www.tinyfish.ai/agent) and [Browser](https://www.tinyfish.ai/browser) pricing. Your dashboard is the place to verify your account's rates before scaling up.

Some controls are account-gated. In particular, the Agent reference labels `agent_config.max_steps` and strict mode as beta features. Including `max_steps` without access can return `403`. Don't copy that option into every request assuming it is generally available.

A good way to estimate a workflow is to run a small representative sample, inspect the steps and cost, then calculate the daily volume. Include failed runs and retries in your estimate.

## When a run fails

Failures can happen when creating a run, while the browser works, or while your application processes the result.

| Symptom | What to check |
| --- | --- |
| `401` | API key, or a Vault connection that needs reconnecting |
| `402` | Wallet balance or an exhausted free allowance |
| `403` | Account access, organization restrictions, or a beta-only option |
| `429` | API rate limit or concurrent-run limit |
| Task times out | Goal size, page load, login state, and server duration limit |
| Run completes with no useful data | Block page, missing fields, wrong page, or an overly broad goal |
| Fetch returns some data and some errors | Handle each URL separately |
| Webhook doesn't arrive | Delivery failure; retrieve the saved run |

Use the run ID to inspect what happened. Step information and available artifacts help distinguish a bad extraction goal from a login problem. Some capture options require account access, so don't assume every run includes a recording or full HTML snapshot.

Stealth mode and proxies can help with some protected sites, but they don't guarantee access. A site can still require verification or block the workflow. Stop and investigate repeated access failures.

Retry temporary infrastructure errors with backoff, honoring `Retry-After` where available. Fix invalid inputs and expired credentials before retrying. For a task that can write data, first check whether the action already happened.

The [error reference](https://docs.tinyfish.ai/error-codes) documents the machine-readable codes. Store those codes alongside a useful message rather than collapsing everything into “automation failed.”

## How I would use TinyFish

I would start with checking a small set of public provider pages behind a comparison tool.

This site has a [hosting picker](https://flaviocopes.com/hosting-picker/) and a [payment picker](https://flaviocopes.com/payment-picker/). Their data needs careful review when providers change prices or conditions. I would use TinyFish to gather proposed changes with source links and check times, then review them before updating the data.

Fetch would handle pages where the information is already visible. I would use Web Agent for selectors or calculators that require interaction, with a bounded goal for each provider. A failed check would leave the existing data in place and create something to investigate.

I would also try it for a vendor portal that makes me repeat the same read-only task. I would want one saved profile for that account, a small output schema, and a run history I can inspect.

## Where I would use something else

For a service with a good API, I would start with that API. For a stable page that needs one extraction rule, I would consider a conventional scraper.

For a test suite that must make the same assertions on every commit, I would write Playwright tests. For a private system reachable only on my laptop or local network, I would first check network access, or use a [local Mac browser with Playwright](https://flaviocopes.com/macos-browser-ai-agents/): TinyFish's cloud browser doesn't automatically inherit my machine's connections.

I would keep consequential actions behind a separate approval step until the workflow has been tested thoroughly. A goal saying “don't buy anything” is useful, but account permissions and application controls should enforce the boundary too.

Start with the practice store, inspect the saved `products.json`, and compare every field with the source page. Then try one real task small enough that you can tell whether the result is correct.
