> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sente.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Runs

> Start a run, follow it to a terminal state, and read every status and error code it can end in.

Everything Sente does to an account happens in a real browser, and each attempt is a **run**. Creating an account, logging back into one, connecting one you already own — each starts a run, and the run is the object you watch. It tells you when the account is ready, when a person is needed, and why it stopped when it stopped.

You never create a run directly. Each account operation returns one:

| Type       | Started by                                                                     | What it does                                                                                                           |
| ---------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `register` | `POST /v1/registrations` · `sente register` · `registrations.register()`       | Signs up for a new account under the identity's email and completes email verification from that inbox.                |
| `login`    | `POST /v1/registrations/:id/login` · `sente relogin` · `registrations.login()` | Re-authenticates an existing account. Also queued automatically when you ask for a session whose login has gone stale. |
| `connect`  | `POST /v1/connections` · `sente connect` · `connections.connect()`             | Logs in to an account **you** own using the credentials you supplied.                                                  |

<Note>
  **One live run per identity.** Starting a second operation on an identity that already has a run in flight returns that in-flight run rather than queuing a competing one — so `register` and `connect` are safe to call again after a network blip.
</Note>

## What a run will not do

Stated up front, because it shapes how you design around runs:

* A **CAPTCHA**, an **SMS/phone step**, or a **payment form** stops the run. Sente does not work around any of them — the run goes `blocked` and pages a human with an interactive live view. See [Human takeover](/runs/human-takeover).
* A **second-factor code sent to your own email or phone** (connected accounts) stops the run the same way. A vaulted TOTP seed is the one MFA case that stays autonomous.
* Email verification for the account's *own* address is the part that is fully automatic: the code or link arrives in the identity's inbox, gets classified on arrival, and is applied inside the run. You never handle it.

## Lifecycle

```text theme={null}
queued ──► running ◄──► awaiting_verification ──► completed
              │
              ▼
           blocked ──resume──► running
              │
              └──(~10 min, nobody resumes)──► failed (BLOCKED_TIMEOUT)
```

| Status                  | Meaning                                                                                                                                                            |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `queued`                | Accepted, waiting for a worker slot. Runs are also gated by a global concurrency cap, so a burst queues instead of failing.                                        |
| `running`               | The browser agent is driving the app.                                                                                                                              |
| `awaiting_verification` | The app sent a verification email; Sente is completing verification from the account's own inbox. Times out after about 8 minutes → `failed VERIFICATION_TIMEOUT`. |
| `blocked`               | The run hit something only a person should do. The browser stays alive and `liveViewUrl` is interactive. Holds \~10 minutes.                                       |
| `completed`             | Terminal. The account row is `active` and `result` carries the agent's final report.                                                                               |
| `failed`                | Terminal. `error.code` says why; `error.detail` carries the agent's own words.                                                                                     |

`blocked` is a stop state, not a failure — the run keeps its browser session and waits for a person.

## Follow a run end to end

The complete pattern: start the operation, wait, handle the one case that needs a human, then use the account.

<CodeGroup>
  ```bash CLI theme={null}
  # Starts the run and waits for it (up to 10 minutes), printing each status change.
  # Exits non-zero unless the run completed. In an interactive terminal it first asks
  # who presses the final submit — pass --autonomous or --confirm-before-submit to skip.
  sente register https://app.example.com --autonomous

  # Or return immediately and follow it yourself:
  sente register https://app.example.com --no-wait   # → prints the run id
  sente run run_8d9e0f1a2b3c4d5e6f708192a3b4c5d6     # status, error, live-view URL
  sente run resume run_8d9e0f1a2b3c4d5e6f708192a3b4c5d6   # after a human cleared a block
  sente run abort  run_8d9e0f1a2b3c4d5e6f708192a3b4c5d6   # stop it
  ```

  ```ts TypeScript theme={null}
  import { Sente } from "@sente-labs/sdk";

  const sente = new Sente({ apiKey: process.env.SENTE_API_TOKEN! });

  // 1. Start it. No identityId — Sente provisions the backing email address itself.
  const { registration, run } = await sente.registrations.register({
    appUrl: "https://app.example.com",
  });

  // 2. Follow it. Returns on completed | failed | blocked. `timeout` is MILLISECONDS.
  let current = await sente.runs.waitForRun(run.id, { timeout: 300_000 });

  // 3. blocked = a person is needed. Sente never works around the gate.
  if (current.status === "blocked") {
    console.log(`needs a human (${current.error?.code}): ${current.liveViewUrl}`);
    // …a person clears the step in the live view, then:
    await sente.runs.resume(current.id);
    current = await sente.runs.waitForRun(current.id, { timeout: 300_000 });
  }

  // 4. Use the account.
  if (current.status === "completed") {
    const creds = await sente.registrations.getCredentials(registration.id);
    console.log("ready:", registration.appUrl, creds.username);
  } else {
    console.error("run", current.status, current.error?.code, current.error?.detail);
  }
  ```

  ```python Python theme={null}
  import os
  from sente import Sente

  sente = Sente(api_key=os.environ["SENTE_API_TOKEN"])

  # 1. Start it. No identity_id — Sente provisions the backing email address itself.
  registration, run = sente.registrations.register(app_url="https://app.example.com")

  # 2. Follow it. Returns on completed | failed | blocked. `timeout` is SECONDS.
  current = sente.runs.wait_for_run(run.id, timeout=300)

  # 3. blocked = a person is needed. Sente never works around the gate.
  if current.status == "blocked":
      print("needs a human (%s): %s" % (current.error["code"], current.live_view_url))
      # …a person clears the step in the live view, then:
      sente.runs.resume(current.id)
      current = sente.runs.wait_for_run(current.id, timeout=300)

  # 4. Use the account.
  if current.status == "completed":
      creds = sente.registrations.get_credentials(registration.id)
      print("ready:", registration.app_url, creds["username"])
  else:
      print("run", current.status, current.error)
  ```
</CodeGroup>

<Warning>
  `waitForRun` / `wait_for_run` never throw on a bad outcome. They return on `completed`, `failed`, or `blocked` — and on their own timeout they return the **last-polled** run, still `running` or `awaiting_verification`. Always branch on `run.status`; never assume the returned run succeeded. Note the unit difference: TypeScript takes milliseconds (default 180000), Python takes seconds (default 180).
</Warning>

## Push instead of poll

`run.completed`, `run.failed`, and `run.blocked` [webhooks](/events/webhooks) fire on the transition, so a server-side agent reacts without a polling loop. They are single-attempt: if your endpoint is down at that moment the notification is gone. Keep `GET /v1/runs/:id` as the backstop for anything you must not miss.

## Blocked codes

While `status` is `blocked`, `error.code` says which gate stopped the run and who is expected to clear it. Full flow: [Human takeover](/runs/human-takeover).

| `error.code`                   | What happened                                                                                            | What to do                                                                                                                               |
| ------------------------------ | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `CAPTCHA_REQUIRED`             | The page put a CAPTCHA in the way.                                                                       | A person completes it in the live view, then resume.                                                                                     |
| `PHONE_REQUIRED`               | The app wants a phone number or an SMS code. Sente has no phone channel.                                 | A person supplies it in the live view, or target an app that verifies by email.                                                          |
| `PAYMENT_REQUIRED`             | The flow asks for card details. Sente never enters payment data.                                         | A person enters it, or abandon the target.                                                                                               |
| `MISSING_FIELD`                | The form needs something the run wasn't given — company name, invite code, plan choice.                  | Fill it in the live view and resume.                                                                                                     |
| `SUBMIT_CONFIRMATION_REQUIRED` | You passed `confirmBeforeSubmit`: the form is filled and waiting for a human to press the final button.  | Review, click submit, resume.                                                                                                            |
| `MFA_REQUIRED`                 | Connected account: a second-factor code went to the owner's own email or phone, which Sente cannot read. | Enter the code in the live view and resume. For emailed codes, `verifyToIdentity` on [connect](/accounts/connect) makes this autonomous. |
| `TOTP_REQUIRED`                | The app wants an authenticator code and no TOTP seed is vaulted (or the vaulted one failed three times). | Enter a code in the live view and resume; re-connect with `totpSeed` so future logins need nobody.                                       |
| `LOGIN_UNCONFIRMED`            | The agent could not confirm it ended up signed in.                                                       | Check the live view — finish signing in and resume, or abort.                                                                            |
| `MANUAL_INTERVENTION`          | You paused the run yourself with `POST /v1/runs/:id/intervene`.                                          | Drive the browser, then resume.                                                                                                          |

## Failure codes

Terminal. `error.detail` usually carries the agent's own description of where it stopped.

| `error.code`           | What it means                                                                                    | What to do                                                                                                                                               |
| ---------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BLOCKED_TIMEOUT`      | The run blocked and nobody resumed within the \~10-minute hold. The browser was released.        | Start the operation again, and wire up the `run.blocked` webhook or `sente watch` so the next block reaches a human in time.                             |
| `VERIFICATION_TIMEOUT` | No usable verification email arrived within the wait window (\~8 minutes).                       | Check the identity's inbox with `GET /v1/messages` — if nothing arrived, the app never sent it (blocked signup, wrong address, silent rejection). Retry. |
| `AUTOMATION_STUCK`     | The agent stopped making progress, exceeded the run's wall clock, or the worker driving it died. | Watch the recording (`GET /v1/runs/:id/recording`) and retry. Repeat failures on the same target usually mean the site is refusing automated access.     |
| `APP_UNREACHABLE`      | Something the run depends on disappeared mid-run — its identity or account row was deleted.      | Recreate the account and retry.                                                                                                                          |
| `ABORTED`              | A person aborted the run (`POST /v1/runs/:id/abort`, `sente run abort`).                         | Nothing — this is your own action.                                                                                                                       |

<Note>
  A blocked-style code can also land on a **failed** run. If the browser session had already died when the gate was detected, takeover was impossible, so the truthful code (`CAPTCHA_REQUIRED`, `PHONE_REQUIRED`, …) is recorded on the failure instead of a generic one. Judge by `status`, not by the code alone.
</Note>

## Errors when starting or controlling a run

| Status | Code                      | Meaning                                                                                        | What to do                                                               |
| ------ | ------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `429`  | `RUN_LIMIT_EXCEEDED`      | Daily or monthly run cap. Failed runs never count.                                             | Wait for the daily window, or upgrade — see [Limits](/trust/limits).     |
| `429`  | `IDENTITY_LIMIT_EXCEEDED` | You omitted `identityId` and auto-provisioning the backing identity would exceed the plan cap. | Delete unused identities, reuse one by passing `identityId`, or upgrade. |
| `409`  | `ALREADY_CONNECTED`       | You called `register` for an app where a **connected** account already exists.                 | Use `registrations.login()` on that account instead.                     |
| `409`  | `ALREADY_REGISTERED`      | You called `connect` for an app where Sente created the account.                               | Use `registrations.login()`.                                             |
| `409`  | `REVOKED`                 | The connection was revoked; it is not re-authenticable.                                        | Re-connect it to re-enable.                                              |
| `409`  | `NOT_BLOCKED`             | `resume` on a run that isn't blocked.                                                          | Re-read the run; it likely timed out or already resumed.                 |
| `409`  | `NOT_INTERVENABLE`        | `intervene` on a run that isn't `running` / `awaiting_verification`.                           | Only live runs can be taken over.                                        |
| `409`  | `ALREADY_TERMINAL`        | `abort` on a run that already ended.                                                           | Nothing to stop.                                                         |

## Edge cases

<AccordionGroup>
  <Accordion title="Retrying a register or connect call">
    Both are idempotent per (identity, app origin). If a run is already in flight for that identity you get that run back. If the account is already `active`, `register` returns it without driving anything — re-authentication is `login`. Calling `connect` again on an existing connection rotates the stored credentials and clears a prior revoke.
  </Accordion>

  <Accordion title="A run whose worker dies">
    Each run heartbeats while it is being driven. If the heartbeat goes stale (about 5 minutes), a sweep fails the run with `AUTOMATION_STUCK` and stops its browser session, so a dead worker can never leave a run stuck `running` forever.
  </Accordion>

  <Accordion title="Watching a run while it happens">
    A run carries `liveViewUrl` once its browser session exists — an interactive view of the exact page the agent is on. The [dashboard](https://app.sente.run) embeds it on the run page with take-over and resume buttons. After the run ends, `GET /v1/runs/:id/recording` returns a video of the session once processing finishes.
  </Accordion>

  <Accordion title="Listing runs">
    `GET /v1/runs` returns the org's runs newest-first, filterable by `identityId`, `status`, `type`, `since`, and `limit` — the activity feed behind the dashboard. The list projection joins the identity email and app URL but drops `result` and `liveViewUrl`; fetch `GET /v1/runs/:id` for those. See the [runs API reference](/api-reference/runs).
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Human takeover" icon="hand" href="/runs/human-takeover">
    What blocks a run, how a person clears it, and how they get paged.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/events/webhooks">
    Get run outcomes pushed instead of polling for them.
  </Card>

  <Card title="Runs API reference" icon="terminal" href="/api-reference/runs">
    Endpoints, exact payloads, and query parameters.
  </Card>
</CardGroup>
