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

# E2EConfig

> Every configuration key, its default, its valid range, and the environment variables.

`E2EConfig` is the type of the object `e2e.config.ts` default-exports. Write
the object literal and end it with `satisfies E2EConfig`: the editor
completes every key, unknown keys are flagged, and the export keeps its
literal type. Every rule below is enforced when the runner resolves the
config, not by the type.

```ts title="e2e.config.ts" theme={"theme":"catppuccin-mocha"}
import type { E2EConfig } from '@e2edev/e2e';
import { playwright } from '@e2edev/playwright';

export default {
  targets: [
    {
      name: 'web',
      engine: playwright({ browser: 'chromium', url: process.env.APP_URL ?? 'http://localhost:3000' }),
    },
  ],
} satisfies E2EConfig;
```

Unknown keys are rejected with `INVALID_CONFIG` at every level. There is no
top-level `app` key: the engine declares the app it drives (see
[the app under test](#the-app-under-test)).

## Loading

`--config <path>` selects an explicit file. Otherwise the runner searches the
working directory and each parent for `e2e.config.ts` or `e2e.config.mts`,
stopping at the repository root. Both names in one directory is
`CONFIG_AMBIGUOUS`. The selected file's directory is the project root.

Config and test modules run as ESM. e2e loads the `.ts`, `.mts`, and `.tsx`
files a project reaches by path as ES modules whatever the nearest
`package.json` `type` says, so a CommonJS package keeps its module type.
Packages imported by name keep the format their own manifest declares. A
config or test written with `require` or `module.exports` fails to load.

## Top level

| Key                | Type                                                   | Default                                                                                                 | Valid range                                                                                                                                                                              |
| ------------------ | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `specVersion`      | `'0.1'`                                                | `'0.1'`                                                                                                 | the one value this runner implements                                                                                                                                                     |
| `projectId`        | `string`                                               | the `name` of the project root's `package.json`, else `unportable-<sha256 of the project root, 32 hex>` | 1-256 characters                                                                                                                                                                         |
| `targets`          | `readonly Target[]`                                    | required                                                                                                | non-empty; see [targets](#targets)                                                                                                                                                       |
| `tests`            | `string \| readonly string[]`                          | `'tests/**/*.e2e.ts'`                                                                                   | non-empty, `/` separators, duplicates removed                                                                                                                                            |
| `timeout`          | `number`                                               | `120000`                                                                                                | positive safe integer                                                                                                                                                                    |
| `launchTimeout`    | `number`                                               | `60000`                                                                                                 | positive safe integer                                                                                                                                                                    |
| `actionTimeout`    | `number`                                               | `30000`                                                                                                 | positive safe integer; bounds every engine operation, including each observation and navigation inside an agent step. A targeted agent action is bounded by the smaller of this and 15 s |
| `assertionTimeout` | `number`                                               | `5000`                                                                                                  | positive safe integer                                                                                                                                                                    |
| `cleanupTimeout`   | `number`                                               | `30000`                                                                                                 | positive safe integer                                                                                                                                                                    |
| `retries`          | `number`                                               | `1` in CI, else `0`                                                                                     | integer 0-10                                                                                                                                                                             |
| `workers`          | `number`                                               | `1` in CI, else `max(1, floor(availableParallelism() / 2))`                                             | integer 1-1024; a target never runs more than the `workers` its engine declares                                                                                                          |
| `artifacts`        | kinds array or `{ kinds?, store?, video? }`            | `['screenshot', 'trace']`                                                                               | see [artifacts](#artifacts)                                                                                                                                                              |
| `reporters`        | `readonly ('list' \| 'json' \| 'junit' \| Reporter)[]` | `['list']`                                                                                              | the three IDs and [reporter objects](/reference/reporters); `json` cannot be combined with `list`; `--reporter` replaces the IDs and keeps the objects                                   |
| `screen`           | object                                                 | see [screen](#screen)                                                                                   |                                                                                                                                                                                          |
| `agents`           | object of agents by name                               | see [agents](#agents)                                                                                   | `default` is the agent tests run with                                                                                                                                                    |
| `cache`            | string or object                                       | `'read-write'` (`'read-only'` in CI when unset)                                                         | see [cache](#cache)                                                                                                                                                                      |
| `limits`           | object                                                 | see [limits](#limits)                                                                                   |                                                                                                                                                                                          |
| `credentials`      | record                                                 | `{}`                                                                                                    | see [credentials](#credentials)                                                                                                                                                          |

Every numeric value must be a safe integer.

## targets

```ts theme={"theme":"catppuccin-mocha"}
export interface Target {
  name?: string;         // defaults to platform
  platform?: Platform;   // defaults to the engine's
  engine?: EngineHandle; // defineEngine(...) from '@e2edev/e2e/engine'
}
```

| Key        | Type           | Default      | Notes                                                                                                                                                                           |
| ---------- | -------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`     | `string`       | `platform`   | Stable ID used by `--target`, reports, sessions, and artifact paths; unique across targets. ASCII letters, digits, `_`, `-`, `.`                                                |
| `platform` | `Platform`     | the engine's | The label `platforms: [...]` on a test filters against. Set it for a target without an engine; a value that disagrees with the engine's is `INVALID_CONFIG`                     |
| `engine`   | `EngineHandle` | none         | [`playwright(...)`](/reference/playwright) for a browser, [`agentDevice(...)`](/reference/agent-device) for a device, any [`defineEngine`](/writing-an-engine) handle otherwise |

`{ engine: playwright({ url }) }` is a complete target. Two targets on one
platform name themselves. A target without an engine runs opaquely through
agent tools; the runner cannot observe or drive it through an engine.
Ordinary tests whose `requires` list names a capability the target lacks are
skipped for that target. A `test.setup` needed by a selected session consumer
must be runnable there; if its required capabilities are missing, selection
fails with `COLLECTION_ERROR`. Calling an unsupported engine operation or
acquiring a fixture it does not contribute fails with
`UNSUPPORTED_CAPABILITY`.

## The app under test

The app is declared by the engine that drives it. The browser engine takes
the declaration as options of `playwright(...)`; the device engine derives it
from the app it pins. The runner resolves it at config load and owns what is
built on it: navigation and origin policy, cache and session identity, the
report's target record, and the app process.

```ts theme={"theme":"catppuccin-mocha"}
export interface EngineAppDeclaration {
  url?: string;
  command?: CommandConfig;
  readyUrl?: string;
  services?: readonly ServiceConfig[];
  allowedOrigins?: readonly string[];
  environment?: 'test' | 'staging' | 'production';
  identity?: string;
}
```

| Key              | Type                                  | Default                                                                                            | Notes                                                                                                                                                                                                                                       |
| ---------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`            | `string`                              | unset                                                                                              | Required once a test calls `app.open()` or navigates relatively, else `APP_URL_REQUIRED` at that call. A missing scheme becomes `https://`, or `http://` for a loopback host. No userinfo, query, or fragment; plain HTTP only for loopback |
| `command`        | `CommandConfig`                       | unset                                                                                              | The app process. Targets declaring the same command share one process                                                                                                                                                                       |
| `readyUrl`       | `string`                              | the resolved `url`                                                                                 | Absolute http(s) readiness probe; a status of 200-499 counts as ready. A `command` with neither is `APP_URL_REQUIRED` at config load                                                                                                        |
| `services`       | `readonly ServiceConfig[]`            | `[]`                                                                                               | Dependency processes started in order before the app command and torn down in reverse                                                                                                                                                       |
| `allowedOrigins` | `readonly string[]`                   | the origin of `url`, else none                                                                     | Each entry must be a serialized origin                                                                                                                                                                                                      |
| `environment`    | `'test' \| 'staging' \| 'production'` | `'test'` for loopback, `.localhost`, and `.test` hosts and without a URL, otherwise `'production'` | A label for the report and the cache/session identity; it never gates a run                                                                                                                                                                 |
| `identity`       | `string`                              | the URL's origin and base path, else none                                                          | Stable logical app identity for cache and session keying. Set it when the origin is ephemeral (a per-PR preview URL) so entries survive redeploys; never share one across different apps                                                    |

### command

```ts theme={"theme":"catppuccin-mocha"}
export interface CommandConfig {
  executable: string;
  args?: readonly string[];
  cwd?: string;
  env?: Readonly<Record<string, string>>;
  startupTimeout?: number;
  shutdownTimeout?: number;
  log?: string;
  reuseExisting?: boolean;
}
```

| Key               | Type                     | Default                  | Notes                                                                                                                                                                                                                                                                                                |
| ----------------- | ------------------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `executable`      | `string`                 |                          | Required, non-empty, resolved with the process `PATH`. Never shell-interpreted                                                                                                                                                                                                                       |
| `args`            | `readonly string[]`      | `[]`                     | Passed verbatim                                                                                                                                                                                                                                                                                      |
| `cwd`             | `string`                 | project root             | Resolved from the project root                                                                                                                                                                                                                                                                       |
| `env`             | `Record<string, string>` | `{}`                     | Adds to the inherited set                                                                                                                                                                                                                                                                            |
| `startupTimeout`  | `number`                 | `60000`                  | Ready-probe budget; expiry is `APP_UNREACHABLE`. One `still starting` notice at half the budget, once that half is at least 5 s                                                                                                                                                                      |
| `shutdownTimeout` | `number`                 | `10000`                  | Grace period before force-kill                                                                                                                                                                                                                                                                       |
| `log`             | `string`                 | unset (output discarded) | File that receives stdout and stderr, appended; resolved from the project root and must stay inside it. The raw, unredacted output; keep it in an ignored directory. A startup failure quotes the last 20 lines appended since the command started, `env` values replaced by `<secret:NAME>` markers |
| `reuseExisting`   | `boolean`                | `false`                  | If `readyUrl` already answers before the spawn, attach to that process and leave it alone on teardown. Without it, an already-answering URL is `APP_ALREADY_RUNNING`. Ignored in CI, with a notice                                                                                                   |

The child inherits only `PATH`, `HOME`, `TMPDIR`, `TMP`, `TEMP`,
`SystemRoot`, and `COMSPEC`. Model credentials, `E2E_USER_*` values, and CI
tokens are not inherited automatically. Every value explicitly supplied in
`command.env` is forwarded, including a credential or token. The runner
never terminates a process it did not start.

### services

```ts theme={"theme":"catppuccin-mocha"}
export interface ServiceConfig extends CommandConfig {
  name?: string;
  readyUrl?: string;
  waitForExit?: boolean;
  teardown?: CommandConfig;
}
```

| Key           | Type            | Default              | Notes                                                                                                            |
| ------------- | --------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `name`        | `string`        | executable base name | Label in errors, reporter output, and the report. Non-empty, at most 64 characters, unique across named services |
| `readyUrl`    | `string`        | unset                | HTTP readiness probe, polled like the app's                                                                      |
| `waitForExit` | `boolean`       | `false`              | The process is the step: ready when it exits 0. A non-zero exit is `APP_UNREACHABLE`                             |
| `teardown`    | `CommandConfig` | unset                | Run during teardown after the service is stopped, waited on within its own `startupTimeout`                      |

Exactly one of `readyUrl` or `waitForExit` is required per service.
`reuseExisting` works on a `readyUrl` service and skips its `teardown`; on a
`waitForExit` service it is `INVALID_CONFIG`. Services start in declaration
order before any app command, each ready before the next starts. Services
declared identically by several targets start once; two targets that order
shared services differently are `INVALID_CONFIG`.

On every exit path the runner stops every app command, then the services in
reverse order, then runs their `teardown` commands in reverse order. A
failing teardown is recorded as a `cleanup` run error and never skips the
teardowns after it. [Starting your app](/starting-your-app) shows a full
example.

## artifacts

```ts theme={"theme":"catppuccin-mocha"}
artifacts: {
  kinds: ['screenshot', 'trace', 'video'],
  store: {
    async put(artifact) {
      // artifact.bytes, .sha256, .mediaType, .path, .runId, .testId, .attemptId, .stepId?, .startedAt?
      const key = await bucket.upload(artifact.path, artifact.bytes);
      return { ref: key };
    },
  },
  video: { retain: 'on-failure' },
},
```

`kinds` accepts `screenshot`, `trace`, and `video`. The default set is
screenshot and trace, captured best-effort. A configured kind the target's
engine cannot produce fails before collection with `UNSUPPORTED_ARTIFACT`.
`video` is never in the default set; `--video` adds it for one run.

`store` receives every artifact the moment it is complete on disk and returns
its own reference, which the report records as the artifact's `ref` beside
the local path. A failed `put` never fails the run. A store never crosses a
process boundary: each worker constructs its own from the config module.

### video

With `video` among the kinds, or `--video`, every selected engine must support
recording; otherwise configuration fails with `UNSUPPORTED_ARTIFACT`.
Playwright opens a page when recording starts and writes WebM segments, one
per page it records. The agent-device engine writes an MP4 of the device
screen. Other engines define their recording format and when segments exist.
A failed launch or a recording that cannot be finalized may leave no video.

The first file lands under the attempt's artifact directory as
`video/video.webm` for Playwright or `video/video.mp4` for agent-device.
Playwright names later segments `video/video-part<n>.webm`. Each segment has
`startedAt` set to when recording began, so
`step.startedAt - artifact.startedAt` is a step's offset into that segment.

| Key      | Type                    | Default | Notes                                                                                     |
| -------- | ----------------------- | ------- | ----------------------------------------------------------------------------------------- |
| `retain` | `'all' \| 'on-failure'` | `'all'` | `on-failure` deletes the recording of every attempt that passed once its verdict is known |

Recording costs an encoder per page and about a megabit per second of disk.
A recording masks nothing, so a video artifact is recorded with
`redaction: 'incomplete'`. Turning video on or off never invalidates cached
replays.

## screen

```ts theme={"theme":"catppuccin-mocha"}
screen?: {
  testIdAttribute?: string;
};
```

| Key               | Type     | Default         | Notes                                  |
| ----------------- | -------- | --------------- | -------------------------------------- |
| `testIdAttribute` | `string` | `'data-testid'` | Attribute read by `screen.getByTestId` |

## agents

```ts theme={"theme":"catppuccin-mocha"}
agents?: Record<string, StepExecutor | {
  executor?: StepExecutor;
  model?: ModelInstance;
  maxSteps?: number;
  maxModelCalls?: number;
  maxObservationBytes?: number;
  context?: string;
  vision?: boolean | 'only';
  providerOptions?: Record<string, Record<string, unknown>>;
}>;
```

Agents by name. `default` is the one tests run with, and it exists even when
the config names none. Other names are other brains for the same suite:
`e2e run --agent <name>` re-points the default, a test or describe block pins
one with `{ agent: 'name' }` or several with `{ agent: ['buyer', 'admin'] }`,
and any `agent.*` call can name one. An unknown name fails by where it was
written: `--agent` is `INVALID_CONFIG`, a pin is `COLLECTION_ERROR`, a call's
`agent` is `INVALID_ARGUMENT`. Names are ASCII letters, numbers, `_`, `-`,
or `.`. See [Agents and personas](/agents).

```ts title="e2e.config.ts" theme={"theme":"catppuccin-mocha"}
import type { E2EConfig } from '@e2edev/e2e';
import { createAgent } from '@e2edev/e2e/agent';
import { playwright } from '@e2edev/playwright';
import { gateway } from 'ai';

export default {
  targets: [{ engine: playwright({ url: 'http://localhost:3000' }) }],
  agents: {
    default: { model: gateway('openai/gpt-5.6-luna') },
    ux: createAgent({ model: gateway('openai/gpt-5.6-luna'), system: 'Check labels, contrast, and keyboard access.' }),
    thorough: { model: gateway('anthropic/claude-opus-5'), context: 'Verify every claim on screen.' },
  },
} satisfies E2EConfig;
```

Each agent takes three shapes: an options object, an options object with an
`executor`, or the executor itself. The bare form is shorthand for
`{ executor }`. Every diagnostic names the agent: `agents.ux.model`.

| Key                   | Type                                      | Default            | Valid range                                            | Notes                                                                                                                                                                                                     |
| --------------------- | ----------------------------------------- | ------------------ | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `executor`            | `StepExecutor`                            | the built-in agent | `createAgent(...)` or any `{ name, runStep(context) }` | The brain `agent.act()` dispatches to. Never crosses a process boundary; workers rebuild it from the config module. See [Custom executors](/executors)                                                    |
| `model`               | `ModelInstance`                           | unset              | an AI SDK model instance                               | Unset means acquiring `agent` fails with `MODEL_UNAVAILABLE`; a string is `INVALID_CONFIG` naming the constructor to write. Naming a model that differs from `createAgent({ model })` is `INVALID_CONFIG` |
| `maxSteps`            | `number`                                  | `25`               | integer 1-100                                          | Committed actions per agent call                                                                                                                                                                          |
| `maxModelCalls`       | `number`                                  | `25`               | integer 1-100                                          | Model requests per agent call                                                                                                                                                                             |
| `maxObservationBytes` | `number`                                  | `262144`           | integer 1024-16777216                                  | Observation payload ceiling for `act` turns and judgment calls alike                                                                                                                                      |
| `context`             | `string`                                  | unset              | at most `limits.maxAgentContextBytes` UTF-8 bytes      | Trusted project context prepended to agent prompts                                                                                                                                                        |
| `vision`              | `boolean \| 'only'`                       | `false`            |                                                        | Project-wide default for the per-call `vision` option; `'only'` sends pixels instead of the tree. See [Vision](/reference/agent#vision)                                                                   |
| `providerOptions`     | `Record<string, Record<string, unknown>>` | unset              | object keyed by provider name                          | AI SDK provider options sent with every model call, e.g. `{ openai: { reasoningEffort: 'low' } }`. `createAgent({ providerOptions })` overrides it for `act`                                              |

Judgments (`assert`, `waitFor`, `extract`) allow 8192 output tokens per call
and set no temperature.

### model

```ts theme={"theme":"catppuccin-mocha"}
export interface ModelInstance {
  readonly specificationVersion: string;
  readonly provider: string;
  readonly modelId: string;
}
```

The model is always an AI SDK model instance the config constructs. Detection
is structural, so any provider package works without the runner depending on
it, and the instance owns its transport and credentials.

| Gateway                                                 | Constructor                                                                | Package                                    | Reads                                 |
| ------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------- |
| [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) | `gateway('openai/gpt-5.6-luna')`                                           | `ai`                                       | `AI_GATEWAY_API_KEY`                  |
| [OpenRouter](https://openrouter.ai/models)              | `openrouter('openai/gpt-5.6-luna')`                                        | `@openrouter/ai-sdk-provider`              | `OPENROUTER_API_KEY`                  |
| Any OpenAI-compatible endpoint                          | `createOpenAICompatible({ name, baseURL, apiKey? }).chatModel('llama3.2')` | `@ai-sdk/openai-compatible`                | `apiKey`, when the endpoint needs one |
| A provider directly                                     | `openai('gpt-5.6-luna')`, `anthropic(...)`, ...                            | `@ai-sdk/openai`, `@ai-sdk/anthropic`, ... | the provider's own variable           |

Reports record the instance's `provider` and `modelId` as the step's
`provider` and `model`. Each worker re-resolves the config module and
constructs its own instance. A config whose digest differs between the runner
and a worker fails with `CONFIG_NOT_DETERMINISTIC`. See
[Choosing a model](/models).

## credentials

```ts theme={"theme":"catppuccin-mocha"}
credentials?: Readonly<
  Record<
    string,
    {
      username: string;
      password: string | SecretProvider;
      allowedOrigins?: readonly string[];
    }
  >
>;

type SecretProvider = () => string | Promise<string>;
```

| Key              | Type                       | Required | Notes                                                                                                                                                                                              |
| ---------------- | -------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `username`       | `string`                   | yes      | Default username                                                                                                                                                                                   |
| `password`       | `string \| SecretProvider` | yes      | A static value, or a provider called on every authorized fill (a vault lookup, a freshly computed TOTP). Recorded in the config digest by name only. An `E2E_USER_*` override wins over a provider |
| `allowedOrigins` | `readonly string[]`        | no       | Narrows this credential relative to the target's allowed origins. It can only narrow                                                                                                               |

`credentials.user(name)` resolves a configured entry into a `Credential`.
The username is a plain string; the password is an opaque `Secret` that test
code cannot read, accepted only by `locator.fill` and `agent.act` params. An
unconfigured name throws `AUTH_CREDENTIAL_UNAVAILABLE`.

```ts theme={"theme":"catppuccin-mocha"}
export interface Secret {
  readonly name: string;
  readonly purpose: 'password' | 'one-time-code' | 'generic-secret';
  readonly [secretBrand]: true;
}

export interface Credential {
  readonly name: string;
  readonly username: string;
  readonly password: Secret;
  readonly [credentialBrand]: true;
}
```

See [Signing in](/authentication).

## cache

In `read-write` mode, the trace cache records an `agent.act()` only after a
later recorded verification passes, such as a locator assertion or an
`agent.assert()` step. A trailing `act` with no verification is not recorded.
`read-only` mode can replay existing entries but writes none; `off` disables
both. [Caching agent steps](/cache) explains verification and when a replay
hands off.

| Key     | Type                                   |        Default | Description                                                                                                                                                                                          |
| ------- | -------------------------------------- | -------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mode`  | `'off' \| 'read-only' \| 'read-write'` | `'read-write'` | A bare string is shorthand for `{ mode }`. In CI an unset mode becomes `read-only`; set `'read-write'` explicitly when CI restores the cache from its own cache service. `--no-cache` wins over both |
| `store` | `TraceCacheStore`                      |     file store | Custom entry store, for example a shared remote cache. Exempt from the CI demotion; states its own trust through `writable`. Never crosses a process boundary                                        |
| `dir`   | `string`                               |   `.e2e/cache` | File store directory, resolved against the project root                                                                                                                                              |

### Commit your traces

`e2e init` adds `.e2e/cache/` to `.gitignore`, so committing entries is
opt-in. Remove that line to share replays with CI and teammates; CI stays
`read-only` unless `cache: 'read-write'` is set. See
[Caching agent steps](/cache#commit-your-traces).

## limits

Resolved limits are immutable for a run. Per-call options may only lower
them. A key exists here exactly when the runner has an enforcement site for
it.

| Key                     | Type     | Default | Range      | Description              |
| ----------------------- | -------- | ------: | ---------- | ------------------------ |
| `maxAgentContextBytes`  | `number` | `16384` | 1024-65536 | Trusted agent context    |
| `maxLedgerBytes`        | `number` |  `8192` | 1024-65536 | Prior-step ledger        |
| `maxEventsPerStep`      | `number` |  `1000` | 1-10000    | Events recorded per step |
| `maxModelTokensPerCall` | `number` | `64000` | 1-1000000  | Tokens per model request |

## Environment variables

The runner reads a closed set, listed on the
[Environment variables](/reference/environment) page. The ones that change
what a config resolves to:

| Variable                   | Effect                                         |
| -------------------------- | ---------------------------------------------- |
| `E2E_USER_<NAME>_USERNAME` | Overrides `credentials.<name>.username`        |
| `E2E_USER_<NAME>_PASSWORD` | Overrides `credentials.<name>.password`        |
| `CI`                       | Switches CI defaults and rejects focused tests |

`<NAME>` is the credential name uppercased with every character outside
`A-Z0-9` replaced by `_`, so `power-user` becomes `E2E_USER_POWER_USER_USERNAME`.
A pair for a name absent from `credentials` is ignored.

### Resolution order

| Setting                          | Order                                                                                                                                              |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Base URL                         | the engine's declared `url`, else `APP_URL_REQUIRED` at the first navigation. The runner reads no `APP_URL`; the scaffolded config reads it itself |
| Model                            | the model passed to `createAgent({ model })`, then `agents.<name>.model`, then `MODEL_UNAVAILABLE` at `agent` acquisition                          |
| Model credential                 | the provider package's own variable or the key passed at construction, read by the provider on the first model call                                |
| Credential username and password | the `E2E_USER_*` variable, then the config value                                                                                                   |

A missing model is checked once per run, when the `agent` fixture is first
acquired, and stops the run with exit 2. A custom executor with no model
skips the check.

### CI

CI mode is on unless `CI` is unset, empty, whitespace only, `0`, or `false`
(trimmed and lowercased first). Every other value turns it on.

| Setting                 | Local                                       | CI                        |
| ----------------------- | ------------------------------------------- | ------------------------- |
| `retries`               | `0`                                         | `1`                       |
| `workers`               | `max(1, floor(availableParallelism() / 2))` | `1`                       |
| `cache`                 | `read-write`                                | `read-only` when unset    |
| `test.only`             | allowed                                     | `ONLY_IN_CI`, exit code 2 |
| `command.reuseExisting` | honored                                     | ignored, with a notice    |

<CardGroup cols={2}>
  <Card title="Starting your app" href="/starting-your-app" />

  <Card title="Continuous integration" href="/ci" />

  <Card title="CLI" href="/reference/cli" />

  <Card title="Errors" href="/reference/errors" />
</CardGroup>
