# Why a form should never submit twice

Kaabist · 10 October 2026

Retrying is the right answer to most failures in web automation, and the wrong one for a submit. Kaabist fences every step that changes something: it records that it is about to act, acts, and records that it did. A job that finds an open fence never acts again; it stops and asks you.

Most failures in web automation have the same cure: try again. A page loaded slowly, a request timed out, a worker restarted. Run the step once more and it usually works.

There is one kind of step where that cure is the disease.

## The step you cannot take back

Some steps change something in the world. They file an insurance claim, place an order, send a message, pay an invoice. Call them side effects.

Now picture the moment it goes wrong. The program clicks **Submit**. The browser sends the request. Then the worker running the job disappears: the machine is replaced, the network drops, the process runs out of memory. Nobody saw the confirmation page.

Did the claim go through? Nobody knows. The website might have received it, or not. And the automation's default instinct, *retry*, is the one answer that can turn one claim into two.

Duplicates are expensive in exactly the places people automate most: insurance portals, government forms, supplier orders, payments. A duplicate claim is a phone call, a reversal, sometimes a penalty. Worse, it is often discovered weeks later.

## Fences

Kaabist handles this with a fence. You mark the step:

```json
{
  "click": { "role": "button", "name": "Submit claim" },
  "side_effect": true,
  "expect": { "visible": { "text": "Claim received" } }
}
```

Before the click, the engine writes down that it is **about to execute** this step. After the click, once the page shows what `expect` asks for, it writes down that the step was **executed**. Both records are saved outside the browser, so they survive whatever happens to it.

If a job is resumed and finds a fence that says *about to execute* but never *executed*, it does not click again. It cannot know what happened, so it does not guess. It pauses with the reason `side_effect_ambiguous` (it cannot tell whether the step went through) and asks you, or the teammate you chose, to look at the portal and decide.

Three more rules follow from the same idea:

- A fenced step is never retried automatically, and it cannot be optional.
- Restarting from a checkpoint can never go back past a side effect that already happened.
- If a job is stopped while a fence is open, it waits for you to decide instead of being put back in the queue.

## Your side of the wire

The fence protects the website from the job. Your code also needs protecting from the network between you and Kaabist: you send a request to start a job, the connection drops, and you do not know whether the job was created.

That is what an idempotency key is for. Send the same `Idempotency-Key` with the same request and Kaabist returns the job it already created instead of starting another one:

```
curl https://api.kaab.ist/v1/jobs \
  -H "Authorization: Bearer $KAAB_API_KEY" \
  -H "Idempotency-Key: claim-0042" \
  -d '{"program": "file-claim@production", "inputs": {"claim_id": "0042"}}'
```

Together, the two make the whole path once-only: your request creates one job, and that job presses the button at most once.

## Testing without filing

A fence also gives you a clean way to test. A dry run goes through every step up to the first fenced one and stops just before it. You see that the program logs in, fills the form and reaches the button, and nothing is filed. Coding agents that build programs through Kaabist's MCP server test this way by default.

## Once, or you decide

Retrying is how automation becomes reliable. Refusing to retry, in exactly the right place, is how it becomes trustworthy. A submit happens once, or you decide what happens next.

