Modalities: how steps reach the world
Where you are in the journey: you've built plays whose steps run local processes. But the work you'll want to crystallize lives everywhere — behind APIs, on your machine, and on web pages with no API at all. This section introduces the product's three kinds of reach, and the routing instinct for choosing between them.
The product phrase is "three kinds of reach and one memory". Before meeting the three reaches, meet the one memory they all share.
First, the workspace: a trace sandbox
A workspace is where every reach lands. Think of it as a trace sandbox: an isolated recording surface that persists every event of a working turn — each API call, each process you run, each page you visit — as it happens, in order, with nothing scrolling away.
rote init triage-outage --seq # open the sandbox; you're now on the recordFrom that moment, every call any reach makes becomes a numbered, durable event:
01@1 an API response (adapter call)02@2 a process capture (shell)03@3 a page snapshot (browser)Three properties make this a sandbox rather than a log:
- Isolated — one workspace per task. Events from your outage triage don't mingle with yesterday's release work; each canvas is its own addressing space, archived independently.
- Persistent — the events are Context Addressable Units: immutable once created, queryable forever (
rote query @2 '.field' -r), surviving the end of the session and the agent's context window alike. The turn's thinking may be ephemeral; the turn's evidence is not. - Uniform — an API response, a process capture, and a page snapshot are peers. Whatever reach produced an event, downstream work holds a reference to it (
@N), never a pasted payload.
That last property is why the modalities below compose so freely: the workspace doesn't care how you reached the world, only what came back. And because later events reference earlier ones by @N{.field}, the sandbox's trace is silently accumulating the dataflow graph your play will crystallize from (that's section 8's story).
The product's own metaphor: "a rote workspace is a flight recorder." Everything below writes into it.
The three reaches
All three write into that same workspace, where their events sit side by side as @1, @2, @3.
Reach 1 · APIs — adapters
An adapter connects rote to any service with a machine-readable description: OpenAPI, GraphQL, gRPC, Google Discovery, or a live MCP server. There's a catalog of 870+ known APIs, so most services are one command away:
rote adapter new githubThe mental shift from MCP-as-you-know-it, in the product's words:
MCP servers say "here's a process that wraps an API." Adapters say "here's an API — the spec is the server."
Every adapter exposes the probe/call pattern — search first, never load everything:
01# probe: semantic search over the API's capabilities, by intent02github_probe "list pull requests for a repository"03 04# call: execute the discovered operation05github_call pulls.list owner=modiqo repo=rote state=openDuring exploration you drive probe/call from the workspace; each call lands as a CAU. When you crystallize, adapter calls become adapter steps in the DAG:
01steps:02 ensure_auth:03 type: adapter.auth.ensure04 endpoint: adapter/github05 on_missing: authorize06 on_expired: refresh07 list_pulls:08 type: adapter/github # endpoint: adapter/<installed-adapter-id>09 method: pulls.list10 params: { owner: modiqo, repo: rote, state: open }11 depends_on: [ensure_auth]One publishing rule to know early: a play whose steps call an adapter must declare it in requires_endpoints — that field is the only input to adapter installation for whoever pulls your play, and the registry refuses a push that omits it.
Cross-service discovery, when you don't know which adapter you need:
rote explore "send a message to a slack channel"Reach 2 · Shell — recorded processes
You've been using this one since section 3. rote proc run executes a local process and captures it — stdout, stderr, exit status — as a CAU instead of terminal scrollback:
01rote proc run gh pr list --json number,title02rote proc run python3 analyze.py --input data.csv03rote query @1 '.stdout.text | fromjson | .[0].title' -rTwo properties make this more than a fancy $(...):
- A policy engine classifies risk and blocks destructive commands before they spawn — while still recording the attempt.
- The capture is queryable forever. A 40k-token command output doesn't re-enter anyone's context window; you read fields out of it by reference.
In a crystallized play, shell work becomes the process.exec steps you already know from section 6 — including the exit contract: the child's exit status is the DAG's failure signal, stdout is data. And because steps have no TTY, any subcommand that would prompt needs its consent flag (--yes) passed explicitly inside the step.
Reach 3 · Browser — drive the web
For the parts of the world with no API: the browser reach drives the web, including attaching to your own logged-in browser session. Login walls, CAPTCHAs, and human-approval moments are detected and surfaced rather than fumbled.
rote browse https://news.ycombinator.comExploration follows a mandatory chain — navigate → wait → snapshot → slice — never a raw page dump:
01rote browse wait --text "login" # settle first02rote browse snapshot # capture as a CAU03# then slice what you need: clickable | links | headings | forms | errorsCrystallized, browser work becomes typed browser steps:
01steps:02 open_page:03 type: browser.navigate04 url: https://example.com/status05 settle:06 type: browser.wait07 text: "All systems"08 timeout_ms: 1500009 depends_on: [open_page]10 read_status:11 type: browser.extract12 slice: headings13 limit: 1014 depends_on: [settle](browser.click and browser.type exist for interactions, targeting elements by snapshot ref or visible text.)
The routing instinct: lightest first
When a task could be reached more than one way, the product's substrate router rule is lightest-first:
- Installed adapter — typed, authenticated, drift-fingerprinted. Best when it exists.
- Shell + public JSON — a
curl/CLI capture is often all a public endpoint needs. - Headless browse — the heaviest reach; reserve it for pages that genuinely have no API.
Choose per reading, not per play — a single play can probe an API, run a local tool, and read one stubborn web page, each as its own step. The workspace doesn't care: every reach lands the same way, and the crystallizer compiles them into one DAG.
A real example of the choice mattering: modiqo/whale-flow-monitor originally drove two MCP adapters. Both merely wrapped public REST APIs — so its steps now call the endpoints directly, which dropped the adapter setup requirement entirely and exposed a rotted endpoint path the adapter had been hiding. Lightest-first isn't just cheaper; it's more honest.