# Runtime variables

How project variables become Playwright environment variables and how jobs consume them.

Runtime variables let a project provide values to a Playwright run without hard-coding them inside the script. They are the right place for API keys, queue IDs, project IDs, tenant names, target hosts, or feature flags used during execution.

## How injection works

See the diagram or table on the linked documentation page.

At run dispatch time, the backend reads `project_variables` for the project. Plain variables are copied directly. Secret variables are decrypted only when the caller is allowed to use variables. The backend sends the resolved values to the worker as `env_vars`, plus a list of `secret_env_keys` for redaction.

The worker starts Playwright with those values in the child process environment. The script reads them through `process.env`.

```ts
const PROJECT_ID = requiredEnv('HAPPY_PATH_QUEUE_PROJECT_ID');
const QUEUE_ID = requiredEnv('HAPPY_PATH_QUEUE_ID');
const API_KEY = requiredEnv('HAPPY_PATH_QUEUE_API_KEY');

function requiredEnv(name: string) {
  const value = process.env[name]?.trim();
  if (!value) throw new Error(`Missing required runtime variable: ${name}`);
  return value;
}
```

## Job contract

See the diagram or table on the linked documentation page.

A variable is available only to the Node process running Playwright. It is not automatically available to application JavaScript loaded in Chromium, Firefox, or WebKit.

If the browser page needs the value, pass it explicitly:

```ts
const tenant = requiredEnv('TEST_TENANT');

await page.evaluate((tenantName) => {
  window.localStorage.setItem('tenant', tenantName);
}, tenant);
```

## Names and reserved prefixes

Native SDK bridge variables use the reserved `BERNOPS_` namespace and are injected and rotated by the worker. Jobs must not define or copy them. See [Automation SDK transport selection](/docs/automation-sdk#choose-a-transport).

Variable names use environment-variable syntax: uppercase letters, numbers, and underscores, and they cannot start with a number.

Good names:

- `HAPPY_PATH_QUEUE_API_KEY`
- `HAPPY_PATH_QUEUE_ID`
- `TARGET_TENANT`
- `CHECKOUT_REGION`

Do not use reserved names or prefixes such as `PATH`, `HOME`, `AB_LOG_URL`, `BERNOPS_*`, `SUPABASE_*`, `NEXT_PUBLIC_*`, `PLAYWRIGHT_*`, `npm_*`, or `npm_config_*`.

## Secret behavior

Secrets are stored encrypted and are write-only in the UI. During execution, BernOps decrypts them for the worker process, registers their values with the worker redactor, and avoids exposing the secret value in run metadata.

**What users can see**

Run metadata records which variable names were resolved, whether each was secret, and the variable version. It does not store the secret plaintext.


## Permission model

User JWT runs receive plain project variables by default. Secret variables are injected for project admins only when the run request explicitly sets `include_secret_variables: true`. API-key saved job executions receive secret variables automatically when the key has `variables:use`, unless the request explicitly sets `include_secret_variables: false`.

This matters for saved job execution from CI. If the script calls `requiredEnv('HAPPY_PATH_QUEUE_API_KEY')`, a calling API key with `variables:use` receives that permitted secret by default. A key without `variables:use`, or a request that explicitly disables secret variables, starts without that secret and the script fails with the missing variable error.

## Base URL variables

`BASE_URL` and `TARGET_URL` are special. If the run request does not provide a base URL, BernOps first checks runtime variables with those names. If neither exists, it falls back to `http://localhost:3000`.

The worker also forces both `BASE_URL` and `TARGET_URL` to the final resolved base URL so jobs can consistently use either name.

## Debugging missing variables

When a job reports `Missing required runtime variable`, check these in order:

1. The variable exists on the correct project.
2. The name matches exactly, including underscores.
3. The caller can use secrets: project admin for JWT runs, or `variables:use` for API keys.
4. The variable name is not reserved and was accepted when saved.
5. The run is using the expected project, not another project with a similar job.
