Intent programs

For AI agents: this page is Markdown at https://squiggle.sh/docs/agents/programs.md. Every docs page is listed at https://squiggle.sh/llms.txt, and all of them are one file at https://squiggle.sh/llms-full.txt.

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.

TaskSign in and check the balance

  1. A typical agent loop

    Screenshot, think, act, one action at a time.

    5model turns

  2. Squiggle, first run

    One program for the whole journey. The model is woken again only for a decision, like a popup.

    1model turn

  3. Squiggle, every run after

    The verified steps were saved beside the flow, so they replay with no model at all.

    0model turns

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:

intent program
intent: check out with a saved cardgoal:  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:

Terminal
squiggle agent validate checkout.yaml✔ 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:

AnswerDoes
pickUse one of the ranked candidates.
patchRun these steps instead.
retryTry the same step again.
skipMove on to the next step.
abortStop 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:

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