Skip to content

Adapter Format (v2)

The canonical reference for writing Uni-CLI adapters. 90% of adapters are YAML; the TypeScript escape hatch covers the remaining 10% where pipeline primitives are insufficient.

Principles

  1. YAML first. If the command is expressible as a finite pipeline over typed steps, write YAML. The system validates, migrates, and self-repairs YAML in ways it cannot do for arbitrary TypeScript.
  2. Agent-editable. Keep adapters under ~50 lines of YAML whenever possible. Agents read and patch these files during self-repair.
  3. Deterministic. The pipeline must be reproducible given the same inputs and upstream state. No wall-clock randomness, no hidden subprocess state.
  4. Minimum capability. Declare the smallest transport surface the command needs (http.fetch, not cua.anything). Dispatchers use this to route calls safely.

Table of Contents

  1. YAML schema (default path)
  2. TypeScript escape hatch
  3. Schema-v2 required fields
  4. Strategy → transport migration
  5. Full examples
  6. Troubleshooting

YAML schema (default path)

A minimum viable YAML adapter:

yaml
site: example
name: top
description: "List top items from example.com"
type: web-api
transport: http
strategy: public
capabilities: [fetch, map, limit]
minimum_capability: http.fetch
trust: public
confidentiality: public
quarantine: false

pipeline:
  - fetch:
      url: "https://example.com/api/top"
  - select: "data.items"
  - map:
      id: "${{ item.id }}"
      title: "${{ item.title }}"
  - limit: 20

columns: [id, title]

All fields

FieldRequiredTypeNotes
siteyesstringAdapter site/service key, kebab-case. Used as unicli SITE CMD.
nameyesstringCommand name; unique per site.
descriptionrecommendedstringOne-line description for unicli list and for agents.
typeoptionalweb-api | browser | bridge | desktop | serviceOmit for implicit web-api. Historical; the transport field is the v2 source of truth.
transportyes (v2)http | cdp-browser | subprocess | desktop-ax | desktop-uia | desktop-atspi | cuaThe runtime dispatcher key.
strategyoptionalpublic | cookie | header | intercept | uiKept for 1 release as alias; see migration table below.
capabilitiesyes (v2)string[]List of pipeline step names this command may invoke (e.g. [fetch, map, limit]).
minimum_capabilityyes (v2)stringSingle dispatcher capability required (e.g. http.fetch, cdp-browser.navigate).
trustyes (v2)public | user | systemProvenance trust level. Default for committed YAML: public.
confidentialityyes (v2)public | internal | privateData sensitivity. Default: public. Auth-required adapters: internal or private.
quarantineyes (v2)booleanIf true, command is skipped by unicli test and marked [quarantined] in unicli list until repaired.
argsoptionallistNamed + positional command arguments.
pipelineyeslist of step objectsOrdered sequence of pipeline steps.
columnsrecommendedstring[]Default column order for the md / csv formatters.
rate_limitoptionalobjectPer-domain token bucket config; see src/engine/steps/rate-limit.ts.

Args

yaml
args:
  - { name: query, type: string, required: true, positional: true }
  - { name: limit, type: int, default: 20 }
  - { name: sort, type: string, default: "hot" }

Types: string, int, float, bool, string[]. Positional args populate ${{ args.NAME }} in pipeline templates. Named flags become --NAME VALUE on the command line.

Pipeline steps

Pipeline steps are documented in docs/reference/pipeline.md. The most common ones:

StepTransportPurpose
fetchhttpHTTP request with retry, cookie injection, JSON parse.
fetch_texthttpHTTP request returning raw text (RSS, HTML).
selectpureJSONPath-style navigation into the response.
mappureTransform each item via template with ${{ item.x }}.
filterpureKeep items matching a predicate.
sortpureSort by field.
limitpureCap result count.
navigatecdp-browserNavigate a Chrome page via CDP.
interceptcdp-browserCapture matching XHR/fetch responses.
execsubprocessRun a subprocess with stdin/env/timeout.
snapshotcdp-browser + desktop-ax + cuaDOM/AX tree snapshot with ref numbers.
cua_clickcuaCoordinate-level click via CUA backend.

Each step has a typed schema; unknown fields are rejected at load time.


TypeScript escape hatch

Use TS when:

  • The command needs control flow that YAML pipelines cannot express (multi-phase retries with domain-specific backoff, cursor-based pagination with server-computed tokens, stateful protocols like OAuth dance).
  • The adapter wraps a library that is easier to call than to shell out.
  • You need a typed response shape exported for downstream consumers.

Do not use TS because "it's faster to write" — YAML is the shared vocabulary the self-repair loop and unicli migrate understand.

typescript
// src/adapters/example/complex.ts
import { cli, Strategy } from "../../registry.js";

cli({
  site: "example",
  name: "complex",
  description: "Paginated search with server-computed cursors",
  strategy: Strategy.COOKIE,
  args: [
    { name: "query", required: true, positional: true },
    { name: "max_pages", type: "int", default: 5 },
  ],
  func: async (page, kwargs) => {
    const results: unknown[] = [];
    let cursor: string | undefined;
    for (let i = 0; i < kwargs.max_pages; i++) {
      const res = await fetch(`https://example.com/search`, {
        method: "POST",
        body: JSON.stringify({ q: kwargs.query, cursor }),
      });
      const json = await res.json();
      results.push(...json.hits);
      cursor = json.next_cursor;
      if (!cursor) break;
    }
    return results;
  },
});

Required schema-v2 metadata for TS adapters

Because TS adapters cannot be statically analysed, the cli({...}) call must still carry the v2 metadata fields so they appear in manifests and the CI lint:

typescript
cli({
  site: "example",
  name: "complex",
  capabilities: ["fetch"],
  minimum_capability: "http.fetch",
  trust: "public",
  confidentiality: "public",
  quarantine: false,
  // ...
});

Omitting these is a build error under npm run lint:adapters.


Schema-v2 required fields

Schema v2 requires five metadata fields for CI safety, capability routing, and honest provenance:

FieldWhy it existsDefault when absent
capabilitiesDeclares the step surface. Used by dispatchers and by unicli lint to ensure only declared steps are used.Inferred from pipeline by unicli migrate schema-v2.
minimum_capabilitySmallest transport the dispatcher must support.http.fetch for legacy YAML (safe baseline).
trustProvenance — is this adapter first-party, community, or user-supplied?public for committed YAML.
confidentialityData-sensitivity label. Drives dispatcher routing when --confidentiality flag is set.public unless strategy is cookie | header | intercept | ui (in which case the migrator sets internal).
quarantineSkip this command in CI until repaired. Set by unicli repair when the command has failed three times.false.

Migration is automated: unicli migrate schema-v2 reads all committed YAML, fills defaults deterministically, and writes back. The migration tool is idempotent — re-running it on a migrated file is a no-op.

Strategy → transport migration

The old strategy field mapped to a mix of transport and auth concerns. In v2, the concerns split: transport routes the call, strategy stays only as a short-lived alias for the auth hint.

Old strategyNew transportNew auth hint (kept in strategy for 1 release)
publichttppublic
cookiehttpcookie
headerhttpheader
interceptcdp-browserintercept
uicdp-browserui
(new) desktopdesktop-ax | desktop-uia | desktop-atspinone
(new) cuacuanone

The unicli migrate tool handles the split. For a hand-written adapter, set both fields explicitly and do not rely on legacy alias inference.


Full examples

1. YAML, simplest case (web-api, public)

yaml
# src/adapters/hackernews/top.yaml
site: hackernews
name: top
description: "Hacker News front page — top stories right now"
type: web-api
transport: http
strategy: public
capabilities: [fetch, select, map, limit]
minimum_capability: http.fetch
trust: public
confidentiality: public
quarantine: false

args:
  - { name: limit, type: int, default: 30 }

pipeline:
  - fetch:
      url: "https://hacker-news.firebaseio.com/v0/topstories.json"
  - limit: "${{ args.limit }}"
  - parallel:
      max: 10
      map:
        - fetch:
            url: "https://hacker-news.firebaseio.com/v0/item/${{ item }}.json"
  - map:
      rank: "${{ index + 1 }}"
      title: "${{ item.title }}"
      score: "${{ item.score }}"
      author: "${{ item.by }}"
      comments: "${{ item.descendants }}"
      url: "${{ item.url }}"

columns: [rank, title, score, author, comments, url]

2. TypeScript escape hatch (OAuth dance)

typescript
// src/adapters/notion/search.ts
import { cli } from "../../registry.js";

cli({
  site: "notion",
  name: "search",
  description: "Search Notion workspace (requires OAuth)",
  capabilities: ["fetch"],
  minimum_capability: "http.fetch",
  trust: "public",
  confidentiality: "private", // accesses user workspace
  quarantine: false,
  args: [{ name: "query", required: true, positional: true }],
  func: async (_page, { query }) => {
    const token = await ensureNotionOAuthToken();
    const res = await fetch("https://api.notion.com/v1/search", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
        "Notion-Version": "2022-06-28",
      },
      body: JSON.stringify({ query, page_size: 20 }),
    });
    const json = (await res.json()) as { results: unknown[] };
    return json.results;
  },
});

3. YAML with intercept (browser capture)

yaml
# src/adapters/twitter/timeline.yaml
site: twitter
name: timeline
description: "Home timeline via XHR intercept"
type: browser
transport: cdp-browser
strategy: intercept
capabilities: [navigate, intercept, select, map, limit]
minimum_capability: cdp-browser.intercept
trust: public
confidentiality: private # user's timeline
quarantine: false

args:
  - { name: limit, type: int, default: 20 }

pipeline:
  - navigate:
      url: "https://twitter.com/home"
  - intercept:
      pattern: "/graphql/.*/HomeTimeline"
      wait: 8000
  - select: "data.home.home_timeline_urt.instructions[0].entries"
  - filter: "${{ item.entryId startsWith 'tweet-' }}"
  - map:
      id: "${{ item.content.itemContent.tweet_results.result.rest_id }}"
      text: "${{ item.content.itemContent.tweet_results.result.legacy.full_text }}"
      author: "${{ item.content.itemContent.tweet_results.result.core.user_results.result.legacy.screen_name }}"
  - limit: "${{ args.limit }}"

columns: [id, author, text]

4. YAML, quarantined (example of the CI gate)

yaml
# src/adapters/example/broken.yaml
site: example
name: broken
description: "Endpoint retired 2026-03-01; pending repair"
type: web-api
transport: http
strategy: public
capabilities: [fetch, map]
minimum_capability: http.fetch
trust: public
confidentiality: public
quarantine: true
quarantineReason: "HTTP 404 since upstream deprecated /v1/feed; needs /v2 port"

pipeline:
  - fetch:
      url: "https://example.com/api/v1/feed"
  - map:
      id: "${{ item.id }}"
      title: "${{ item.title }}"

columns: [id, title]

Quarantined commands show up in unicli list with a [quarantined] tag, are skipped by unicli test, and emit an informative error envelope when invoked directly. The quarantineReason is free-form text shown in unicli doctor.

5. YAML, CUA pipeline (computer-use agent transport)

yaml
# src/adapters/figma/click-through.yaml
site: figma
name: click-through
description: "Click a canvas element by natural-language description"
type: browser
transport: cua
strategy: ui
capabilities: [cua_snapshot, cua_click, cua_wait, cua_ask, assert]
minimum_capability: cua.snapshot
trust: public
confidentiality: internal
quarantine: false

args:
  - { name: target, type: string, required: true, positional: true }
  - { name: backend, type: string, default: "anthropic" }

pipeline:
  - cua_backend:
      name: "${{ args.backend }}"
  - cua_snapshot: {}
  - cua_ask:
      prompt: "Find the UI element matching: ${{ args.target }}"
      returns: { ref: string }
  - cua_click:
      ref: "${{ vars.ref }}"
  - cua_wait:
      for: "idle"
      timeout: 3000
  - assert:
      url_contains: "/edited"

columns: [ok, url]

CUA commands are expensive because screenshot and LLM inference cost scales with backend complexity. Do not default to CUA when a cdp-browser adapter is feasible; the dispatcher will warn if a CUA command could have been expressed as intercept.


Troubleshooting

SymptomLikely causeFix
Unknown step: ... at load timeStep name typo or step not registeredCheck src/engine/steps/ for supported names.
Adapter schema v2 violation: missing minimum_capabilityPre-v2 YAML not migratedRun unicli migrate schema-v2 src/adapters/SITE/CMD.yaml.
Forbidden: step X not in capabilitiesPipeline uses a step not declared in capabilitiesAdd the step name to capabilities, or drop the step.
Strategy 'legacy-xyz' unknownTypo or dropped strategyConsult the Strategy migration table; legacy-xyz is probably replaced by a named transport.
HTTP 403 Forbidden on cookie strategyCookie file staleunicli auth setup SITE to re-authenticate; run unicli repair SITE/CMD for directed patch suggestions.
Interception timed out after 8000msSelector/URL pattern changed upstreamInspect page network panel, update the pattern in intercept step, re-run.
cua_backend: anthropic not configuredANTHROPIC_API_KEY unset or CUA backend sidecar not runningunicli daemon status to check; export ANTHROPIC_API_KEY=....
quarantine: true but command still tries to runYou called the command directly; quarantine only affects CI + unicli testRemove quarantine once repaired, or run unicli repair SITE/CMD to auto-patch + lift the flag.
trust: user adapter refuses to runCI safety gate rejects user-trust adapters when UNICLI_TRUST_FLOOR=publicSet trust: public and get the file reviewed, or run with UNICLI_TRUST_FLOOR=user locally.

For anything not on this list, unicli doctor prints the relevant diagnostics. If that does not resolve the issue, open an issue with the full stderr envelope — the adapter_path / step / action / suggestion fields are what we need to help.


See also

  • docs/reference/pipeline.md — full step catalog with examples.
  • src/core/schema-v2.ts — the authoritative Zod schema.
  • src/adapters/ — committed adapters to read as working examples.

Document version: v2. Last updated 2026-04-25.

Released under the Apache-2.0 License