usewrit-app Datasets API

Unified read-only catalogue over everything that has accumulated extracted data — crawl and workflow origins alike, keyed by the same id (`source_type` tells them apart). Records/export/search accept `?format=json|csv|markdown|html`; a non-json format returns rendered TEXT instead of the JSON envelope, and markdown/html are CONTENT-AWARE (a crawl's pages render as documents, structured data as a table). Mirrors the cloud `/api/v1/datasets/*` surface (tagged `Public API · Datasets`), so a dataset reads the same locally or forwarded to the fleet.

Operations 6

GET /v1/datasets The unified dataset catalogue #
GET /v1/datasets/search Global full-text search across every dataset #
GET /v1/datasets/{id} Dataset metadata + inferred schema #
GET /v1/datasets/{id}/records Page through a dataset's records #
GET /v1/datasets/{id}/export Download a dataset's full records #
GET /v1/datasets/{id}/search Full-text search within one dataset #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/usewrit-app:usewrit-app-datasets-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

usewrit-app-datasets-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Writ Agent Datasets API
  version: 0.1.0
  license:
    name: MIT
    identifier: MIT
  description: 'Loopback HTTP API of the Writ local agent (`writ-agentd`), the daemon embedded in the

    Writ desktop app and the self-host build. This document is generated against the Rust

    daemon source (`writ-agent/src/local/`), which is the wire truth.


    ## Transport


    - Plain HTTP on `127.0.0.1:8131` (default; `WRIT_PORT` env overrides).

    - Optional HTTPS twin on `127.0.0.1:8132`, served with a certificate minted by a

    per-install, name-constrained local CA (`~/.writ/tls/ca.pem`). Clients must trust that

    CA file (or pin it) to use the TLS port; the payloads are identical on both ports.

    - The daemon enforces a DNS-rebind guard: `Host`/`Origin` must be loopback. Talk to

    `127.0.0.1`, not `localhost`.


    ## Errors


    Domain errors are JSON `{"error": "", "code": ""}` (see the

    `Error` schema). Codes include `bad_request`, `unauthorized`, `forbidden`, `not_found`,

    `vault_locked` (HTTP 423), `too_many_requests`, `device_capacity` (HTTP 409),

    `captcha_required` (HTTP 402), `internal`.


    **Caveat:** some 4xx bodies are `text/plain`, not JSON — axum framework rejections

    (e.g. a JSON body that fails to deserialize returns

    `Failed to deserialize the JSON body into the target type: missing field ''url''` as

    plain text with a 4xx status). Clients must tolerate non-JSON error bodies.


    ## List envelopes


    The wire is deliberately documented as-is (SDKs normalize; this spec does not):


    - `{"data": [...], "count": n}` — workflows, personas, persona runs, secrets, files, keys

    - `{"data": [...], "count": n, "total": n}` — runs

    - **bare JSON array** — monitors, monitor selectors, extractors, automations,

    `/v1/changes/recent`

    - bespoke objects — `/v1/monitors/{id}/changes`, `/v1/data`, the workflow-data table'
servers:
- url: http://127.0.0.1:8131
  description: Plain-HTTP loopback listener (default port; WRIT_PORT overrides).
- url: https://127.0.0.1:8132
  description: HTTPS twin of the same API, terminated with a certificate from the per-install local CA at ~/.writ/tls/ca.pem. Trust that CA (or pin it) in your HTTP client.
security:
- bearerAuth: []
tags:
- name: Datasets
  description: Unified read-only catalogue over everything that has accumulated extracted data — crawl and workflow origins alike, keyed by the same id (`source_type` tells them apart). Records/export/search accept `?format=json|csv|markdown|html`; a non-json format returns rendered TEXT instead of the JSON envelope, and markdown/html are CONTENT-AWARE (a crawl's pages render as documents, structured data as a table). Mirrors the cloud `/api/v1/datasets/*` surface (tagged `Public API · Datasets`), so a dataset reads the same locally or forwarded to the fleet.
paths:
  /v1/datasets:
    get:
      operationId: listDatasets
      tags:
      - Datasets
      summary: The unified dataset catalogue
      description: 'Every data source that has accumulated extracted data, framed as a dataset: local data-bearing workflows plus (on a cloud-linked daemon) forwarded cloud crawl datasets, merged into one list. `source_type` is `crawl` for a whole-site crawl, otherwise `workflow`; `origin` marks where the dataset lives. Read a dataset''s rows with `GET /v1/datasets/{id}/records`.'
      responses:
        '200':
          description: Catalogue entries under a `datasets` key (NOT the `{data, count}` envelope).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatasetList'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/datasets/search:
    get:
      operationId: searchDatasets
      tags:
      - Datasets
      summary: Global full-text search across every dataset
      description: 'Full-text search over every local dataset at once, backed by the SQLite FTS5 index (not a bounded scan), so it stays fast and does not miss older data. Terms are space-separated, ANDed, case-insensitive and prefix-matched; a record must contain EVERY term. Results come back newest-first, each tagged with the dataset it came from and a `highlight` snippet. Extracted data covers both JSON fields and markdown (a crawl page is a record with a `markdown` field), so both are searched.


        A non-json `format` renders the matches as TEXT instead of the JSON envelope — the per-result `dataset` tag and `highlight` snippet exist only in the JSON shape. `truncated` flags that the candidate-run cap (500) was hit.'
      parameters:
      - $ref: '#/components/parameters/DatasetSearchQuery'
      - $ref: '#/components/parameters/DatasetSearchLimit'
      - $ref: '#/components/parameters/DatasetSearchOffset'
      - $ref: '#/components/parameters/DatasetFormat'
      responses:
        '200':
          $ref: '#/components/responses/DatasetSearchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/datasets/{id}:
    parameters:
    - $ref: '#/components/parameters/DatasetId'
    get:
      operationId: getDataset
      tags:
      - Datasets
      summary: Dataset metadata + inferred schema
      description: 'One dataset''s metadata and inferred schema: its columns, per-column `facets` (inferred type, non-empty count, numeric min/max, a distinct pick-list when low-cardinality), total `row_count`, the `run_count` behind it, and `truncated` when the run-scan cap bounded the answer.


        On a cloud-linked daemon a crawl dataset that is absent locally FORWARDS to the cloud dataset, and the forwarded body is the cloud facets shape — which also carries `collections` (the nested array paths you can pivot to). A purely local dataset does not return `collections`.'
      responses:
        '200':
          description: The dataset's metadata + schema.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatasetMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/datasets/{id}/records:
    parameters:
    - $ref: '#/components/parameters/DatasetId'
    get:
      operationId: getDatasetRecords
      tags:
      - Datasets
      summary: Page through a dataset's records
      description: 'The dataset''s records as one sortable / searchable / filterable table (the flat grid). A run that extracted a LIST contributes one record per item; each record carries the run it came from and, with `include_inputs`, that run''s inputs as `input.` columns. Identical query semantics to `GET /v1/workflows/{id}/data` (`view=all`).


        A non-json `format` serves THIS SAME PAGE rendered as text — pagination still applies, so use `/v1/datasets/{id}/export` for the whole set. markdown/html are content-aware: a crawl''s pages render as documents, structured data as a table.


        On a cloud-linked daemon a crawl dataset absent locally forwards to the fleet and the response is re-keyed to this same shape.'
      parameters:
      - $ref: '#/components/parameters/DatasetQ'
      - $ref: '#/components/parameters/DatasetFilter'
      - $ref: '#/components/parameters/DatasetFilters'
      - $ref: '#/components/parameters/DatasetSortBy'
      - $ref: '#/components/parameters/DatasetSortDir'
      - name: limit
        in: query
        description: Page size (clamped to 1..=500; default 50).
        schema:
          type: integer
          default: 50
      - name: offset
        in: query
        schema:
          type: integer
          default: 0
      - $ref: '#/components/parameters/DatasetIncludeInputs'
      - $ref: '#/components/parameters/DatasetCollection'
      - $ref: '#/components/parameters/DatasetFormat'
      responses:
        '200':
          description: The records page — the JSON envelope for `format=json` (the default), or the rendered page as TEXT for any other format.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatasetRecords'
            text/csv:
              schema:
                type: string
                description: '`format=csv` — RFC-4180 CSV of this page.'
            text/markdown:
              schema:
                type: string
                description: '`format=markdown` — content-aware: documents when the records carry a long-form content column, otherwise a table.'
            text/html:
              schema:
                type: string
                description: '`format=html` — the markdown render as HTML. Scraped third-party content is escaped and link schemes allowlisted; served `nosniff` under a script-less CSP.'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/datasets/{id}/export:
    parameters:
    - $ref: '#/components/parameters/DatasetId'
    get:
      operationId: exportDataset
      tags:
      - Datasets
      summary: Download a dataset's full records
      description: 'Downloads the dataset''s full (search / sort / filter applied, UN-paginated) records, bounded by the run-scan cap, framed as an attachment. The bytes are identical to `GET /v1/workflows/{id}/data/export`, which this delegates to.


        NOTE the default here is `csv`, NOT `json` — the one place the dataset surface departs from the `format=json` default of every other dataset read. markdown/html are content-aware: a crawl''s pages download as readable documents, structured data as a table.'
      parameters:
      - $ref: '#/components/parameters/DatasetExportFormat'
      - $ref: '#/components/parameters/DatasetQ'
      - $ref: '#/components/parameters/DatasetFilter'
      - $ref: '#/components/parameters/DatasetFilters'
      - $ref: '#/components/parameters/DatasetSortBy'
      - $ref: '#/components/parameters/DatasetSortDir'
      - $ref: '#/components/parameters/DatasetIncludeInputs'
      - $ref: '#/components/parameters/DatasetCollection'
      responses:
        '200':
          description: 'The exported records as a download (`Content-Disposition: attachment`).'
          content:
            text/csv:
              schema:
                type: string
                description: '`format=csv` (the default) — RFC-4180 CSV; run-meta columns then data columns.'
            application/json:
              schema:
                type: array
                description: '`format=json` — one object per record.'
                items:
                  type: object
            text/markdown:
              schema:
                type: string
                description: '`format=markdown` — content-aware documents or a table.'
            text/html:
              schema:
                type: string
                description: '`format=html` — the markdown render as safe HTML.'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/datasets/{id}/search:
    parameters:
    - $ref: '#/components/parameters/DatasetId'
    get:
      operationId: searchDataset
      tags:
      - Datasets
      summary: Full-text search within one dataset
      description: 'The same full-text find as `GET /v1/datasets/search` — same terms semantics, same ranking, same result shape — scoped to this dataset''s id.


        On a cloud-linked daemon a crawl dataset absent locally forwards the query to the fleet''s data table; that forwarded body is the cloud table shape, and `offset` is not forwarded.'
      parameters:
      - $ref: '#/components/parameters/DatasetSearchQuery'
      - $ref: '#/components/parameters/DatasetSearchLimit'
      - $ref: '#/components/parameters/DatasetSearchOffset'
      - $ref: '#/components/parameters/DatasetFormat'
      responses:
        '200':
          $ref: '#/components/responses/DatasetSearchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  parameters:
    DatasetSearchLimit:
      name: limit
      in: query
      description: Page size over the ranked hits (clamped to 1..=200; default 50).
      schema:
        type: integer
        default: 50
    DatasetIncludeInputs:
      name: include_inputs
      in: query
      description: Also surface run input values as `input.<name>` columns.
      schema:
        type: boolean
        default: false
    DatasetQ:
      name: q
      in: query
      description: Global substring filter across extracted fields (and inputs, when included).
      schema:
        type: string
    DatasetSearchQuery:
      name: q
      in: query
      required: true
      description: Space-separated keywords — ANDed, case-insensitive, prefix-matched; a record must contain EVERY term. A query with no usable terms returns an empty result set rather than an error.
      schema:
        type: string
    DatasetSearchOffset:
      name: offset
      in: query
      schema:
        type: integer
        default: 0
    DatasetExportFormat:
      name: format
      in: query
      description: Output shape, defaulting to `csv` on this route (every other dataset read defaults to `json`). `csv`/`json` return the tabular records; `markdown`/`html` render them and are CONTENT-AWARE — a crawl's pages export as readable documents, structured data as a table. An unknown value is a 400.
      schema:
        type: string
        enum:
        - csv
        - json
        - markdown
        - html
        default: csv
    DatasetSortBy:
      name: sort_by
      in: query
      description: A data column, `input.<name>`, or `run_at` / `status` / `duration_ms` / `run_id`.
      schema:
        type: string
    DatasetCollection:
      name: collection
      in: query
      description: Pivot to a nested array path (e.g. `posts.items`), one row per item. ACCEPTED BUT IGNORED by the daemon — nested collections are scoped client-side by the desktop app, and only the cloud `/api/v1/datasets/*` twin pivots server-side. Passing it here changes nothing rather than erroring.
      schema:
        type: string
    DatasetFilters:
      name: filters
      in: query
      description: Structured smart filters — a JSON array of clauses, e.g. `[{"col":"price","op":"between","min":10,"max":50}]`.
      schema:
        type: string
    DatasetId:
      name: id
      in: path
      required: true
      description: Dataset id — the same id as the workflow/crawl that produced the data (`GET /v1/datasets` lists them).
      schema:
        type: integer
        format: int64
    DatasetFormat:
      name: format
      in: query
      description: 'Output shape. `json` (the default) returns the documented JSON envelope; ANY other value returns rendered TEXT instead — `csv` as RFC-4180 CSV, and `markdown`/`html` CONTENT-AWARE: a dataset whose records carry a long-form content column (a crawl''s pages carry `markdown`) renders as DOCUMENTS — one section per record, heading + source link + body — while anything else renders as a TABLE mirroring the CSV columns. An unknown value is a 400, never a silent fall back to JSON.'
      schema:
        type: string
        enum:
        - json
        - csv
        - markdown
        - html
        default: json
    DatasetFilter:
      name: filter
      in: query
      description: Per-column filter `column:substring` (repeatable).
      schema:
        type: array
        items:
          type: string
      explode: true
    DatasetSortDir:
      name: sort_dir
      in: query
      schema:
        type: string
        enum:
        - asc
        - desc
        default: desc
  schemas:
    Dataset:
      type: object
      description: One entry in the unified dataset catalogue.
      required:
      - id
      - name
      - source_type
      - run_count
      - last_updated
      properties:
        id:
          type: integer
          format: int64
          description: The id of the workflow/crawl that produced the data.
        name:
          type:
          - string
          - 'null'
        source_type:
          type: string
          description: '`crawl` for a whole-site crawl dataset, otherwise `workflow`.'
          enum:
          - crawl
          - workflow
        run_count:
          type: integer
          description: Distinct successful runs that contributed at least one record.
        last_updated:
          type:
          - string
          - 'null'
          description: Newest contributing run's timestamp.
        origin:
          type: string
          description: Where the dataset lives — present on a cloud-linked daemon, which merges forwarded cloud crawl datasets into this list.
    DatasetRecords:
      type: object
      description: '`GET /v1/datasets/{id}/records` with `format=json` — a page of the dataset''s records. Reuses the workflow-data row shape.'
      required:
      - dataset
      - columns
      - declared
      - records
      - total
      - scanned_runs
      - truncated
      - limit
      - offset
      properties:
        dataset:
          $ref: '#/components/schemas/DatasetRef'
        columns:
          type: array
          items:
            type: string
        declared:
          type: boolean
          description: The columns come from the workflow's declared output fields.
        records:
          type: array
          items:
            $ref: '#/components/schemas/DataTableRow'
        total:
          type:
          - integer
          - 'null'
          description: Matching records before pagination.
        scanned_runs:
          type:
          - integer
          - 'null'
        truncated:
          type: boolean
        limit:
          type:
          - integer
          - 'null'
        offset:
          type:
          - integer
          - 'null'
    DataTableRow:
      type: object
      description: 'One table row: run-meta at the top level, data columns nested under `fields`. Lens views (`view=latest|run`) add a `lineage` sibling (`uid`, `change`, `changed_fields` — present only when change is `changed` —, `changed_leaf_count`, `first_seen_at`, `last_seen_at`, `versions`, `source`, and `prev_run_id` on run views).'
      required:
      - run_id
      - run_at
      - status
      - record_index
      - fields
      properties:
        run_id:
          type: integer
          format: int64
        run_at:
          type:
          - string
          - 'null'
        status:
          type:
          - string
          - 'null'
        record_index:
          type: integer
        fields:
          type: object
          description: The extracted data columns (redacted; a column absent for a row is null).
          additionalProperties: true
        lineage:
          type: object
          description: Lens views only.
          additionalProperties: true
    DatasetSearchResult:
      type: object
      description: '`GET /v1/datasets/search` and `GET /v1/datasets/{id}/search` with `format=json`.'
      required:
      - query
      - terms
      - results
      - total
      - truncated
      - scanned_runs
      properties:
        query:
          type: string
          description: The query as received (trimmed).
        terms:
          type: array
          description: The parsed terms — ANDed, case-insensitive, prefix-matched.
          items:
            type: string
        results:
          type: array
          description: Matched records, newest-first.
          items:
            $ref: '#/components/schemas/DatasetSearchHit'
        total:
          type: integer
          description: Matching records before pagination.
        truncated:
          type: boolean
          description: The candidate-run cap (500) was hit, so older matches may be missing.
        scanned_runs:
          type:
          - integer
          - 'null'
    DatasetSearchHit:
      type: object
      description: One matched record, tagged with the dataset it came from.
      required:
      - dataset
      - run_id
      - run_at
      - fields
      - highlight
      properties:
        dataset:
          $ref: '#/components/schemas/DatasetRef'
        run_id:
          type:
          - integer
          - 'null'
          format: int64
        run_at:
          type:
          - string
          - 'null'
        fields:
          type: object
          description: The matched record's extracted columns.
          additionalProperties: true
        highlight:
          description: A snippet around the first matching term, or null when none could be built (a forwarded cloud search never carries one).
          oneOf:
          - type: object
            required:
            - field
            - snippet
            properties:
              field:
                type: string
                description: The column the snippet was taken from.
              snippet:
                type: string
          - type: 'null'
    DatasetRef:
      type: object
      description: The dataset a record page or search hit belongs to.
      required:
      - id
      - name
      - source_type
      properties:
        id:
          type:
          - integer
          - 'null'
          format: int64
        name:
          type:
          - string
          - 'null'
        source_type:
          type: string
          enum:
          - crawl
          - workflow
    DatasetMeta:
      type: object
      description: A dataset's metadata + inferred schema. A cloud crawl dataset forwarded by a linked daemon returns the cloud facets shape, which additionally carries `collections`.
      required:
      - id
      - name
      - source_type
      - columns
      - facets
      - row_count
      - run_count
      - truncated
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        source_type:
          type: string
          enum:
          - crawl
          - workflow
        columns:
          type: array
          items:
            type: string
        facets:
          type: object
          description: Per-column facet descriptors (inferred type, non-empty count, numeric min/max, distinct pick-list when low-cardinality).
          additionalProperties: true
        collections:
          type: array
          description: Nested array paths (e.g. `posts.items`) you can pivot to with `collection`. Only on a FORWARDED cloud dataset — a local dataset omits this key.
          items:
            type: string
        row_count:
          type: integer
        run_count:
          type: integer
          description: Runs actually scanned to build this schema.
        truncated:
          type: boolean
          description: The run-scan cap bounded the answer.
    Error:
      type: object
      description: 'The daemon''s domain error body. NOTE: some 4xx responses (axum framework rejections) are `text/plain` instead of this JSON shape. Certain codes attach extra fields (e.g. `device_capacity` adds `would_use_units_per_min` / `budget_units_per_min`; `captcha_required` adds `captcha_type` / `url`).'
      required:
      - error
      - code
      properties:
        error:
          type: string
          description: 'Human-readable message (e.g. `not found: workflow 999999`).'
        code:
          type: string
          description: 'Stable machine code: `bad_request`, `unauthorized`, `forbidden`, `not_found`, `vault_locked`, `too_many_requests`, `captcha_required`, `device_capacity`, `cloud_unreachable`, `db_error`, `io_error`, `json_error`, `internal`, …'
      additionalProperties: true
    DatasetList:
      type: object
      description: '`GET /v1/datasets` — a bespoke `{datasets}` envelope, NOT `{data, count}`.'
      required:
      - datasets
      properties:
        datasets:
          type: array
          items:
            $ref: '#/components/schemas/Dataset'
  responses:
    NotFound:
      description: The addressed resource does not exist.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing/invalid bearer token, or the Origin/Host guard rejected the request.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    DatasetSearchResponse:
      description: The matches — the JSON envelope for `format=json` (the default), or the matches rendered as TEXT for any other format. The per-result `dataset` tag and `highlight` snippet exist only in the JSON shape.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/DatasetSearchResult'
        text/csv:
          schema:
            type: string
            description: '`format=csv` — RFC-4180 CSV of the matched records.'
        text/markdown:
          schema:
            type: string
            description: '`format=markdown` — content-aware: matched documents render as documents, structured matches as a table.'
        text/html:
          schema:
            type: string
            description: '`format=html` — the markdown render as safe HTML.'
    BadRequest:
      description: 'Invalid input. Domain validation errors use the JSON Error shape; axum body-deserialize rejections are `text/plain` with a bare message (e.g. `Failed to deserialize the JSON body into the target type: missing field ''url''`).'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
        text/plain:
          schema:
            type: string
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Every route (including `/v1/agent` and `/v1/health`) requires\n`Authorization: Bearer <token>`. Token families:\n\n- `wlt_…` — the runtime token from `runtime.json` (full access, used by the desktop\n  UI and SDK discovery).\n- `wlk_…` — a scoped API key minted via `POST /v1/keys` (CSV scopes `read|run|admin`;\n  `/v1/keys` itself requires the full-access token).\n- `wlo_…` — an OAuth 2.1 access token from the daemon's local authorization server\n  (`run` scope).\n\nTreat the token as an opaque string. The daemon additionally enforces a loopback\nOrigin/Host DNS-rebind guard.\n"