# tabstack

> Browsing infrastructure for AI agents on the iii bus — tabstack::* functions turn any URL into clean markdown or structured JSON, and run cited web research through the hosted Tabstack API.

| field | value |
|-------|-------|
| version | 0.2.0 |
| type | bundle |
| license | Apache-2.0 |
| author | Mozilla |

## installation

```sh
iii trigger compose::add worker=tabstack@0.2.0
```

## dependencies

- `configuration` @ `latest`

## readme

# Tabstack

Tabstack reads the sources, checks what is missing, reconciles conflicting information, and returns a cited answer in one call. The same API also returns clean text, matching JSON, or a completed public web task.

Backed by the hosted [Tabstack](https://tabstack.ai) API, built by Mozilla. You
bring a Tabstack API key; usage is billed to your own account. Full endpoint
semantics live in the [Tabstack API reference](https://docs.tabstack.ai/api).

## Install

```bash
iii trigger compose::add worker=tabstack
```

`iii trigger compose::add` resolves the worker and its dependencies, writes exact
declarations to `worker-compose.yaml`, and reconciles the Compose project.

## Configuration

The worker needs a Tabstack API key.
[Sign up free](https://console.tabstack.ai/signup?source=iii) — the trial needs
no card.

It registers a schema and seed in the `configuration` worker on boot, so the key
can be set from the Console or with `configuration::set`:

```yaml
tabstack:
  api_key: your-api-key                 # required; no default
  base_url: https://api.tabstack.ai/v1  # override only for testing
```

If no key is stored, the worker falls back to the `TABSTACK_API_KEY` environment
variable. With neither set, every call fails with `tabstack_not_configured`.

Note the configuration entry is namespaced: in the `default` namespace the id is
`default-tabstack`, not `tabstack`.

## Quickstart

Read a page as markdown:

```bash
iii trigger tabstack::extract-markdown url=https://example.com
```

```json
{
  "ok": true,
  "url": "https://example.com",
  "content": "---\ntitle: Example Domain\n---\n\nThis domain is for use in documentation examples..."
}
```

Pull structured data out of a page by describing the shape you want:

```bash
iii trigger tabstack::extract-json url=https://news.ycombinator.com \
  json_schema='{"type":"object","properties":{"stories":{"type":"array","items":{"type":"object","properties":{"rank":{"type":"integer"},"title":{"type":"string"},"points":{"type":"integer"}}},"maxItems":5}}}'
```

```json
{
  "ok": true,
  "data": {
    "stories": [
      { "rank": 1, "title": "Web Search API", "points": 318 },
      { "rank": 2, "title": "The future of independence is interdependence", "points": 41 }
    ]
  }
}
```

From another worker:

```ts
import { registerWorker } from 'iii-sdk';

const iii = registerWorker('ws://localhost:49134', { workerName: 'my-worker' });

const result = await iii.trigger({
  function_id: 'tabstack::extract-markdown',
  payload: { url: 'https://example.com' },
  timeoutMs: 60_000,
});
```

**Raise the call timeout.** iii's default is 30 seconds. `extract-markdown` and
`extract-json` usually return well inside that, but `effort: "max"` renders the
page in a full browser and is documented at up to 60 seconds, and `automate`
runs for minutes. A caller on the default gives up before the work finishes.

From the CLI it is a **flag**, not a key/value pair — `--timeout-ms` goes before
the function name. `timeout_ms=…` as a pair is passed through as payload and
silently has no effect:

```bash
iii trigger --timeout-ms 300000 tabstack::automate task='...' url='...'
```

From another worker it is `timeoutMs` on the `trigger` call.

## The functions

| Function | Takes | Returns |
|---|---|---|
| `tabstack::extract-markdown` | `url` | the page as markdown in `content` |
| `tabstack::extract-json` | `url`, `json_schema` | matching object in `data` |
| `tabstack::generate-json` | `url`, `json_schema`, `instructions` | matching object in `data` |
| `tabstack::research` | `query` | a cited markdown answer in `report` |
| `tabstack::automate` | `task` | the agent's final answer in `result` |

See the [Tabstack API reference](https://docs.tabstack.ai/api) for more details about each function.

### Researching rather than reading

`research` is the one function that does not take a url. Give it a question and
it searches, reads and synthesises an answer with inline citations:

```bash
iii trigger --timeout-ms 180000 tabstack::research query='What is the iii.dev worker registry?'
```

```json
{
  "ok": true,
  "report": "The iii.dev worker registry is a central online repository located at `workers.iii.dev` ... [1][2]",
  "metadata": {
    "citedPages": [{ "id": "5YI5Rc", "title": "iii worker - iii.dev", "url": "https://workers.iii.dev/" }],
    "mode": "fast",
    "totalPagesAnalyzed": 3
  }
}
```

### Completing a task in a browser

`automate` drives a real browser from a plain-language task: navigate, read,
fill forms, work through multi-step flows.

```bash
iii trigger --timeout-ms 300000 tabstack::automate \
  task='Fill in the contact form with the supplied details' \
  url=https://example.com/contact \
  data='{"name":"Alex Johnson","email":"alex@example.com"}' \
  guardrails='fill the fields only, do not submit' \
  trusted_hostnames='["example.com"]'
```

```json
{ "ok": true, "result": "The name and email fields have been populated. The form was not submitted." }
```

**Put every value the task needs in `data`.** A value that is missing may be
filled with something the agent invented rather than reported back, so do not
rely on it to ask you.

`guardrails` is worth using. It constrains what the agent may do — "browse and
extract only, do not submit any form" is a sensible default for anything
read-only.

`trusted_hostnames` lifts the action firewall for fills and submissions on the
hosts you list. Page content on those hosts can steer the agent, so list only
sites you would trust with whatever is in `data`.

Runs take minutes. The timeout is a CLI flag, not a payload key — see above.

### Choosing between extract-json and generate-json

`extract-json` takes a schema alone and is the cheaper of the two. Reach for it
whenever the schema says enough about what you want.

`generate-json` additionally takes natural-language `instructions`, for when the
schema alone cannot express *how* values should be derived — ranking, classifying,
or comparing across a whole page. "Pick the dominant theme across these stories"
needs an instruction; a bare schema cannot say it.

## Reading the result

Every function returns an envelope. On success, `ok` is `true` and the result
sits alongside it. When Tabstack returns an error, `ok` is `false` and the call
still succeeds at the iii level:

```json
{ "ok": false, "error": "tabstack_invalid_request", "message": "invalid URL" }
```

| `error` | Meaning |
|---|---|
| `tabstack_auth_failed` | key missing, wrong or revoked |
| `tabstack_out_of_credits` | account balance exhausted |
| `tabstack_rate_limited` | too many requests; back off and retry |
| `tabstack_not_found` | endpoint not found |
| `tabstack_invalid_request` | bad URL, malformed schema, or similar |
| `tabstack_upstream_error` | Tabstack-side failure; safe to retry |
| `tabstack_automation_failed` | the browser task could not be completed; `message` says why |
| `tabstack_incomplete_stream` | the response ended before finishing (`research`, `automate`) |

Failures are returned rather than thrown so callers can branch on `error`. The
Node SDK collapses every thrown handler error to `invocation_failed`, which would
make these indistinguishable.

Two cases *are* thrown, because no useful result exists: `tabstack_not_configured`
when no API key is available, and `tabstack_unreachable` / `tabstack_timeout` when
the request never reached Tabstack at all.

These codes map onto the HTTP statuses in the
[Tabstack API reference](https://docs.tabstack.ai/api), which is the authority on
what each endpoint can return.

## api reference

```json
{
  "functions": [
    {
      "description": "Drive a browser with a plain-language task: navigate, read, fill forms, complete multi-step flows. Returns the agent's final answer. Supply every value the task needs in `data` — a value that is missing may be filled with something the agent invented rather than reported, so do not rely on it to ask. Runs for minutes, so raise your call timeout well above the 30s default.",
      "metadata": {},
      "name": "tabstack::automate",
      "request_schema": {
        "properties": {
          "data": {
            "description": "Values the task may need, e.g. form fields. Supply these up front: if the agent needs something that is missing, the call returns tabstack_input_required naming the fields rather than guessing.",
            "type": "object"
          },
          "guardrails": {
            "description": "Safety constraints, e.g. \"browse and extract only, do not submit any form\".",
            "type": "string"
          },
          "max_iterations": {
            "description": "Cap on task iterations. Default 50.",
            "type": "integer"
          },
          "task": {
            "description": "What to do, in plain language. e.g. \"Find the top 3 trending repositories and extract their names and star counts\".",
            "type": "string"
          },
          "trusted_hostnames": {
            "description": "Hostnames where filling and submitting is permitted. Page content on these hosts can steer the agent, so list only sites you trust with the data in `data`.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "url": {
            "description": "Page to start from.",
            "type": "string"
          }
        },
        "required": [
          "task"
        ],
        "type": "object"
      },
      "response_schema": {
        "properties": {
          "error": {
            "description": "Failure code, present only when ok is false. One of: tabstack_auth_failed, tabstack_out_of_credits, tabstack_rate_limited, tabstack_not_found, tabstack_invalid_request, tabstack_upstream_error.",
            "type": "string"
          },
          "message": {
            "description": "Human-readable failure detail, present only when ok is false.",
            "type": "string"
          },
          "ok": {
            "description": "True on success. When false, read `error` and `message` instead of the result fields.",
            "type": "boolean"
          },
          "result": {
            "description": "The agent's final answer once the task completed.",
            "type": "string"
          }
        },
        "required": [
          "ok"
        ],
        "type": "object"
      }
    },
    {
      "description": "Pull structured data off a page into a shape you define. Supply a JSON Schema and get back matching data. This is the cheaper of the two JSON functions — reach for it whenever the schema alone says enough about what you want.",
      "metadata": {},
      "name": "tabstack::extract-json",
      "request_schema": {
        "properties": {
          "effort": {
            "default": "standard",
            "description": "Speed against capability. \"min\" is fastest with no fallback (1-5s); \"standard\" is balanced (3-15s); \"max\" renders in a full browser for JS-heavy sites (15-60s).",
            "enum": [
              "min",
              "standard",
              "max"
            ],
            "type": "string"
          },
          "json_schema": {
            "description": "JSON Schema describing the shape you want back. Drives the structure of `data`.",
            "type": "object"
          },
          "nocache": {
            "default": false,
            "description": "Bypass cache and refetch.",
            "type": "boolean"
          },
          "url": {
            "description": "URL to extract structured data from.",
            "type": "string"
          }
        },
        "required": [
          "url",
          "json_schema"
        ],
        "type": "object"
      },
      "response_schema": {
        "properties": {
          "data": {
            "description": "The extracted or generated object, matching the `json_schema` you supplied.",
            "type": "object"
          },
          "error": {
            "description": "Failure code, present only when ok is false. One of: tabstack_auth_failed, tabstack_out_of_credits, tabstack_rate_limited, tabstack_not_found, tabstack_invalid_request, tabstack_upstream_error.",
            "type": "string"
          },
          "message": {
            "description": "Human-readable failure detail, present only when ok is false.",
            "type": "string"
          },
          "ok": {
            "description": "True on success. When false, read `error` and `message` instead of the result fields.",
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "type": "object"
      }
    },
    {
      "description": "Fetch a URL and return it as clean markdown. Use this to read a web page. Prefer it over extract-json when you want the prose rather than specific fields.",
      "metadata": {},
      "name": "tabstack::extract-markdown",
      "request_schema": {
        "properties": {
          "content": {
            "default": "main",
            "description": "Content scope. \"main\" returns the main article content; \"full\" returns the whole page including navigation, footer and links.",
            "enum": [
              "main",
              "full"
            ],
            "type": "string"
          },
          "effort": {
            "default": "standard",
            "description": "Speed against capability. \"min\" is fastest with no fallback (1-5s); \"standard\" is balanced (3-15s); \"max\" renders in a full browser for JS-heavy sites (15-60s).",
            "enum": [
              "min",
              "standard",
              "max"
            ],
            "type": "string"
          },
          "metadata": {
            "default": false,
            "description": "Include Open Graph and HTML metadata as a separate response field.",
            "type": "boolean"
          },
          "nocache": {
            "default": false,
            "description": "Bypass cache and refetch.",
            "type": "boolean"
          },
          "url": {
            "description": "URL to fetch and convert to markdown.",
            "type": "string"
          }
        },
        "required": [
          "url"
        ],
        "type": "object"
      },
      "response_schema": {
        "properties": {
          "content": {
            "description": "The markdown content, with metadata as YAML frontmatter by default.",
            "type": "string"
          },
          "error": {
            "description": "Failure code, present only when ok is false. One of: tabstack_auth_failed, tabstack_out_of_credits, tabstack_rate_limited, tabstack_not_found, tabstack_invalid_request, tabstack_upstream_error.",
            "type": "string"
          },
          "message": {
            "description": "Human-readable failure detail, present only when ok is false.",
            "type": "string"
          },
          "metadata": {
            "description": "Page metadata. Present only when `metadata` was true.",
            "type": "object"
          },
          "ok": {
            "description": "True on success. When false, read `error` and `message` instead of the result fields.",
            "type": "boolean"
          },
          "url": {
            "description": "The URL that was converted.",
            "type": "string"
          }
        },
        "required": [
          "ok"
        ],
        "type": "object"
      }
    },
    {
      "description": "Produce structured output from a page using natural-language instructions alongside a JSON Schema. Use it when the schema alone cannot say how the values should be derived — ranking, classifying, comparing across a whole page, or any case where you need to steer the interpretation. Costs more than extract-json, so prefer that one when a bare schema is sufficient.",
      "metadata": {},
      "name": "tabstack::generate-json",
      "request_schema": {
        "properties": {
          "effort": {
            "default": "standard",
            "description": "Speed against capability. \"min\" is fastest with no fallback (1-5s); \"standard\" is balanced (3-15s); \"max\" renders in a full browser for JS-heavy sites (15-60s).",
            "enum": [
              "min",
              "standard",
              "max"
            ],
            "type": "string"
          },
          "instructions": {
            "description": "What to produce from the page. This is the reasoning task, not a selector.",
            "type": "string"
          },
          "json_schema": {
            "description": "JSON Schema describing the shape you want back. Drives the structure of `data`.",
            "type": "object"
          },
          "nocache": {
            "default": false,
            "description": "Bypass cache and refetch.",
            "type": "boolean"
          },
          "url": {
            "description": "URL to reason over.",
            "type": "string"
          }
        },
        "required": [
          "url",
          "json_schema",
          "instructions"
        ],
        "type": "object"
      },
      "response_schema": {
        "properties": {
          "data": {
            "description": "The extracted or generated object, matching the `json_schema` you supplied.",
            "type": "object"
          },
          "error": {
            "description": "Failure code, present only when ok is false. One of: tabstack_auth_failed, tabstack_out_of_credits, tabstack_rate_limited, tabstack_not_found, tabstack_invalid_request, tabstack_upstream_error.",
            "type": "string"
          },
          "message": {
            "description": "Human-readable failure detail, present only when ok is false.",
            "type": "string"
          },
          "ok": {
            "description": "True on success. When false, read `error` and `message` instead of the result fields.",
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "type": "object"
      }
    },
    {
      "description": "Answer a question by researching the live web, returning a markdown report with inline citations. Use this when the answer is spread across sources you have not identified yet; use extract-markdown when you already know the page to read. Runs for tens of seconds or more, so raise your call timeout.",
      "metadata": {},
      "name": "tabstack::research",
      "request_schema": {
        "properties": {
          "fetch_timeout": {
            "description": "Seconds to allow for fetching each web page.",
            "type": "integer"
          },
          "mode": {
            "default": "fast",
            "description": "How much work to do. \"fast\" answers quickly; \"balanced\" researches more thoroughly and costs more.",
            "enum": [
              "fast",
              "balanced"
            ],
            "type": "string"
          },
          "nocache": {
            "description": "Bypass cache and research afresh.",
            "type": "boolean"
          },
          "query": {
            "description": "The question to answer. Maximum 10,000 characters.",
            "type": "string"
          }
        },
        "required": [
          "query"
        ],
        "type": "object"
      },
      "response_schema": {
        "properties": {
          "error": {
            "description": "Failure code, present only when ok is false. One of: tabstack_auth_failed, tabstack_out_of_credits, tabstack_rate_limited, tabstack_not_found, tabstack_invalid_request, tabstack_upstream_error.",
            "type": "string"
          },
          "message": {
            "description": "Human-readable failure detail, present only when ok is false.",
            "type": "string"
          },
          "metadata": {
            "description": "Cited pages, executed queries and metrics for the run. Page `fullText` is stripped; fetch a cited url with extract-markdown if you need the source text.",
            "type": "object"
          },
          "ok": {
            "description": "True on success. When false, read `error` and `message` instead of the result fields.",
            "type": "boolean"
          },
          "report": {
            "description": "The answer as markdown, with inline citations to the sources used.",
            "type": "string"
          }
        },
        "required": [
          "ok"
        ],
        "type": "object"
      }
    }
  ],
  "triggers": []
}
```
