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

# Long-Running Tasks

> Keep a Box running longer and restart processes after resume or fork.

Boxes have an auto-stop timer. The default is 1 hour. The timer counts from creation, not from your last activity: a box with a 1 hour TTL stops one hour after it started, even mid-work. Resuming a stopped box restarts the timer.

<Note>
  Everything on this page needs a payment method on the account. On the free trial auto-stop cannot be disabled and cannot exceed 2 hours: the call is refused with `trial_auto_stop_required`. See [On the free trial](/box/billing#on-the-free-trial).
</Note>

For work that should keep running until you stop it yourself, disable auto-stop when you create the Box or update an existing Box's TTL to `null`:

<CodeGroup>
  ```bash CLI theme={null}
  box new --no-auto-stop
  box extend bx_f7k2q9hd --no-auto-stop
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOX_API_BASE/boxes" \
    -H "Authorization: Bearer $BOX_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"ttlSeconds":null}'

  curl -sS -X PATCH "$BOX_API_BASE/boxes/bx_f7k2q9hd" \
    -H "Authorization: Bearer $BOX_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"ttlSeconds":null}'
  ```

  ```ts TypeScript theme={null}
  const created = await box.create({ createBoxRequest: { ttlSeconds: null } });

  await box.update({
    boxId: "bx_f7k2q9hd",
    updateBoxRequest: { ttlSeconds: null },
  });
  ```

  ```python Python theme={null}
  created = box.create(CreateBoxRequest(ttl_seconds=None))

  box.update("bx_f7k2q9hd", UpdateBoxRequest(ttl_seconds=None))
  ```
</CodeGroup>

You can also set **No auto-stop** from the dashboard.

Use this for demos, browser sessions, long setup jobs, or unattended workflows where stopping the VM would interrupt work and force you to restart processes.

`resume` and `fork` take `ttlSeconds` too, so a Box that comes back does not need a second call to set its lifetime. Resume keeps the Box's current setting when you omit it; a fork always defaults to 1 hour rather than inheriting, so forking a Box with auto-stop off does not silently produce another Box nothing will stop. See [Snapshots & Copies](/box/snapshots).

## Timed Extension

If you still want an auto-stop deadline, extend the Box instead:

<CodeGroup>
  ```bash CLI theme={null}
  box extend bx_f7k2q9hd --hours 12
  box extend bx_f7k2q9hd --ttl 2592000
  ```

  ```bash curl theme={null}
  curl -sS -X PATCH "$BOX_API_BASE/boxes/bx_f7k2q9hd" \
    -H "Authorization: Bearer $BOX_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"ttlSeconds":43200}'
  ```

  ```ts TypeScript theme={null}
  await box.update({
    boxId: "bx_f7k2q9hd",
    updateBoxRequest: { ttlSeconds: 12 * 60 * 60 },
  });
  ```

  ```python Python theme={null}
  box.update("bx_f7k2q9hd", UpdateBoxRequest(ttl_seconds=12 * 60 * 60))
  ```
</CodeGroup>

`2592000` seconds is 30 days. This is the maximum numeric TTL accepted by the Box API; larger values are capped to 30 days.

## Resume and Fork

`box stop` snapshots the filesystem. `box resume` restores that filesystem, and `box fork` creates a new Box from the latest snapshot. Resume takes a few seconds regardless of how much data the Box holds. See [Snapshots](/box/snapshots) for what is captured, restore behavior, retention, and how to inspect or download a snapshot.

Stop/resume behaves like a server reboot: **systemd services you enabled start again automatically**. Processes you ran by hand (dev servers, background jobs, tunnels, browser sessions) do not survive a reboot; restart them, or make them a systemd service so they come back on their own (see below).

## Run a Background Command

For one-off jobs that outlive the synchronous command limit (builds, installs, data processing), start the command detached and poll for its result. This replaces the `nohup ... &` pattern for simple cases: no SSH session to keep open, no unit file to write. Synchronous commands cap at `timeoutSeconds` 600 (10 minutes); anything longer is start → poll → collect.

<CodeGroup>
  ```bash CLI theme={null}
  pid="$(box exec bx_f7k2q9hd --detach "npm run build" | jq -r .processId)"
  box exec bx_f7k2q9hd --status "$pid"
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOX_API_BASE/boxes/$BOX_ID/commands" \
    -H "Authorization: Bearer $BOX_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"npm run build","detached":true}'
  # {"type":"command.started","processId":1234,"pid":1234,"logPath":"/home/user/.ascii/processes/1234.log",...}

  curl -sS "$BOX_API_BASE/boxes/$BOX_ID/commands/1234" \
    -H "Authorization: Bearer $BOX_API_KEY"
  # {"type":"command.status","running":false,"exitCode":0,"stdout":"...","stderr":"...",...}
  ```

  ```ts TypeScript theme={null}
  const started = await box.command({
    boxId,
    commandRequest: { command: "npm run build", detached: true },
  });

  const status = await box.commandStatus({ boxId, processId: started.processId });
  ```

  ```python Python theme={null}
  from ascii_box_sdk.models.command_request import CommandRequest

  started = box.command(box_id, CommandRequest(command="npm run build", detached=True))

  status = box.command_status(box_id, started.process_id)
  ```
</CodeGroup>

The command keeps running on the box after the start call returns, with stdout/stderr appended to `~/.ascii/processes/<pid>.log` on the box. The status call reports `running`, the `exitCode` once finished, and a tail of each log; read the full log from the box when you need more than the tail.

The process runs under the box user's own systemd user manager, so a restart of the box's agent (a platform upgrade, for example) does not interrupt it. The agent does forget the process on restart: the status degrades to `lost`, `running` becomes a best-effort probe, `exitCode` is no longer reported, and the logs are read from the on-disk files. Detached processes do not survive a stop, resume, or fork on their own. For a process that must come back on its own, run it as an always-on systemd service (below).

## Run an Always-On Service

To make a process survive stop, resume, and fork without any action on your side, run it as an enabled systemd service. The unit file lives under `/etc`, which is snapshotted, so a resumed or forked box starts it automatically.

```bash theme={null}
sudo tee /etc/systemd/system/my-app.service >/dev/null <<'UNIT'
[Unit]
Description=My app
After=network.target

[Service]
User=user
WorkingDirectory=/home/user/my-app
ExecStart=/usr/bin/npm run start
Restart=always

[Install]
WantedBy=multi-user.target
UNIT

sudo systemctl daemon-reload
sudo systemctl enable --now my-app
```

`Restart=always` also revives the process if it crashes. Check on it with `systemctl status my-app` and read its logs with `journalctl -u my-app`.

These are in-box shell commands: run them over `box ssh`, or programmatically through the [command endpoint](/box/api/reference/agent/execute-box-command) or the SDKs' `command` method.

If the service serves HTTP, expose it with `host <port>`. Hosting the same port again after a resume returns the same URL and token, so links you handed out keep working. See [Hosting](/box/hosting).

Resume or fork from the latest snapshot when a user needs to continue or branch prior work:

<CodeGroup>
  ```bash CLI theme={null}
  box resume bx_f7k2q9hd
  box fork bx_f7k2q9hd
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOX_API_BASE/boxes/bx_f7k2q9hd/resume" \
    -H "Authorization: Bearer $BOX_API_KEY"

  curl -sS -X POST "$BOX_API_BASE/boxes/bx_f7k2q9hd/fork" \
    -H "Authorization: Bearer $BOX_API_KEY"
  ```

  ```ts TypeScript theme={null}
  await box.resume({ boxId: "bx_f7k2q9hd" });
  const forked = await box.fork({ boxId: "bx_f7k2q9hd" });
  ```

  ```python Python theme={null}
  box.resume("bx_f7k2q9hd")
  forked = box.fork("bx_f7k2q9hd")
  ```
</CodeGroup>

For repeatable workflows, keep setup and start commands idempotent:

<CodeGroup>
  ```bash CLI theme={null}
  box ssh "$box_id" -- bash -s < ./setup.sh
  box ssh "$box_id" "cd /home/user/ariana-ide-private && npm run dev"
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOX_API_BASE/boxes/$BOX_ID/commands" \
    -H "Authorization: Bearer $BOX_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"bash ./setup.sh","cwd":"ariana-ide-private"}'

  curl -sS -X POST "$BOX_API_BASE/boxes/$BOX_ID/commands" \
    -H "Authorization: Bearer $BOX_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"npm run dev","cwd":"ariana-ide-private"}'
  ```

  ```ts TypeScript theme={null}
  await box.command({ boxId, commandRequest: { command: "bash ./setup.sh", cwd: "ariana-ide-private" } });
  await box.command({ boxId, commandRequest: { command: "npm run dev", cwd: "ariana-ide-private" } });
  ```

  ```python Python theme={null}
  box.command(box_id, CommandRequest(command="bash ./setup.sh", cwd="ariana-ide-private"))
  box.command(box_id, CommandRequest(command="npm run dev", cwd="ariana-ide-private"))
  ```
</CodeGroup>

Command calls are never retried automatically: running a command is not idempotent, so a timeout (`retryable: false`) or a network failure mid-call leaves the decision to you, because the command may already be running on the box. Synchronous commands accept `timeoutSeconds` from 1 to 600 (default 30); values outside that range are refused with a 400 `invalid_timeout` error.

## Related

* [Snapshots](/box/snapshots)
* [Environments](/box/environments)
* [SSH Access](/box/ssh-access)
* [Desktop Streaming](/box/desktop-streaming)
* [Hosting](/box/hosting)
