Docs
Dashboard
Guides / SessionsBeta

How runs and sessions end

Every way an agent run or a session ends, what to do about each, and the habits that keep sessions from running longer than you want: stop in finally, timeouts, keepAlive, idleTimeout, cancelling, reconnecting.

Every session and every agent run ends for a reason, and the reason is always on it: stopReason on the session, errorCode on the run. The session.stopped and agent_run.finished webhooks carry them too. A session always stops: it is saved and free, and you can resume it. It is deleted only when you delete it, or when your plan’s retention days pass. This page puts the rules in one place, with the habits that keep sessions from running longer than you want.

One job per setting

SettingOnWhat it does
timeoutThe sessionIts one clock: the session stops at expiresAt, which it always shows. Change it while the session runs with Update a session. A run's own session takes timeout in the run's session (default 30 minutes, or your plan's maximum when shorter).
idleTimeoutThe sessionOpt-in: it stops (idle) after that many seconds with no activity at all. 30 seconds up to its timeout.
keepAliveThe sessionOnly about disconnects: a browser-only session without it stops about 5 seconds after its last client leaves. It never makes a session last longer than its timeout.
maxStepsThe runIts work budget: 30 by default, 1 to 1,000, or null for no step limit (the session's time then bounds it).
maxCostUsdThe runIts money budget, 0.01 to 100 dollars of model cost (optional). One model call can go a little past it.
maxConsecutiveErrorsThe runHow many tool errors in a row stop it: 5 by default, 1 to 20. A tool that works resets the count.

Waiting for a person (a hand-over, a CAPTCHA) uses the session’s time, not steps. A run without a step limit should have a cost limit or a sensible timeout.

When a run stops

A run stops at the first of these. The limits are checked before each model call.

CaseerrorCodeCan be resumedIts session
It finishes (status completed)null–Its own session is stopped first (agent_run, or task for a task's run), so you can resume it.
It uses all its stepsmax_stepsYesIts own session is kept running for the resume window, then stopped.
It reaches maxCostUsdmax_costYesAs at the step limit.
maxConsecutiveErrors tool errors in a rowtoo_many_errorsYesAs at the step limit.
The same call with the same result 5 times in a rowno_progressYesAs at the step limit.
The API server running it stopped, and the session goes onserver_restartedYesAs at the step limit.
Its session reaches its timesession_timeoutNoThe session stops (timeout).
Its session stops or is deleted another way (you stopped it, its machine was lost, …)session_endedNoAs it stopped; error names its stopReason (or says it was deleted).
The plan's monthly model spend cap is reachedspend_limitNoIts own session is stopped.
Your organization or project reaches its monthly spending limit, and the run is refused before its next stepspend_limitNoIts own session is stopped.
The spending limit stops its session first (within about a minute)session_endedNoStopped (spend_limit); error names it.
You cancel it (status canceled)nullNoIts own session is stopped; a session you passed in stays.

A session you passed in with sessionId is never stopped by the run. If it was stopped, the run resumes it first. With keepSession: true, the run’s own session keeps running until its expiresAt, with keepAlive on: stop it yourself, or give it an idleTimeout.

The resume window

A run that stopped at one of its limits keeps its progress. While resumable is set ({ until }), Resume a run: the same run goes on in the same browser and shell, with the same run id. Its own session is kept for 10 minutes for that, or less when the session’s own time ends sooner (until is the earlier of the two). The session is billed like any running session meanwhile, and stopped when the window passes unused. See Pause and resume a run.

When a session stops

stopReasonWhat happened
requestedYou, the console, or the CLI or an SDK stopped it.
timeoutIt reached its expiresAt.
idleIts idleTimeout passed with no activity (checked every few seconds, so it can stop up to about 15 seconds late).
no_clientsA browser-only session without keepAlive lost its last client, with no agent run, script or step working in it.
agent_runThe agent run that created it finished, or its resume window passed unused.
taskThe run of a task that created it finished.
machine_lostIts machine failed and could not be recovered. It keeps its last checkpoint; with none, nothing was saved.
api_restartThe API restarted and the session's machine could not be taken back, with no checkpoint to start it again from. Nothing was saved.
account_recoveredA password reset recovered an account whose email was not confirmed. Nothing is kept.
suspendedAn admin suspended the organization. Its running sessions stop, saving what they can.
spend_limitThe organization or the project reached the monthly spending limit its owners set. Running sessions stop within about a minute.
out_of_creditNo credit left. Used only when refusing work at zero credit is switched on, which it is not yet: today, running out of credit only sends an alert.

Every stop saves the session as it is, except where the table says nothing was saved. A stopped session is free and is kept for your plan’s retention days (its deletesAt), then deleted. See How long a stopped session is kept. A session whose machine failed comes back from its last checkpoint when it has one; without one it stops with stopReason: "machine_lost" and the checkpoint is what it saved. See Checkpoints and recovery.

What counts as disconnected

  • Only browser-only sessions without keepAlive stop on disconnect, about 5 seconds after their last Playwright or Puppeteer client leaves. The live view is not a client for this.
  • Never while an agent run, a script or a plain-English step works in the session, or while a run in it can still be resumed. When the last of these ends and nobody is connected, the 5 seconds start then.
  • Sessions with a shell, and sessions nobody ever connected to (API-only use), run until their timeout.

When the API restarts

When the platform’s API restarts, for example during a deploy, sessions keep running: the new API takes their machines back. api_restart now means only a session whose machine could not be taken back and had no checkpoint.

An agent run’s loop lives in the API server that runs it. When that server stops, the run ends at once when the same server starts again, or within about 90 seconds when another server notices, as server_restarted: the browser and shell kept their state, so resume the run. Until then, pause, resume and messages answer 409 run_not_live, and its event stream gets its done event on whichever server you are connected to.

Best practices

Stop sessions in finally

A session is billed per second until it stops. Stop it when you are done, also when your code fails on the way. A stopped session is free, and you can resume it if you need it again:

SESSION_ID=$(curl -s -X POST https://api.boxline.dev/v1/sessions -H "x-api-key: $BOXLINE_API_KEY" -H "content-type: application/json" -d '{"timeout": 600}' | jq -r .id)trap 'curl -s -X POST https://api.boxline.dev/v1/sessions/$SESSION_ID/stop -H "x-api-key: $BOXLINE_API_KEY" > /dev/null' EXIT# … work with the session …

Choose timeouts

  • Give each session the longest time its job needs, plus a margin, not your plan’s maximum. It stops the bill if everything else goes wrong.
  • For an agent run in its own session, set timeout in the run’s session. A run whose session reaches its time fails with session_timeout and cannot be resumed, so give long jobs enough time up front.
  • To go on past it, raise the session’s timeout while it runs, with Update a session: the new number is its whole length from its last start, up to your plan’s longest session. A run shows its session’s end as sessionExpiresAt, and the session.expiring webhook comes about a minute before.
curl -X PATCH https://api.boxline.dev/v1/sessions/$SESSION_ID \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -d '{"timeout": 2400}'   # it had 1800: ten more minutes

keepAlive only covers disconnects

keepAlive: true keeps a browser-only session running between connections. It does not end the session for you and does not make it outlive its timeout: the session runs, and is billed, until you stop it or it reaches its time. Pair it with an idleTimeout.

idleTimeout for crashed clients

When a client crashes, a keepAlive session, or one with a shell, has nothing that ends it until its timeout. idleTimeout stops it after that many seconds with no activity: CDP commands, live-view input, terminal keys, exec, files, actions and steps, scripts, and agent runs. A run working in the session, or one that can still be resumed, counts as activity the whole time.

curl -X POST https://api.boxline.dev/v1/sessions \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -d '{"keepAlive": true, "timeout": 3600, "idleTimeout": 300}'# Change it later (counted from then), or null to switch it off:curl -X PATCH https://api.boxline.dev/v1/sessions/$SESSION_ID \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -d '{"idleTimeout": 600}'

Your own wait does not cancel a run

When your code stops waiting for a run, the run goes on. bx.agent.wait() giving up, a closed event stream, or your process exiting never stops it. Cancel it when you no longer want it:

curl -X POST https://api.boxline.dev/v1/agent/runs/$RUN_ID/cancel \  -H "x-api-key: $BOXLINE_API_KEY"

Reconnecting

  • A dropped CDP connection: connect again to the same connectUrl. A browser-only session without keepAlive waits about 5 seconds for you; use keepAlive when you need longer.
  • When the platform replaces a machine: clients are disconnected; connect again to the same connectUrl, which now points at the new machine.
  • A stopped session: call resume first (POST /v1/sessions/:id/resume). Connecting, or calling actions, exec or files, on a stopped session answers 409 session_not_running. An agent run given its id resumes it.
  • A run’s events: open GET /v1/agent/runs/:id/events/stream again. A late viewer first gets the steps so far, and the running command with its output.
  • After changing timeout: take the fresh URLs from the answer (or read the session again); a URL’s token lasts until 5 minutes after the expiresAt it was signed with.

These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.