---
title: Actions DSL
description: The selector + extractor + function language for hand-written scrapes.
---

The `actions` field is a tree. Leaves extract values; branches recurse into matched elements; `fn` nodes are imperative steps (mostly `browser`; see the matrix below).

CSS is the **default** selector syntax. Prefer bare selectors (`h1`, `a.cta`) — do not prefix with `css=` (accepted but redundant). Use `xpath=` for XPath. jQuery extensions (`:contains()`, `:eq()`, …) are **not** supported and throw at runtime.

## Selector syntax

```
h1                           → text of the first <h1> (default extractor: text)
a.primary@href               → absolutized href
xpath=//*[@id='x']@html      → outer HTML
"div[data-x='@']"@text       → quoting lets you put '@' inside the selector
```

Full string form: `[scheme=]<selector>@<extractor>`.

- **Scheme** — omit (`css` default) or `xpath=` / `text=`.
- **Selector** — standard CSS or XPath. Wrap in quotes if it contains `@`.
- **Extractor** — what to pull (see table). Defaults to `text`.

## Extractors

| Extractor | Returns | Notes |
|---|---|---|
| `text` / `textOriginal` | `string` | Visible text (`text` trims). |
| `html` / `innerHtml` | `string` | Outer / inner HTML. |
| `md` / `fitMd` | `string` | Element as Markdown; `fitMd` strips boilerplate harder. |
| `value` | `string \| null` | Input value. Browser engine. |
| `tagName` | `string` | e.g. `"div"`. |
| `cssSelector` | `string` | A CSS path for the matched element. |
| `json` / `json:<path>` | `unknown` | Parse text as JSON; optional path drill. See [JSON paths](#json-paths). |
| `attributes` | `Record<string, string>` | All DOM attributes. |
| `href` / `hrefOriginal` | `string` | Absolutized (`Original` = raw). Same pattern for `src` / `poster` / `srcset`. |
| `table` / `tableJson` / `tableArray` | rows | `options.headers = false` → 2D array. |
| `screenshot` | `string` | **Deterministic `browser` only** — public PNG URL by default (`returnType: 'dataUrl'` for base64). |
| _anything else_ | `string \| null` | Named DOM attribute via `getAttribute`. |

`context` / `ocr` / `htmlTree` are not supported in v2.

## Branches: `many`, `output`

```json
{
  "cards": {
    "selector": ".sf-card",
    "many": true,
    "output": {
      "title": "a.sf-title@text",
      "link": "a.sf-title@href",
      "points": ".sf-points"
    }
  }
}
```

- `many: true` — apply once per match → array.
- `many: false` (default) — first match only.
- Nested branches are **relative to the matched element**.

### Flat vs nested `output` (easy to get wrong)

| Form | Example | How the string is read |
|---|---|---|
| Flat string | `{ "selector": "a", "many": true, "output": "href" }` | `"href"` = **extractor name** on each match |
| Nested self | `{ "selector": "a", "many": true, "output": { "url": "@href", "label": "@text" } }` | Leading `@` = extractor on **this** match |
| Nested child | `{ "selector": "li", "many": true, "output": { "link": "a@href" } }` | Look up descendant `a` (**first** match only) |
| Nested list | `{ "output": { "tags": { "selector": "a.tag", "many": true, "output": "text" } } }` | Array of values inside each row |
| Nested bug (scalar) | `{ "output": { "tags": "a.tag@text" } }` | **First tag only** — not an array |
| Nested bug (null) | `{ "output": { "url": "href", "label": "text" } }` | CSS for `<href>`/`<text>` → **null** |

String nested leaves **never** return arrays. For list fields inside a row (tags, chips, multi-images), use object form + `many: true`.

Always model multi-field rows as **one** nested `many: true` action. Never parallel arrays (`titles[]` + `authors[]`) — they desync when a selector misses a row.

## JSON paths

| Form | Returns | Example |
|---|---|---|
| Dot / bracket | scalar | `json:items.0.name` |
| JSONPath (`*`, `..`, or `$…`) | **array** of matches | `json:items[*].name` |

### JSON-row projection

When `selector` ends in `@json:<path>` **and** `output` is an object, each row is projected. Leaf strings are **row-relative JSON paths** (no `@` — `@` means DOM extractors and is rejected here).

```jsonc
{
  "images": {
    "selector": "body@json:products[*].images[*]",
    "output": { "pos": "position", "src": "src" }
  }
}
```

## Function actions (`fn`)

```json
{
  "login": { "fn": "fill", "selector": "input[name=email]", "args": "ada@example.com" },
  "submit": { "fn": "click", "selector": "button[type=submit]" },
  "wait": { "fn": "wait", "args": 1500 }
}
```

Keys run top-to-bottom; `fn` nodes sequence side effects.

### html vs browser

| `fn` / extractor | `html` | `browser` |
|---|---|---|
| `wait` | Rejected — HTML engine, use `browser` | Yes |
| `goto` | Rejected | Yes |
| `back` | Rejected | Yes |
| `forward` | Rejected | Yes |
| `click` | Rejected | Yes |
| `fill` | Rejected | Yes |
| `selectOption` | Rejected | Yes |
| `2fa` | Rejected | Yes (`otpauth` only) |
| `evaluate` | Both — JSDOM `eval`, **sync only** (no async / Promise) | Page evaluate; async allowed, 60s timeout |
| `screenshot` extractor | Not supported — use `browser` | Yes (PNG URL, or `returnType: "dataUrl"`) |

Engine pick and request options: [Engines](/docs/engines).

## Options

Per-action `options`:

| Field | Meaning |
|---|---|
| `timeout` | Per-action ms (caps apply). |
| `filter` | Regex; value must match. On flat `many`, drops non-matches. |
| `match` | Regex; return first capture group (or full match). |
| `maxChars` | Cap string length after match/filter (+head / −tail). |
| `headers` | `tableJson`: `false` → 2D array. |
| `excludeTags` / `includeTags` | `md` / `fitMd` / html strip lists. |
| `returnType` / `fullPage` | Screenshot only. |

Top-level request `options` (sibling of `actions`): `waitFor`, `timeoutMs`, `resolution`, `headless`, `blockAds` — see [Engines](/docs/engines).

## End-to-end examples

Static list — `html` against `/fixtures/listing`:

```json
{
  "url": "https://scrapesilo.com/fixtures/listing",
  "engine": "html",
  "actions": {
    "cards": {
      "selector": ".sf-card",
      "many": true,
      "output": {
        "title": "a.sf-title@text",
        "link": "a.sf-title@href",
        "points": ".sf-points"
      }
    }
  }
}
```

Interaction — `browser` against `/fixtures/form` (`fill` + `click`, then extract):

```json
{
  "url": "https://scrapesilo.com/fixtures/form",
  "engine": "browser",
  "actions": {
    "type": { "fn": "fill", "selector": "#sf-q", "args": "widget" },
    "submit": { "fn": "click", "selector": "#sf-submit" },
    "title": ".sf-result-title",
    "count": ".sf-result-count"
  }
}
```

Write `actions` yourself. For interpretation (outline, summary, “is X present?”), use [`POST /analyse`](/docs/api/analyse) — not scrape, and not a `query` field on `/scrape`.

## Copy for an agent

```
ScrapeSilo actions DSL (POST /scrape). Required: url, engine ("html"|"browser"), actions. No query. No engine "ai".

Rules:
- CSS is the default selector syntax. Write h1, not css=h1. Use xpath= only when needed. No jQuery (:contains, :eq) — they throw.
- String leaf: "selector@extractor" (default extractor: text). Example: a.sf-cta@href
- Rows: one nested { selector, many: true, output: { ... } }. Never parallel arrays (titles[] + authors[]).
- List field inside a row needs object form + many: true. A string leaf is first-match only.
- Extractor on the matched element itself needs a leading @ in nested output ({ "url": "@href" }). Bare "href" is CSS for a <href> tag → null.
- html fn limits: wait/goto/back/forward/click/fill/selectOption/2fa are rejected. evaluate is sync JSDOM only. screenshot extractor is browser only.
- Interpretation (outline, summary) → POST /analyse, not scrape.

html (static list):
{
  "url": "https://scrapesilo.com/fixtures/listing",
  "engine": "html",
  "actions": {
    "cards": {
      "selector": ".sf-card",
      "many": true,
      "output": {
        "title": "a.sf-title@text",
        "link": "a.sf-title@href",
        "points": ".sf-points"
      }
    }
  }
}

browser (fill + click):
{
  "url": "https://scrapesilo.com/fixtures/form",
  "engine": "browser",
  "actions": {
    "type": { "fn": "fill", "selector": "#sf-q", "args": "widget" },
    "submit": { "fn": "click", "selector": "#sf-submit" },
    "title": ".sf-result-title",
    "count": ".sf-result-count"
  }
}
```
