Skills

How to compile a reusable browser skill

Turn a working run into a named, versioned skill that replays with no model call — including the three token families that decide whether the skill is reusable or frozen to one account.

A skill is the unit of reuse: a named, versioned action path distilled from a successful run, with the parts that varied lifted out so it serves every customer, date or record rather than only the one it was compiled on. This guide covers compiling one, reading its contract, replaying it, and the mistake that produces a skill which technically runs and does the wrong thing forever.

What is a browser skill?

It is a compiled program: the ordered actions that accomplished a goal, minimized to drop the retries and dead ends, addressed by element index rather than by selector, with the variable parts replaced by tokens. Because it is deterministic, replaying it makes no planner call — it executes the recorded path against the live page.

How do I compile one?

POST /api/v1/skills. It takes either shape — { target, goal, as } for a registry goal, or { url, prompt, success, as } for free text — and `as` is what names the skill. A compile costs 50 credits, settled higher-of against the model cost of the discovery it ran. A successful compile also embeds the intent, which is what makes the skill reachable later through dispatch.

Compile a skillbash
curl -X POST https://twin-browser.com/api/v1/skills \
  -H "Authorization: Bearer $TWIN_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "url": "https://app.example.com/invoices",
    "prompt": "Export the statement for {{param:month}} as PDF",
    "success": { "kind": "urlIncludes", "value": "/invoices/export" },
    "as": "export-invoices"
  }'

# → the compiled skill descriptor, with its version and runId

The three token families — and the mistake to avoid

A skill is only account-independent when everything that varied is late-bound, and there are three kinds with three different owners. Get this wrong and you get a skill that runs perfectly and does the wrong thing every time — a "post a comment" skill that re-posts whatever sentence the compile run happened to type.

{{secret:NAME}} — owned by the vault

A credential. Stored write-only at POST /api/v1/secrets, resolved inside the browser at fill time, redacted from the returned path, the frames and the logs. Never appears in a prompt as a literal.

{{var:NAME}} — owned by your tenant

A per-tenant entity value: a profile URL, an account id. Captured after a run and forwarded on the next one, so it does not have to be passed by the caller.

{{param:NAME}} — owned by the caller

A call argument: the month to export, the text to post. Supplied per invocation in `params`. A replay with a param token still unresolved is REFUSED before it opens a browser, rather than run with a stale literal.

How do I know what a skill expects?

Read the catalog. GET /api/v1/catalog returns each compiled skill with the params it takes, the secrets its path fills, and whether the host's auth is ready for it — so an agent can plan a run before spending a credit, and you can see at a glance whether a skill is genuinely parameterized or quietly frozen.

Read the run contractbash
curl "https://twin-browser.com/api/v1/catalog?host=app.example.com" \
  -H "Authorization: Bearer $TWIN_API_KEY"

# {
#   "count": 1,
#   "skills": [
#     { "name": "export-invoices", "version": 3,
#       "params": ["month"],
#       "secrets": ["EXAMPLE_PASSWORD"],
#       "auth": { "mode": "session", "ready": true, "missing": [] } }
#   ]
# }

How do I replay it?

POST /api/v1/skills/{name}/run — the path parameter is the NAME you compiled it under, not an id — with a `target` and the params the catalog named. The replay costs 1 credit and makes no model call.

Replay deterministicallybash
curl -X POST https://twin-browser.com/api/v1/skills/export-invoices/run \
  -H "Authorization: Bearer $TWIN_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "target": "https://app.example.com/invoices",
        "params": { "month": "2026-05" } }'

# { "success": true, "steps": 4, "runId": "…",
#   "credits_charged": 1 }

What happens when the page changes?

A replay that no longer matches returns a failure and is refunded — it does not silently fall back to the planner, because a cheap deterministic path that quietly becomes an expensive one is a worse surprise than a failure. Route the goal through POST /api/v1/dispatch instead and a failed match falls through to a fresh compile, storing a new version under the same name.

Common questions

Do I address a skill by id or by name?
By name. The route is POST /api/v1/skills/{name}/run, where {name} is the value you passed as `as` at compile time. There is no id-based replay endpoint.
What happens if I forget a param?
The replay is refused before it opens a browser, naming the token it could not resolve. That is deliberate: substituting a default would silently re-run the compile-time literal, which is the failure mode this whole mechanism exists to prevent.
Can a param value contain a secret token?
No. Vault tokens are resolved server-side only, and a param value that tries to smuggle one in is rejected — otherwise a caller could reference someone else's vault entry through an argument.
Do I have to name a skill to reuse it?
No. POST /api/v1/dispatch matches your goal to a compiled skill by meaning and replays it, so you can keep sending goals in words and still get the replay price.

Delegate the work. Keep the decision.

Hand off a real task, set the guardrails, and let repeated work compile into a skill that replays deterministically at near-zero cost.