# 7 · 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.

```bash
rote init triage-outage --seq     # open the sandbox; you're now on the record
```

From that moment, every call any reach makes becomes a numbered, durable event:

```
@1   an API response        (adapter call)
@2   a process capture      (shell)
@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](08-workspace-and-caus.md)'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`.

![Three kinds of reach, one memory](assets/modalities.svg)

## 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:

```bash
rote adapter new github
```

The 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:

```bash
# probe: semantic search over the API's capabilities, by intent
github_probe   "list pull requests for a repository"

# call: execute the discovered operation
github_call    pulls.list  owner=modiqo repo=rote state=open
```

During 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:

```yaml
steps:
  ensure_auth:
    type: adapter.auth.ensure
    endpoint: adapter/github
    on_missing: authorize
    on_expired: refresh
  list_pulls:
    type: adapter/github          # endpoint: adapter/<installed-adapter-id>
    method: pulls.list
    params: { owner: modiqo, repo: rote, state: open }
    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:

```bash
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:

```bash
rote proc run gh pr list --json number,title
rote proc run python3 analyze.py --input data.csv
rote query @1 '.stdout.text | fromjson | .[0].title' -r
```

Two 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.

```bash
rote browse https://news.ycombinator.com
```

Exploration follows a mandatory chain — **navigate → wait → snapshot → slice** — never a raw page dump:

```bash
rote browse wait --text "login"        # settle first
rote browse snapshot                   # capture as a CAU
# then slice what you need: clickable | links | headings | forms | errors
```

Crystallized, browser work becomes typed browser steps:

```yaml
steps:
  open_page:
    type: browser.navigate
    url: https://example.com/status
  settle:
    type: browser.wait
    text: "All systems"
    timeout_ms: 15000
    depends_on: [open_page]
  read_status:
    type: browser.extract
    slice: headings
    limit: 10
    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:

1. **Installed adapter** — typed, authenticated, drift-fingerprinted. Best when it exists.
2. **Shell + public JSON** — a `curl`/CLI capture is often all a public endpoint needs.
3. **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.

---

**Next:** [8 · The workspace, CAUs, and the DAG underneath →](08-workspace-and-caus.md)
