Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Writ Agent Data 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: Data
description: Extracted-data explorer — aggregate tables, facets, exports, lineage runs.
paths:
/v1/data:
get:
operationId: listDataWorkflows
tags:
- Data
summary: Data-explorer workflow picker
description: Every workflow that has produced extracted data in a SUCCESSFUL run, with a contributing-run count, last-data timestamp and a small `last_delta` summary (null for large or single-snapshot datasets). Sorted by most recent data first.
responses:
'200':
description: Picker entries under a `workflows` key.
content:
application/json:
schema:
$ref: '#/components/schemas/DataWorkflowsList'
'401':
$ref: '#/components/responses/Unauthorized'
/v1/workflows/{id}/data:
parameters:
- $ref: '#/components/parameters/WorkflowId'
get:
operationId: getWorkflowData
tags:
- Data
summary: Aggregated extracted-data table
description: 'Aggregates the workflow''s successful runs'' `extracted_data` into one sortable / searchable table. `view=all` (default) is the flat grid; `view=latest` is the deduplicated current dataset (adds `identity`, `counts`, `sources`, per-row `lineage`); `view=run&run_id=N` is one snapshot annotated vs the previous chain member (adds `delta`, `removed_records`, `prev_run_id`). Lens-parameter violations return a FastAPI-shaped 400 body `{"detail": "..."}` (not the standard Error shape).'
parameters:
- name: q
in: query
description: Global substring filter across extracted fields.
schema:
type: string
- name: filter
in: query
description: Per-column legacy filter `column:substring` (repeatable).
schema:
type: array
items:
type: string
explode: true
- 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
- name: sort_by
in: query
description: A data column or `run_at` / `status` / `duration_ms` / `run_id` (lens views also accept `first_seen_at` / `last_seen_at` / `versions`).
schema:
type: string
- name: sort_dir
in: query
schema:
type: string
enum:
- asc
- desc
default: desc
- 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
- name: include_inputs
in: query
description: Also surface run input values as `input.<name>` columns (view=all only).
schema:
type: boolean
default: false
- name: view
in: query
description: 'Lens: `latest` | `run` | `all` (default `all`).'
schema:
type: string
enum:
- all
- latest
- run
default: all
- name: run_id
in: query
description: The snapshot to show — required when `view=run`.
schema:
type: integer
format: int64
- name: key
in: query
description: Explicit identity key — comma-separated field names (identity pinning echo).
schema:
type: string
- name: include_missing
in: query
description: view=latest only. Include records missing from the newest snapshot. Parsed STRICTLY — only the literal string `true` enables it.
schema:
type: string
- name: source
in: query
description: 'view=latest/run: only records from this originating list key (empty string selects untagged records).'
schema:
type: string
responses:
'200':
description: The aggregated data table (view-dependent extras included).
content:
application/json:
schema:
$ref: '#/components/schemas/WorkflowDataTable'
'400':
description: 'Lens-parameter violation. NOTE the body is the FastAPI-parity shape `{"detail": "..."}`, not the standard Error object.'
content:
application/json:
schema:
type: object
required:
- detail
properties:
detail:
type: string
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
delete:
operationId: deleteWorkflowData
tags:
- Data
summary: Delete extracted-data rows
description: Removes extracted rows from the workflow's runs. The delete set is the union of the explicit `records` refs and every stored version of each uid in `record_uids`. The run rows themselves are preserved — only their `extracted_data` is mutated.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DeleteWorkflowDataRequest'
responses:
'200':
description: Deletion summary.
content:
application/json:
schema:
$ref: '#/components/schemas/DeleteWorkflowDataResult'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
/v1/workflows/{id}/data/runs:
get:
operationId: listWorkflowDataRuns
tags:
- Data
summary: Data snapshot index
description: Every successful data-bearing run (newest first) with record count, explicit-empty flag and delta vs the previous chain member (null for the oldest), plus the chosen identity.
parameters:
- $ref: '#/components/parameters/WorkflowId'
- name: key
in: query
description: Explicit identity key — comma-separated field names.
schema:
type: string
responses:
'200':
description: The snapshot index.
content:
application/json:
schema:
$ref: '#/components/schemas/DataRunsIndex'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
/v1/workflows/{id}/data/facets:
get:
operationId: getWorkflowDataFacets
tags:
- Data
summary: Per-column facets
description: Per-column facets over the workflow's extracted data (inferred type, non-empty count, numeric min/max, distinct pick-list when low-cardinality). `view=latest|run` computes over exactly that lens's rowset and adds a `sources` envelope.
parameters:
- $ref: '#/components/parameters/WorkflowId'
- name: include_inputs
in: query
schema:
type: boolean
default: false
- name: view
in: query
schema:
type: string
enum:
- all
- latest
- run
default: all
- name: run_id
in: query
schema:
type: integer
format: int64
- name: key
in: query
schema:
type: string
- name: include_missing
in: query
description: Strict `true` includes missing records in the view=latest rowset.
schema:
type: string
- name: source
in: query
schema:
type: string
responses:
'200':
description: Facets over the (lens) rowset.
content:
application/json:
schema:
$ref: '#/components/schemas/DataFacets'
'400':
description: Lens-parameter violation (FastAPI-shaped `detail` body).
content:
application/json:
schema:
type: object
required:
- detail
properties:
detail:
type: string
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
/v1/workflows/{id}/data/export:
get:
operationId: exportWorkflowData
tags:
- Data
summary: Export the extracted-data table
description: Downloads the full (search/sort/filter applied, UN-paginated) table as CSV (`?format=csv`, default) or JSON (`?format=json`), bounded by the 1000-run scan cap. Lens views (`view=latest|run`) attach lineage — extra CSV columns, or a `_lineage` object on each JSON row. Accepts the same query parameters as the table endpoint (`q`, `filter`, `filters`, `sort_by`, `sort_dir`, `view`, `run_id`, `key`, `include_missing`, `source`) plus `format`.
parameters:
- $ref: '#/components/parameters/WorkflowId'
- name: format
in: query
description: Output shape. `csv` (default) or `json` return the tabular data; `markdown` and `html` render it and are CONTENT-AWARE — a crawl's pages (records carrying a long-form `markdown` column) export as readable documents, structured data as a table.
schema:
type: string
enum:
- csv
- json
- markdown
- html
default: csv
- name: q
in: query
schema:
type: string
- name: filter
in: query
schema:
type: array
items:
type: string
explode: true
- name: filters
in: query
schema:
type: string
- name: sort_by
in: query
schema:
type: string
- name: sort_dir
in: query
schema:
type: string
enum:
- asc
- desc
default: desc
- name: view
in: query
schema:
type: string
enum:
- all
- latest
- run
default: all
- name: run_id
in: query
schema:
type: integer
format: int64
- name: key
in: query
schema:
type: string
- name: include_missing
in: query
schema:
type: string
- name: source
in: query
schema:
type: string
responses:
'200':
description: The exported table as a CSV or JSON attachment.
content:
text/csv:
schema:
type: string
description: RFC-4180 CSV; run-meta columns, data columns, then lineage columns (lens views).
application/json:
schema:
type: array
description: One object per row; lens views add a `_lineage` object per row.
items:
type: object
'400':
description: Lens-parameter violation (FastAPI-shaped `detail` body).
content:
application/json:
schema:
type: object
required:
- detail
properties:
detail:
type: string
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
components:
parameters:
WorkflowId:
name: id
in: path
required: true
description: Workflow row id.
schema:
type: integer
format: int64
schemas:
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
DataRunsIndex:
type: object
description: The snapshot index (`GET /v1/workflows/{id}/data/runs`).
required:
- runs
- identity
- scanned_runs
- truncated
properties:
runs:
type: array
description: Newest-first successful data-bearing runs; each entry carries `run_id`, record count, explicit-empty flag, per-snapshot delta (null for the oldest) and the run's `status`.
items:
type: object
additionalProperties: true
identity:
type: object
description: The chosen record identity.
additionalProperties: true
scanned_runs:
type: integer
truncated:
type: boolean
DeleteWorkflowDataResult:
type: object
required:
- deleted
- resolved
- unmatched
properties:
deleted:
type: integer
description: Number of extracted rows removed.
resolved:
type: object
description: '`{uid: n_versions}` for each matched uid.'
additionalProperties:
type: integer
unmatched:
type: array
description: Requested uids that matched nothing.
items:
type: string
WorkflowDataTable:
type: object
description: The aggregated extracted-data table. The base fields are always present; lens views add `identity` (+ `counts`/`sources` on latest, + `delta`/`removed_records`/ `prev_run_id`/`sources` on run views).
required:
- workflow_id
- workflow_name
- columns
- declared
- rows
- total
- scanned_runs
- truncated
- limit
- offset
properties:
workflow_id:
type: integer
format: int64
workflow_name:
type: string
columns:
type: array
items:
type: string
declared:
type: boolean
description: Whether the columns come from the workflow's declared output fields.
rows:
type: array
items:
$ref: '#/components/schemas/DataTableRow'
total:
type: integer
description: Post-filter row total before pagination.
scanned_runs:
type: integer
description: Successful data-bearing runs scanned (capped at 1000).
truncated:
type: boolean
description: True when the scan hit the 1000-run cap.
limit:
type:
- integer
- 'null'
offset:
type: integer
identity:
type: object
description: Lens views only — the chosen record identity.
additionalProperties: true
counts:
type: object
description: view=latest only — post-dedup change counts.
properties:
new:
type: integer
changed:
type: integer
same:
type: integer
missing:
type: integer
sources:
type: object
description: 'Lens views only — `{<list key | "">: count}` buckets of the rowset by originating list key (pre-source-filter).'
additionalProperties:
type: integer
delta:
type: object
description: view=run only — the snapshot delta vs the previous chain member.
additionalProperties: true
removed_records:
type: array
description: view=run only — records that vanished vs the previous snapshot.
items:
type: object
required:
- uid
- fields
properties:
uid:
type: string
fields:
type: object
additionalProperties: true
prev_run_id:
type:
- integer
- 'null'
format: int64
description: view=run only — the previous chain member (null for the oldest).
DataFacets:
type: object
description: Per-column facets (`GET /v1/workflows/{id}/data/facets`).
required:
- workflow_id
- columns
- facets
- row_count
- scanned_runs
- truncated
properties:
workflow_id:
type: integer
format: int64
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
row_count:
type: integer
scanned_runs:
type: integer
truncated:
type: boolean
sources:
type: object
description: Lens views only — pre-filter source buckets.
additionalProperties:
type: integer
DataWorkflowSummary:
type: object
required:
- workflow_id
- workflow_name
- run_count
- last_data_at
- last_delta
properties:
workflow_id:
type: integer
format: int64
workflow_name:
type: string
run_count:
type: integer
description: Distinct successful runs that contributed at least one row.
last_data_at:
type:
- string
- 'null'
description: Newest contributing run's timestamp.
last_delta:
description: Newest two snapshots compared under the sampled identity; null when the dataset has fewer than 2 snapshots or is too large.
oneOf:
- type: object
required:
- new
- changed
- removed
properties:
new:
type: integer
changed:
type: integer
removed:
type: integer
- type: '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
DataWorkflowsList:
type: object
description: The Data-explorer workflow picker payload.
required:
- workflows
properties:
workflows:
type: array
items:
$ref: '#/components/schemas/DataWorkflowSummary'
DeleteWorkflowDataRequest:
type: object
description: Body for `DELETE /v1/workflows/{id}/data`. The delete set is `records` ∪ every stored version of each uid in `record_uids`. An empty body deletes nothing (200 with zeros).
properties:
records:
type: array
items:
type: object
required:
- run_id
properties:
run_id:
type: integer
format: int64
record_index:
type: integer
default: 0
record_uids:
type: array
description: Lineage uids — each expands to EVERY stored version.
items:
type: string
key:
type:
- string
- 'null'
description: Identity pinning echo — the identity fields the uids came from.
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'
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"