Tools — data collection and retrieval

🔵 Engines for fetching data from outside: HTTP scraping, local archive, JSON, RSS, PDF, geocoding. For a summary table of all engines, see "Tools Reference".

5.4 site_tool

A universal declarative HTTP/API client: request → field extraction → output format. Despite the name, it's not just a scraper — it supports arbitrary REST: method (GET/POST/PUT/PATCH/DELETE), bodies json/form/multipart/body, custom headers, OAuth2, HMAC signing, cookies, pagination. For REST integrations you can write kind: rest_tool — it's an alias of the same engine (clearer in meaning).

kind: site_tool                     # или rest_tool — алиас для REST/API
name: my_scraper
description: Парсит страницу товара.
parameters:
  type: object
  properties:
    url: { type: string }
  required: [url]

fetch:
  url: "{{ input.url }}"          # шаблон MiniJinja
  method: POST                    # GET (по умолч.) | POST | PUT | PATCH | DELETE
  json: { query: "{{ input.q }}" }  # JSON-тело (листья — шаблоны)
  # body: "q={{ input.q | urlencode }}"   # сырое тело (шаблон)
  # form: { search: "{{ input.q }}" }      # x-www-form-urlencoded
  # multipart: { fields: {...}, files: {...} }
  timeout_secs: 15
  cache_ttl_secs: 300
  follow_redirects: true
  cookies: true
  headers:
    User-Agent: "MyBot/1.0"
    Authorization: "Bearer {{ env_var('API_KEY') }}"
  # oauth2:                          # OAuth2 (refresh_token / client_credentials)
  #   token_url: "https://oauth.yandex.ru/token"
  #   grant: refresh_token
  #   client_id_env: X_CLIENT_ID
  #   client_secret_env: X_CLIENT_SECRET
  #   refresh_token_env: X_REFRESH_TOKEN
  #   prefix: "OAuth "               # префикс заголовка (Bearer по умолч.; Yandex — OAuth)

source:
  type: script_json               # JSON внутри <script>
  contains: productData
  # type: json                    # или тело само — JSON

extract: { ... }
output: { ... }

fetch — URL

Two styles:

  1. Direct URL — url: "{{ input.url }}".
  2. Path builder — base + path_opt + path + query (example — drom_search.yaml):
fetch:
  base: "https://auto.drom.ru/"
  path_opt:
    - when: { has: region }
      value: "region{{ region | region_code }}"
    - when: { equals: [condition, "new"] }
      value: "new"
  path:
    - "{{ brand }}"
    - "{{ model | default('') | slugify }}"
    - "{% if page is defined and page > 1 %}page{{ page }}{% endif %}"
  query:
    unsold: "1"
    minprice: "{{ price_from }}"
  filters:
    slugify: _builtin_
    region_code:
      moscow: "77"
      nsk: "54"
    region_code_ci:
      case_insensitive: true
      default: "77"
      "Москва": "77"

filters maps filter names to _builtin_ (e.g. slugify) or a key→value table. The lookup table is either a plain map, or an object with case_insensitive: true and default (fallback when the value is absent).

env_var('NAME') reads an environment variable — use it for keys in headers.

query is also applied with a direct url (added with URL encoding).

Templates: namespaces and functions

In all config fields (url, base/path/path_opt, query, headers, body, json, form, multipart, output) the same context is available:

That is, {{ org }} and {{ input.org }} are interchangeable in any field (previously flat names were only visible to path).

Available MiniJinja functions:

Function What it does
env_var('NAME') value of an environment variable ("" if not set)
state('scope', 'key') value from the caller's state; scope = agent | user | session
template_var('name') session template variable (template_vars), e.g. admin_templates

state(...) resolves scope from the calling session, not from the arguments: this way agent settings (e.g. state('agent', 'repo'), written by state_tool) are substituted into the request without passing them as an argument. A missing key renders as empty (undefined); calling state(...) outside a tool invocation or without an enabled state-store is a render error, not a silently empty value.

fetch — shared profile (extends)

To avoid repeating base_url and Authorization, move them into a profile and specify extends (path relative to the tool's YAML):

# tools/_shared.yaml
base_url: "https://api.example.com"
headers:
  Authorization: "Bearer {{ env_var('API_KEY') }}"
# tools/list.yaml
fetch:
  extends: _shared.yaml
  path: ["issues"]
  headers:
    Accept: "text/plain"      # перекрывает Accept из профиля

The tool's own url/base/headers take precedence over the profile.

A profile is not a tool (it has no kind): don't list it in tool_files, otherwise the loader won't recognize the file and the tool won't be registered. You can verify that all tool_files actually loaded in templates_list (fields tools_declared/tools_loaded/tools_failed).

fetch — request body

Exactly one of body / json / form / multipart (an ambiguous config is rejected at load time). json: — JSON body, each string leaf is a template. form: — urlencoded. multipart: { fields, files } — files are local paths.

Booleans/null in MiniJinja render Python-style (True/False/None), which is invalid JSON — in a raw body, pass values through tojson:

body: '{"completed": {{ input.completed | tojson }}}'

For a json body, omit_empty: true recursively drops keys with empty values ("", null, [], {}) — handy for PATCH.

fetch — retries / backoff

Transient failures are retried automatically: HTTP 429, 5xx, network/timeout errors. retries — number of attempts (default 3, 0 disables); backoff_ms — base delay with doubling (default 3000 → 3s, 6s, 12s).

fetch — pagination (token style)

For lists with a next-page token, add pagination:

pagination:
  token_field: /next_page_token
  token_param: page_token
  items_field: /issues
  max_items: 100
  max_pages: 10

The tool follows pages itself (aggregating items) and reports where to continue. Result: { items, page_info: { returned, total, has_more, next_token } }. The LLM continues with a call using page_token: <next_token>.

source — where structured data lives

Field Values
type script_json (JSON blob in <script>) or json (body is JSON)
contains substring that the script must contain (for script_json)
min_length min. script length (default 5000)

Other source types:

fetch — two-step token (auth)

When a token is needed first (anonymous authorization), add auth:

fetch:
  auth:
    token_url: "https://…/api/v1/auth/anonymous"
    method: POST
    json: { app: "mobile" }        # или body / form / headers
    field: token.accessToken       # JSON-указатель /a/b или точка a.b
    header: Authorization
    prefix: "Bearer "

fetch — OAuth2 (oauth2)

For OAuth2 services (Bitrix24, amoCRM, Google) — oauth2. Unlike auth, the engine caches the access token until expiry, rotates the refresh token, and on HTTP 401 refreshes the token itself and retries the request. Secrets are taken from env (never in YAML):

fetch:
  oauth2:
    token_url: "https://…/oauth2/token"
    grant: refresh_token          # refresh_token (по умолч.) | client_credentials
    client_id_env: OAUTH_CLIENT_ID
    client_secret_env: OAUTH_CLIENT_SECRET
    refresh_token_env: OAUTH_REFRESH_TOKEN   # для grant: refresh_token
    # scope: "..."                # для grant: client_credentials
    header: Authorization         # по умолчанию
    prefix: "Bearer "             # по умолчанию

The response is parsed by standard OAuth2 fields: access_token, expires_in (seconds) and optional refresh_token. The returned refresh_token replaces the cached one for subsequent refreshes. oauth2 and auth are mutually exclusive.

fetch — request signing (sign)

HMAC-SHA256 for APIs that require signing (YuKassa, Tinkoff, Sber). The canonical string is a MiniJinja template with the variables method, url, body (canonical body) and input:

fetch:
  sign:
    secret_env: SIGN_SECRET       # ключ подписи из env
    template: "{{ method }}{{ url }}{{ body }}"   # по умолчанию
    header: X-Signature           # либо header, либо query
    # query: sign
    encoding: hex                 # hex (по умолч.) | base64

header and query are mutually exclusive: the signature goes either into a header or into a query parameter.

extract — field types

Three types, distinguished by keys.

Simple field — navigation by JSON pointer:

extract:
  location:
    pointer: /geoInfo/0/text
    value: /sub/path           # доп. навигация вглубь
    fallback: "unknown"
    transform: int

Pointers can be MiniJinja templates from input; the segment * (or ~first) steps over a dynamic key.

Array field — array filtering by discriminator:

  year:
    pointer: /fields
    array_match: { type: year }   # первый элемент с .type == "year"
    value: /payload
    transform: int
  photos:
    pointer: /gallery/images
    collect_all: true             # все элементы → массив
    value: /image/src
    transform: [ { skip: 5 }, { take: 20 } ]

Computed field — conditional logic:

  status:
    compute:
      - when: { op: not_null, pointer: /soldNotification }   then: sold
      - when: { op: equals, pointer: /shouldShowDeleted, value: true }  then: deleted
    default: active

op in deliveries: not_null, equals.

Transformations

Applied in order (can be chained as a list):

Transformation YAML Description
Lookup { lookup: { "1": foo, "2": bar } } value by table (unknown → "?")
Truncate { truncate: 2000 } truncate to N characters
Take { take: 20 } take the first N
Skip { skip: 5 } discard the first N
Len len array length
NotNull not_null true if not null
Bool bool to boolean
Int int to integer
Filter { filter: "not {{ item._sold }}" } discard elements (arrays)
Transliterate transliterate transliteration to slug
StripHtml strip_html remove HTML/XML tags (<noindex>…</noindex> → text)

output — format

Optional MiniJinja template. Leaves are rendered from input and extracted. {{ extracted._warnings }} is available when fields did not resolve. Without output the extracted map is returned as is.

Error handling

The engine does not panic: errors return success: false with a message (HTTP 403/404/429/5xx, network errors, "structured data not found").


5.5 archive_tool

Search over a local archive.

kind: archive_tool
name: ngu_search
description: Поиск по архиву НГУ.
parameters:
  type: object
  properties:
    query: { type: string }
  required: [query]

mode: search_grouped
archive:
  path: data/ngu
  default_limit: 10

filters:
  - { field: players, op: players_min, param: players_min }
  - { field: price,   op: gte,        param: price_min }

sort:
  field: price
  order: asc

mode

Mode Purpose
search keyword search
search_grouped results grouped by page (matched_chunks)
lookup full document by URL
read_chunks range of chunks (start_chunk, end_chunk)
outline page structure: title, number of characters/chunks, chunk previews

The archive is a directory with meta.json, docs.bin (documents by chunks) and terms.bin (term index); assembled by the agent-os scrape command.


5.6 json_tool

Search over preloaded JSON.

kind: json_tool
name: find_brands
description: Поиск брендов по имени.
parameters:
  type: object
  properties:
    query: { type: string }
    section: { type: string, enum: [auto, moto, truck] }
  required: [query]

data_file: data/drom_brands.json

search:
  paths: ["{{ input.section | default('*') }}.top", "*.all"]
  field: name
  query_param: query
  mode: contains
  limit: 20
  dedupe_by: id
  source_field: section

computed:
  slug: "{{ item.url | path_segment(0) }}"
Field Description
data_file JSON file to load
search.paths JSON search paths; * = wildcard key
search.field field for matching
search.query_param input parameter with the query
search.mode contains (partial match)
search.limit maximum results
search.dedupe_by deduplication by field
computed MiniJinja templates per result (item in scope)

5.7 rss_tool

Search over RSS feeds.

kind: rss_tool
name: rss_search
description: Поиск новостей по ключевому слову.
parameters:
  type: object
  properties:
    site: { type: string }
    query: { type: string }
  required: [site, query]

feeds:
  lenta: "https://lenta.ru/rss"
  ria: "https://ria.ru/export/rss2/archive/index.xml"
  rbc: "https://rssexport.rbc.ru/rbcnews/news/30/full.rss"

site — key in feeds. The result carries count, titles, snippets, URLs.


5.8 pdf_tool

Reading PDF page by page.

kind: pdf_tool
name: pdf_read
description: Скачать и прочитать PDF.
parameters:
  type: object
  properties:
    url: { type: string }
    page: { type: integer, default: 1 }
  required: [url]

fetch:
  url: "{{ input.url }}"
  timeout_secs: 30
  cache_ttl_secs: 3600

pdf:
  chars_per_page: 3000

output:
  page: "{{ extracted.page }}"
  total_pages: "{{ extracted.total_pages }}"
  text: "{{ extracted.text }}"
  next_page: "{{ extracted.next_page }}"

5.15 geo_tool

Geocoding and places (OpenStreetMap). Free, no keys, geocoding and POI search (Nominatim + Overpass API) — the agent does not make up addresses.

kind: geo_tool
name: geo_places
description: Найти места категории рядом.
operation: places
operation Purpose Parameters
locate geocoding a place → coordinates + bbox query
places POI of a category nearby / within bbox category, place/bbox, limit
distance sorting addresses by distance place, addresses

Categories (RU/EN synonyms): сто/автосервис/car_repair, шиномонтаж, аптека/pharmacy, банк/атм, кафе/ресторан, школа, магазин/ продукты/супермаркет, заправка/азс, парковка. Unknown category → Overpass search shop=<value>.

Optional: nominatim_url / overpass_url, user_agent, ui.


5.15.1 rasp_tool

Transport schedules (trains, commuter trains, planes, buses, motor ships, ferries) via the free Yandex.Raspisaniya API. Authorization — a free key (apikey query parameter), which is issued in the developer dashboard developer.tech.yandex.ru (the "Yandex.Raspisaniya" API).

kind: rasp_tool
name: rasp
description: Расписания транспорта Яндекса.
api_key_env: YANDEX_RASP_API_KEY   # или api_key: "..." (не рекомендуется)
lang: ru_RU                        # опц., язык ответов
Field Description
api_key / api_key_env free key (inline or env variable)
lang response language (default ru_RU)

Actions (the action argument):

Action API method What it does
station_list stations_list search stations by name (query) → name + code
search search routes A→B (from, to, date, transport_types)
schedule schedule station board (station, date, direction)
thread thread trip details (uid)
nearest_stations nearest_stations nearest stations to a point (lat, lng)
nearest_settlement nearest_settlement nearest settlement (lat, lng)

Stations can be specified either by name (resolved to a code automatically) or by a ready code yandex_code (for example s2000006). For search cities are resolved to a settlement code (city→city), for schedule — to a specific station (with priority given to an actual railway station).

{ "action": "station_list", "query": "Москва" }
{ "action": "search", "from": "Москва", "to": "Санкт-Петербург", "date": "2026-09-15", "transport_types": ["train"] }
{ "action": "schedule", "station": "Москва", "date": "2026-09-15", "direction": "departure" }
{ "action": "thread", "uid": "122S_15_2" }
{ "action": "nearest_stations", "lat": 55.75, "lng": 37.61 }

transport_types: plane, train, suburban, bus, water (by default — all). A mandatory condition of the API is to credit Yandex as the data source.


5.23 browser_tool

Headless browser. For sites that render via JavaScript (SPAs, infinite scroll, logins) — a regular HTTP fetch site_tool is not enough. browser_tool loads the page in Chromium, performs interactions and returns the result — either via a JS script executed inside the page ("internal scripting"), or via the rendered text.

The browser is looked for on the system, and if it is absent — it is downloaded automatically (chrome-headless-shell from Chrome-for-Testing; the same logic as OfficeCLI's PDF export).

Browser processes are reused (pool): each call opens a fresh isolated tab (CDP browser context) in a live Chromium and on completion returns the browser to the pool — repeated calls do not pay ~1–2 s for startup. The number processes is limited by the variable AGENT_OS_MAX_BROWSERS (default 4); excess calls wait for a free slot (up to 60 s, then an error).

kind: browser_tool
name: scrape_spa
description: Извлечь товары из JS-листинга.
parameters:
  type: object
  properties:
    url: { type: string }
  required: [url]

steps:                          # необязательные действия, по порядку
  - { type: goto, url: "{{ input.url }}" }
  - { type: wait, selector: ".product-card" }
  - { type: click, selector: "button.load-more" }
  - { type: type, selector: "#q", text: "{{ input.q }}" }
  - { type: check, selector: "input[name=agree]" }
  - { type: eval, script: "return document.title" }
  - { type: screenshot, name: "page.png" }
  - { type: pdf, name: "page.pdf" }

script: |                       # JS внутри страницы → итоговый результат (JSON)
  [...document.querySelectorAll('.product-card')].map(c => ({
    title: c.querySelector('h2')?.innerText,
    price: c.querySelector('.price')?.innerText,
  }))

timeout_secs: 30

Steps

type Fields Action
goto url navigate and wait for readyState
wait ms or selector pause or wait for a selector
click selector click the first matching element
type selector, text fill <input>/<textarea>/<select> and trigger events
check selector, checked? set checked on a checkbox/radio (default true)
eval script execute JS in the page (the result is saved)
screenshot name PNG screenshot → file
pdf name print to PDF → file

Scripting and result

The string fields of steps (url, selector, text) are MiniJinja templates over input. The JS in script / eval.script runs in the page context; the call's arguments are available as the global variable input (for example input.url).

Result: if script (or the last eval) returned a value, it goes into data and content (a string or pretty-JSON); otherwise the page text is returned. screenshot/pdf form downloadable blocks file in visual; an array of objects is additionally rendered as a table.

5.24 web_search

🔵 Internet search via a provider (FireCrawl by default). The engine is supplied by the module searchers (a linked crate or DLL); the core registers kind: web_search автоматически, когда модуль включён. Демо-агент searchers-demo comes bundled.

Supported providers: firecrawl (default), tavily, brave, serper, serpapi, exa, google (CSE), searxng (self-hosted, no key). Each has its own key/environment variable — the full table is on the page Module searchers.

Call parameters (set by the LLM):

Parameter Required Description
query yes search query
limit no number of results, 1–20 (default 5)
provider no provider (default firecrawl)

Config:

kind: web_search
name: web_search
description: Поиск в интернете (title, url, snippet).
# provider: firecrawl            # firecrawl|tavily|brave|serper|serpapi|exa|google|searxng
# api_key: fc-...                # явный ключ (не хранить в git)
# api_key_env: FIRECRAWL_API_KEY # по умолчанию — своя для провайдера
# base_url: https://api.firecrawl.dev
# cx: <google-cse-id>            # только для provider: google
# limit: 5

The key is resolved in the order: api_key from the config → the variable api_key_env (by default, its own for each provider: FIRECRAWL_API_KEY, TAVILY_API_KEY, BRAVE_API_KEY, SERPER_API_KEY, SERPAPI_API_KEY, EXA_API_KEY, GOOGLE_API_KEY). searxng requires no key. The key can also be set via the module settings — the value is projected into the corresponding variable without a restart.

The result is JSON: { provider, query, count, results: [ { title, url, description } ] }.

The module, the demo agent, and details are on the page Module searchers.