# Intent programs

Most agents drive an app one action at a time: look, think, tap, and look again. With Squiggle your agent writes the whole journey once, as a program. Squiggle runs it and asks the model again only when something needs a decision.

## What a program looks like

A program’s steps read like a flow’s. It adds a goal that must hold at the end, and the points where the agent wants to be asked:

```yaml
intent: check out with a saved card
goal:
  route: OrderConfirmation
  visible: { text: "Order placed" }
plan:
  - tap: { testID: cart }
  - scrollUntilVisible: { element: { testID: checkout }, direction: down }
  - tap: { testID: checkout }
  - branch:
      if: { visible: { text: "Add a card" } }
      then:
        - escalate: { reason: "no saved card on this account", want: decision }
      else:
        - tap: { testID: pay-now }
  - waitFor: { route: OrderConfirmation, timeout: auto }
```

Your agent sends it with `run_intent`. The run fails unless the `goal` holds afterwards, however the steps went. `timeout: auto` takes its wait from how long this screen change has taken before, instead of a number the model guessed.

## Checked before anything is tapped

Before the run touches the app, Squiggle checks every selector and screen against what it has seen of your app, and checks the program’s assertions against the newest recording that covers them. You can run the same check yourself:

```shell
squiggle agent validate checkout.yaml
```

Output:

```text
✔ PASS: reached static
▌ static selector {"testID":"pay-now"} is not in screen memory — guard it with a branch+escalate, or explore first
▌ static route 'OrderConfirmation' has never been observed — the plan may be aiming at a screen that does not exist
```

A program aimed at a screen that doesn’t exist stops here, with nothing tapped. When no recording covers the program, the check says it could only look at the selectors, rather than passing quietly. Screens you declared in `.squiggle/interfaces/<route>.json` count as seen, so a program written before its screen is built still passes this check. A recording that never went to a screen says nothing about it: when the program asserts on a screen the recording never reached, and your app, a declaration or an earlier visit knows that screen, the check doesn’t block. A screen nothing knows still blocks the run. Your agent can also send several candidate programs at once; the best one runs and the others are kept as fallbacks.

## When the run needs a decision

At an `escalate` step, or when a step can’t continue, the run stops and sends your agent a packet: where it stopped and why, what changed on screen since the start, the controls that might be the one it meant, ranked, and the latest logs. The agent answers with one of:

| Answer | Does |
| --- | --- |
| `pick` | Use one of the ranked candidates. |
| `patch` | Run these steps instead. |
| `retry` | Try the same step again. |
| `skip` | Move on to the next step. |
| `abort` | Stop the run, with a reason. |

Each candidate carries `terms`, the points each selector field earned against that control, and `short`, the points it falls below the match threshold of 60. Together they say which field to change. When a wait timed out because the screen never settled, the packet’s `settleBlockers` names the component still animating and the line that renders it.

With any answer, the agent can teach a popup handler (`learnInterrupt`): next time that popup appears, Squiggle dismisses it without asking.

While a run is going, `steer` adds a hint the run must follow. If the server stops mid-run, `intent_continue` picks the run up from where it was, and answers the decisions already made without asking the model again.

## Try several endings from one point

`branch` takes a program, a step number `at` and a list of `alternatives`, each with its own `goal`. It runs the first `at` steps, then each alternative from that same point, one after another, and ranks them by whether they reached their goal, then by time. The call returns when every branch has run.

By default, Squiggle relaunches the app and replays the first steps before every branch. With `fromSlot: true`, it runs them once, saves a slot there, and loads that slot before each branch instead, which takes about half a second rather than a full replay. The steps before `at` must end on a known screen: include a `waitFor` or `assert` on a route. A slot holds the navigation state and your stores, not state kept inside a component. The result names the `slot` it saved, so you can re-run one branch alone, or gives `slotFallback`, the reason it replayed instead.

To pay for fewer replays, pass `order: 'margin'` and `firstWin: true`. The alternatives then run likeliest first: each one's first tap is scored against the controls Squiggle last saw on the branch screen, and the one that most surely matches a control there goes first. The sweep stops at the first branch that reaches its goal and lists the rest in `notRun`. Each branch reports its `margin`. If Squiggle has not yet seen the branch screen with its controls, or the first steps don't end on a known screen, the branches run in the order you gave and `orderNote` says why.

From the terminal, `squiggle agent branch` runs the same sweep, with each alternative in its own program file:

```shell
squiggle agent branch checkout.yaml --at 4 --alt pay-card.yaml --alt pay-wallet.yaml --from-slot --order margin --first-win
```

It exits 0 when an alternative reaches its goal. See the [CLI reference](https://squiggle.sh/docs/reference/cli) for every flag.

## Cheaper the more you use it

- **Popups are learned once.** A handler taught on one run works on every later run.
- **Journeys go in the library.** Save a program that worked with `library_save`, and running it again costs no model calls.
- **Screens are remembered.** Every run records which tap led to which screen and how long it took. `route` gives the shortest known path to a screen, and `predict` says what a tap has led to before, without touching the app. Give `predict` a `horizon` of up to 10 taps and it also says where the app has usually ended up that many taps on, with the share of recorded paths behind each screen. `what_if` says what a flow did under a slow or failing network, a repeated tap or Back during loading, from the runs already recorded.
- **Answers are reused.** A decision made on the same screen, for the same kind of problem, on the same build, is answered from the record. It still goes through the deny rail and the checks.

## Answer it yourself

A program run from Studio’s Flows screen shows each decision in the run log, and you can answer it there: pick a candidate, retry, skip, abort, or approve a step the deny rail held. Studio never calls a model itself.

> **Programs and agentic flows**
>
> An [agentic flow](https://squiggle.sh/docs/agentic-flows)’s `agent:` step reaches your agent the same way, as a decision to drive. Its answer is a patch of ordinary steps, and once they pass they become the flow’s recording.
