Configuration¶
Everything you can tune in Trailblaze, in one place: which configuration surface to use for what, which one wins when two disagree, and whether a change takes effect immediately or needs a daemon restart.
Related pages:
- LLM Configuration — providers, models, API keys, and the
llm:YAML schema. - CLI reference — the generated command reference, including the authoritative
trailblaze configkey table. - Trailmaps and Project Layout — per-target workspace configuration (
trails/config/).
The configuration surfaces¶
| Surface | Use it for | Where it lives |
|---|---|---|
Persistent config keys — trailblaze config <key> <value> |
Your personal defaults (LLM, drivers, screenshot format, experimental toggles) | trailblaze-settings.json (location below) |
Environment variables — TRAILBLAZE_* |
One-off overrides, CI, and kill-switches | Process environment |
| User YAML config | Personal LLM providers, models, and credentials | ~/.trailblaze/trailblaze.yaml |
| Workspace config | Team defaults committed to the repo (LLM config, targets, trailmaps) | trailblaze-config/trailblaze.yaml (or the legacy trails/config/trailblaze.yaml) |
JVM system properties — -D… |
Daemon-process tuning (rarely needed) | Daemon launch command |
| Android instrumentation arguments | On-device SDK behavior in instrumented test runs | am instrument args |
Where settings are persisted¶
Settings live in trailblaze-settings.json, but which copy applies depends on how you run:
- Installed CLI (e.g. Homebrew): the launcher pins one settings directory for both the CLI and the daemon (
-Dtrailblaze.appdata.dir), so there is a single file. - Daemon:
$TRAILBLAZE_HOME/trailblaze-settings.json, default~/.trailblaze/trailblaze-settings.json.~/.trailblazeis also the daemon’s state directory (logs, TLS keystore). - Standalone
trailblaze configinside a git repository (no launcher pin): falls back to<repo root>/.trailblaze/trailblaze-settings.json, so a source checkout doesn’t mutate your machine-global settings. Note this repo-local file does not drive an already-running daemon — daemon-backed runs read the daemon’s own file above.
Run trailblaze config with no arguments to print every key with its current value. A few persisted settings — the logs directory and the daemon ports — have no trailblaze config key at all; see Settings keys with no trailblaze config key.
Precedence¶
Configuration does not have one total ordering because different settings use different subsets of the surfaces. The important resolution chains are:
- LLM providers and models: environment/per-run override → workspace
trailblaze.yaml→ user~/.trailblaze/trailblaze.yaml→ built-in definitions. - Target: explicit per-run selection → persisted non-default
trailblaze config targetselection → workspacedefaults.target→ built-in target. A persisted selection of the neutraldefaulttarget is treated as unset so it cannot mask the workspace default. - Maximum LLM calls: CLI flag →
TRAILBLAZE_MAX_LLM_CALLS→ workspacedefaults.max-llm-calls→ persisted config key → agent default. - Self-heal: CLI flag →
TRAILBLAZE_SELF_HEAL_ENABLED→ persisted config key →false.
Two more deliberate exceptions:
- Experimental toggles (
stream-screenshots,ios-baguette-video,disable-animations,turbo): the env var can only turn the feature on. An explicit falsey env value does not override atruepersisted config — unset the config key to turn the feature off. - Daemon port: a
serverPortpersisted intrailblaze-settings.jsonoutranksTRAILBLAZE_PORTunless the persisted value equals the default (52525, treated as “not set”). If an env override seems ignored, check for a persisted non-default port — seeserverPort/serverHttpsPort. - Daemon ports must stay outside
52530-59529. That range is where Trailblaze allocates per-device ports, and a device’s port is also a host port: the host bridges to the device withadb forward tcp:<port> tcp:<port>, which takes an already-bound host port without reporting an error. A daemon listening in that range can therefore be silently disconnected the moment a device whose id hashes to its port connects. The daemon refuses to start on such a port rather than fail that way later, so keepTRAILBLAZE_PORToutside52530-59529when running parallel daemons. The HTTPS port derives as+1and is checked too, so52529is refused as well. Above the range is worse, not better: it is further into the OS ephemeral range (32768-60999on Linux,49152+on macOS), where an unrelated outbound connection can be assigned the port as its source and the bind then fails outright. Better still is a port below the ephemeral range altogether —31900, the example the CLI’s own help uses. The default52525sits inside that range, so this is a pre-existing risk rather than one parallel daemons introduce, but a port you are choosing fresh is free to avoid it. The Settings UI refuses an in-range port too, andPUT /trailrunner/api/settingsreturns 200 while silently dropping the field, so a saved value can’t brick the next launch.
When changes take effect¶
Each variable below is marked with when its value is applied:
| Marker | Meaning |
|---|---|
| launch | Read once when the consuming process starts. Restart a running daemon if it consumes this variable. |
| command | Read by each CLI process, so a one-shot VAR=x trailblaze … works. |
| session | Resolved at session/run start from the consuming process’s environment and current persisted config. It never changes mid-session. |
| subprocess | Read when a tool subprocess imports its SDK or module. Set it before that subprocess starts. |
| use | Consulted on each use inside the consuming process rather than cached by Trailblaze. A running process still cannot inherit environment changes made later in its parent shell. |
Environment variables belong to a process environment: changing export VAR=... in your shell never rewrites an already-running daemon’s environment. When a row is marked session or use, restart the daemon if you changed that variable outside the daemon process.
Persistent config keys (trailblaze config)¶
The full generated table (valid values, defaults) lives in the CLI reference. Grouped summary:
| Group | Keys |
|---|---|
| LLM | llm (shorthand provider/model), llm-provider, llm-model — see LLM Configuration |
| Devices & drivers | device, android-driver, ios-driver, web-headless, target |
| Agent & runs | agent, mode, max-llm-calls, self-heal, require-steps |
| Screenshots | screenshot-format, screenshot-max-dimensions, screenshot-quality |
| Capture | capture-video (true/false, default off — the persistent opt-in for session video; env twin TRAILBLAZE_CAPTURE_VIDEO, per-run twin --capture-video) |
| Experimental | stream-screenshots, ios-baguette-video, disable-animations, turbo |
Experimental keys are tri-state (true, false, or unset to inherit the default) and each has an env-var twin documented below.
Settings keys with no trailblaze config key¶
Some settings live only in trailblaze-settings.json. trailblaze config neither lists nor sets them — PUT /trailrunner/api/settings writes them — but they are persisted, so they apply to every later CLI run that reads that settings file. Ask a running daemon what it has persisted:
curl -s "localhost:$(trailblaze app --status | awk '/^ *Port:/ {print $2}')/trailrunner/api/settings" | jq '{logsDirectory, serverPort, serverHttpsPort}'
That response echoes the settings file rather than the values in effect: logsDirectory is null whenever it is being derived, and serverPort still reads 52525 when an environment variable moved the daemon. trailblaze app --status prints the port actually in use.
logsDirectory: where session logs land¶
Every session directory — tool logs, screenshots, device.log, trace.json — is written under this path, and it is the default search root for trailblaze profile and trailblaze otel.
- It is persisted, and it outlives the app that set it. Trailblaze App’s Settings → Workspace → Logs directory → Change writes it, and so does switching the workspace (which moves logs to
<workspace>/logs); from then on both daemon-backed and standalone CLI runs use it. Sending"logsDirectory": ""toPUT /trailrunner/api/settingsclears it back to unset. - It can point outside the current checkout, including at a different checkout entirely. Never assume a run’s logs are under the directory you ran from — read the value and use it.
- Unset, it derives from the app data directory. A repo-local app data directory puts logs beside it —
<git root>/logs, since app data is<git root>/.trailblaze— while the machine-global state directory keeps them inside it, at~/.trailblaze/logs(or$TRAILBLAZE_HOME/logs). An installed binary therefore lands on~/.trailblaze/logsand a source checkout on<git root>/logs. - A CLI settings write materializes the derived value. Any
trailblaze config <key> <value>rewrites the file with the currently derived path filled in, so the file usually carries an absolutelogsDirectoryeven when nobody chose one. Move or rename the checkout afterwards and it keeps pointing at the old location. - Changes apply at daemon start. The daemon opens its logs repository once during boot, so run
trailblaze app --stop, thentrailblaze app, after changing this. - Every run writes and reads here. Each host driver path builds its own logging rule against this directory, then reads the finished session back out of it to generate
recording.trail.yaml(copied next to the trail source) and to compare snapshot goldens. That holds fortrailblaze run, Trailblaze App’s Run, and MCP alike. - Read it at runtime from
GET /trailrunner/api/settings→.logsDirectory. The field echoes the persisted value, sonullmeans the process is deriving it.
serverPort and serverHttpsPort¶
The persisted daemon ports, written by Trailblaze App’s Settings → Workspace → Next HTTP port / Next HTTPS port (then restart) or PUT /trailrunner/api/settings. Both are plain integers defaulting to 52525 / 52526.
- A persisted non-default value outranks
TRAILBLAZE_PORT/TRAILBLAZE_HTTPS_PORT. Full order inside the JVM: an in-process override applied at launch → persisted non-defaultserverPort/serverHttpsPort→ the env var →52525(HTTPS derives as HTTP + 1). A value equal to the default is treated as “not set”, which is what lets the env var through. - Set
TRAILBLAZE_PORTto match a persisted port. Thetrailblazelauncher script does not read the settings file: it defaultsTRAILBLAZE_PORTto52525for its own daemon probe and readiness poll. So withserverPort: 51234persisted and no environment variable, the daemon binds51234while the script waits on52525and then reports that Trailblaze never became ready. Export the matchingTRAILBLAZE_PORT, or move a non-default port to the environment variable and leave the persisted field at its default.trailblaze app --statusreports the port the JVM resolved. - A port inside the device-allocation range never takes effect. Trailblaze App’s Settings refuses it, and
PUT /trailrunner/api/settingsreturns 200 while silently dropping the field, because the daemon refuses to start there — and since that route is served by the daemon, a saved value would leave no UI to undo it. See Precedence. - Changes apply at launch, so restart the daemon.
Workspace configuration (trailblaze.yaml)¶
Most projects need no workspace configuration — a single .trail.yaml file is enough. Add a workspace config dir when a team wants to commit shared targets, LLM settings, toolsets, or tools. Two layouts are supported:
my-project/ my-project/
└── trailblaze-config/ └── trails/
└── trailblaze.yaml ← this ├── config/
│ └── trailblaze.yaml ← or this
(trails can live anywhere, └── login/
e.g. next to features) └── trail.yaml
Trailblaze walks up from the current directory (or from the directory containing the trail you invoked) until it finds a trailblaze.yaml in either trailblaze-config/ (the standalone layout) or the legacy trails/config/. The closest ancestor wins; when both layouts exist at the same ancestor, trailblaze-config/ takes precedence and the CLI prints a consolidation warning. Relative paths inside the file resolve against the config directory it sits in — except trails:, which resolves against the workspace root so one value means the same directory under either layout. See Project Layout for the discovery rules.
trailblaze.yaml is configuration, never a trail. Every section is optional, and an empty file is valid.
# trailblaze-config/trailblaze.yaml (or trails/config/trailblaze.yaml) — everything below is optional
defaults:
target: my-app
max-llm-calls: 25
targets:
- my-app
trails: legacy-trails # only when your trails aren't under <workspace-root>/trails
llm:
providers:
openai:
models:
- id: gpt-5.6-terra
defaults:
model: gpt-5.6-terra
Top-level keys¶
| Key | Type | What it does |
|---|---|---|
defaults |
map | Workspace-wide defaults — see below. |
targets |
list of ids | Target-trailmap ids this workspace opts into. Omit to auto-discover every target trailmap under the workspace config dir’s trailmaps/. Listing ids loads only that subset. Each id must name a target trailmap (one with a target: block); library trailmaps enter scope through a target’s dependencies:. |
trails |
path | Directory holding this workspace’s trail files. trailblaze app uses it as the trails directory; otherwise it applies while no trails directory has been picked. See Declaring a trails directory. |
toolsets |
list | Extra toolsets, either inline or pulled in with ref: path/to/toolset.yaml. |
tools |
list | Extra tools, with the same inline-or-ref: shape. |
providers |
list | Reserved for standalone LLM provider files. Provider and model definitions are read from the llm: block today. |
llm |
map | LLM providers, models, and defaults — see LLM Configuration. |
defaults¶
| Key | What it does |
|---|---|
target |
Target-trailmap id used when nothing more specific is selected. It must match a loaded target (case-sensitive); an unknown id is logged and skipped rather than failing the run. |
llm |
Reserved provider/model shorthand. Use llm.defaults.model today. |
max-llm-calls |
Team-wide positive-integer cap on LLM calls per objective. Per-run CLI and environment overrides still win. |
Declaring a trails directory¶
A workspace’s trails directory (the .trail.yaml files) is a different thing from its
config directory (the one holding trailblaze.yaml). They coincide only in the legacy
layout, where the config dir is nested at trails/config/.
Trailblaze’s default guess for the trails directory is <workspace-root>/trails. When a repo
keeps its trails somewhere else, say so:
# trailblaze-config/trailblaze.yaml
trails: legacy-trails
Relative paths resolve against the workspace root — the directory holding trailblaze-config/
(or holding trails/, in the legacy layout) — so one committed value means the same directory
under either layout. A relative value may not escape that root: the file is shared by everyone
who clones the repo, so ../.. would point the whole team’s app, and its recording writes,
outside their checkout. Use an absolute path when you really do mean somewhere else; it can’t be
portable across machines, so it reads as deliberate rather than as a typo.
trailblaze app honors the declaration: started in a repo that declares trails:, it picks the
declared directory as the trails directory. Otherwise the declaration applies while no trails
directory has been picked (see the order below).
The trails directory resolves in this order:
- A directory you picked in Settings (or via
PUT /trailrunner/api/settings). - The workspace’s
trails:declaration. <app data dir>/../trails.
So the declaration answers the question only when you haven’t. Pick a location in Settings and it
wins in every workspace. To hand the decision back, send "trailsDirectory": "" to
PUT /trailrunner/api/settings.
Two things worth knowing:
- Only an explicit declaration takes effect. Omit the key and nothing changes.
Workspaces already using
<workspace-root>/trailsneed no entry. trailblaze appre-anchors a running daemon. It picks the trails directory of the git repository containing your current directory — itstrails:declaration, or the repo root when it declares none — even when the daemon is already up. That repo’s trailmaps, targets anddefaults.targetcome along. Logs and state go under the repo root either way. Other commands reuse the workspace the daemon is on, so runtrailblaze appfrom the workspace you want.
A declared directory that isn’t on disk is logged and ignored rather than failing the launch,
the same way an unknown defaults.target is.
Trailblaze does not write a trails directory into your settings unless you pick one, so “nobody has chosen yet” stays distinguishable from a real choice. A settings file written by an older version carries the default as though it were a choice; a stored value equal to the default is treated as unchosen so the workspace can still answer.
ref: entries¶
toolsets, tools, and providers accept either an inline entry or a pointer to a separate file:
toolsets:
- ref: toolsets/my-toolset.yaml # relative to the workspace config dir
- name: inline-toolset
tools: [tapOn, assertVisible]
Ref paths are always resolved relative to the directory holding trailblaze.yaml. A leading / is stripped and treated the same way, so /foo.yaml is not an escape to the filesystem root. A ref: entry may not carry sibling keys.
Common workspace tasks¶
| I want to… | Go to |
|---|---|
| Use a model that is not built in | Adding a Model |
| Point at a private LLM gateway | LLM Configuration |
| See the persisted per-machine settings currently in effect | trailblaze config show (CLI) |
| Understand workspace discovery | Project Layout |
| Add custom tools to a project | Your First Trailmap |
The user-level ~/.trailblaze/trailblaze.yaml uses the same llm: schema for personal providers, models, and credentials. Workspace LLM configuration overrides it; see LLM Configuration for the merge rules.
Environment variables¶
Only set the variables in this section; variables the framework sets for its own subprocesses are listed at the end.
Daemon and ports¶
| Variable | Default | Applied | Purpose |
|---|---|---|---|
TRAILBLAZE_PORT |
52525 |
launch | Daemon HTTP port. Override to run isolated/parallel daemons. Moves the HTTPS port with it (+1). Must stay outside 52530-59529 (the device-port range; the HTTPS port, +1, is checked too); ideally below 32768 to stay clear of the OS ephemeral range too (31900 works) — see Precedence above. |
TRAILBLAZE_HTTPS_PORT |
HTTP port + 1 | launch | Daemon HTTPS port (the adb-reverse target for on-device logging). Must be outside 52530-59529 — see Precedence above. |
TRAILBLAZE_HOME |
~/.trailblaze |
launch | Relocates the state directory (logs, TLS keystore, settings) — lets concurrent daemons isolate state. |
TRAILBLAZE_DISABLE_DESKTOP_LOG_FILE |
unset | launch | Set to 1 or true to stop a CLI or daemon process from copying its stdout/stderr to $TRAILBLAZE_HOME/desktop-logs/trailblaze.log (trailblaze-<port>.log on a custom port). For a CI job that captures and filters the process’s output itself, so no unfiltered copy stays on the machine after the job. Anything else leaves the copy on. |
TRAILBLAZE_CONFIG_DIR |
cwd walk-up | command | Authoritative override for the workspace config dir (trailblaze-config/ or the legacy trails/config/). Outranks the working-directory walk-up. |
TRAILBLAZE_DISABLE_DAEMON_AUTOSTART |
unset | use | Kill-switch: “daemon not running” becomes an error instead of an implicit background daemon spawn. |
TRAILBLAZE_MCP_REQUEST_TIMEOUT_MS |
630000 |
command | Per-request CLI→daemon MCP timeout. Default is long because agent commands legitimately run for minutes and a target’s scripted launch tool may use the daemon’s whole nested-callback budget (trailblaze.callback.timeoutMs) plus its buffers; lower it to fail fast when triaging a wedged device, and raise it if you raise that budget. Read from the shell that invoked the command, including for the commands the launcher hands to a running daemon, and it bounds the mcp proxy’s hop to the daemon as well – which is the only lever an external agent has, since raising the callback budget lifts the daemon’s per-tool cap above this default. |
TRAILBLAZE_MCP_PREFLIGHT_TIMEOUT_MS |
5000 |
command | How long the CLI’s read-only pre-flight steps wait (the MCP handshake that opens a session, validating a saved session, autodetecting the only connected device) before the command fails with a pointer at trailblaze status and TRAILBLAZE_IO_PARALLELISM. Separate from the request timeout because these run before your command starts, and a starved daemon answers /ping while never answering them. The launcher forwards it to a daemon-executed command, so upgrade the launcher along with the daemon if you need to raise it; an older one drops the value and the default stays in force. Capped at TRAILBLAZE_MCP_REQUEST_TIMEOUT_MS, since a pre-flight bound above the deadline it carves a window out of could never fire. Unlike that variable, this one is read from your shell rather than the daemon’s. Raise it only for a healthy daemon on a slow machine. |
TRAILBLAZE_RUN_POLL_TIMEOUT_MS |
600000 |
command | Inactivity watchdog for trailblaze run: max time a run may go without new progress before the CLI gives up. Not a wall-clock cap — a trail that keeps advancing runs as long as it needs. |
TRAILBLAZE_CLI_PRINT_STACK_TRACES |
unset | use | Print raw stack traces alongside the structured CLI error envelope. |
TRAILBLAZE_IO_PARALLELISM |
512 when the trailblaze launcher starts the JVM |
launch | How many blocking operations the JVM may have in flight at once. Raise it if a target declares hundreds of scripted tools: each one runs in a subprocess that holds a slot for the whole session, and running out does not fail on the daemon — it keeps answering /ping while every real command hangs. The CLI notices on its next device command: its pre-flight handshake and device probes give up after TRAILBLAZE_MCP_PREFLIGHT_TIMEOUT_MS (5 s by default) and name this variable. Read once at JVM start, so a running daemon must be restarted (trailblaze daemon stop) for a change to apply. Must be an integer between 1 and 2147483647; the launcher rejects anything else rather than letting the JVM die on it later. Anything below 17 is accepted but leaves no room for scripted tools, because 16 slots are held back for the host’s own work. A JVM that does not come from the launcher does not read this variable: a java -jar, an Android host, or a Gradle test worker gets the JVM default of max(64, CPU count) instead, and must be given -Dkotlinx.coroutines.io.parallelism=<n> directly. For the packaged desktop app the value is baked in when the installer is built, so changing it on the machine that runs the app has no effect. |
Development-checkout launcher knobs (the ./trailblaze wrapper script; installed CLIs ignore these): TRAILBLAZE_MAX_HEAP (JVM max heap, default 4g — raise for very large workspaces or heap-hungry report generation), TRAILBLAZE_JAR (path to the built uber JAR), TRAILBLAZE_IPC=0 (disable the daemon IPC fast path), TRAILBLAZE_REBUILD_GRADLE_TASK (override the rebuild task), TRAILBLAZE_FORCE_DAEMON_STOP=1 (stop a busy daemon anyway when the rebuilt JAR needs to take effect now — by default a daemon with in-flight runs is left running).
Runs and recordings¶
| Variable | Default | Applied | Purpose |
|---|---|---|---|
TRAILBLAZE_DEVICE |
unset | command | Manual device override for a shell or non-interactive harness. Interactively you rarely set it — trailblaze device connect records a terminal-scoped pin instead. A --device flag still wins. |
TRAILBLAZE_TARGET |
unset | command | Per-shell target pin for forwarded subcommands (clear = unset). |
TRAILBLAZE_MAX_LLM_CALLS |
25 |
command | Per-objective LLM call cap for the built-in Trailblaze Runner and strategy-graph agents (flag → env → workspace defaults.max-llm-calls → config key). |
TRAILBLAZE_SELF_HEAL_ENABLED |
false |
command | Enable self-heal on recorded replays (flag → env → config key). A multi-device session refuses to open with self-heal on — see TRAILBLAZE_DEVICE_BINDINGS below. |
TRAILBLAZE_DEFAULT_MODEL |
LLM config value | session | Overrides defaults.model from the loaded LLM configuration. |
TRAILBLAZE_OLLAMA_NUM_CTX |
65536 |
session | Positive host-side Ollama num_ctx request override. Malformed or non-positive values fall back to 64K. See Ollama (Local Models). |
TRAILBLAZE_DEFERRED_VARIABLES |
empty | command | Comma-separated {{var}} names excluded from environment expansion in trail templates (left for runtime memory instead). |
TRAILBLAZE_MEMORY_DIAGNOSTICS |
false |
session | Memory diagnostics for --capture-memory: every tool call waits for a reading right before and right after it (an exact per-action delta), and on Android the app collects garbage first so the report’s heap used is live objects only. Off by default because it adds two readings to every tool call; by default readings are taken in the background and the run never waits. Read by the daemon — restart it (trailblaze --stop) for a change to apply. Each memory event records gcForced and readMs, so the cost shows in the report. |
TRAILBLAZE_CAPTURE_MEMORY |
unset | session | Kill switch for app-memory sampling. Memory capture is on by default, so this only takes a falsey value (0 or false): set it to stop every session in this process from sampling, without editing a config or a flag. A truthy value reads the same as unset. For use when sampling itself is the suspect — a device that stalls under the extra shell reads, or a CI lane that wants nothing extra touching the app. Read by the daemon, so restart it (trailblaze --stop) for a change to apply. The session log says when the switch is what turned capture off. |
TRAILBLAZE_DEVICE_BINDINGS |
unset | session | Binds the non-start named devices of a multi-device trail’s config.devices: configuration entry to connected devices, as comma-separated name=deviceInstanceId pairs (e.g. buyer=emulator-5556). The configuration’s first declared device is the start device and binds to the launch device automatically; every other declared name needs an entry here — the declared classifier is the trail’s portable contract, but classifier-based auto-binding is not implemented yet. Prefer trailblaze run --bind buyer=emulator-5556 (repeatable) — it is per-run, so it needs no daemon restart and two multi-device trails can run concurrently against different device sets, which one daemon-wide value cannot express. This variable remains the fallback for callers that cannot pass flags. Read by the daemon, so restart it (trailblaze --stop) if it was started without the variable. The exception is a trail that also declares single-device entries beside its configuration: there the value decides whether the run binds the configuration at all, so trailblaze run forwards its own shell’s value on the request, so the daemon runs what the CLI planned. A run request carrying its own bindings — which --bind sets — replaces this value wholesale rather than merging with it. Three rules govern what such a session accepts, all reported at session start before the first step: it must run with self-heal off (--self-heal=false — healing writes a recovered leg back into your trail source and misaligns the steps around it, so legs end up replaying on the wrong display); every switchDevice in a leg the run will actually replay must name a device the configuration declares (a stale name in a leg the run re-blazes past doesn’t stop it, so re-running with --no-use-recorded-steps is how you repair one — trailblaze check still flags it); and a name may not be bound to a device another name already holds. AI-driven steps are supported — the model is told the device roster and can hand over itself — unless the start target’s excluded_tools: drops switchDevice, in which case the session accepts recorded steps only. |
TRAILBLAZE_DEVICE_CONFIGURATION |
unset | session | Names which of a multi-device trail’s config.devices: configuration entries a run binds. Only needed when a trail declares more than one — a trail declaring exactly one binds it implicitly, and a trail declaring several with no selection is rejected rather than defaulting to the first. A trail that also declares single-device entries beside its configuration binds it only when the run binds companions (--bind or TRAILBLAZE_DEVICE_BINDINGS); without them it runs single-device on its classifier legs. A name the trail doesn’t declare is an error; the variable is ignored on trails that declare no configuration at all, so one daemon can serve both. Prefer trailblaze run --configuration <name>, which is per-run and needs no daemon restart. Read by the daemon (restart it if it was started without the variable), and overridden by a selection on the run request itself — which --configuration sets. |
TRAILBLAZE_TRAILS_DIR |
configured trails root | launch | Overrides the Trailblaze App web UI’s primary trails root. Unset, the UI uses the effective trails directory — the launch workspace’s trails: declaration if it has one, else the directory configured in the app (the app-data trails/ dir unless you picked a workspace) — falling back to <cwd>/trails only when that path doesn’t exist. |
Android devices¶
The accessibility-driver switches below are read inside the Android app/service process. Trailblaze does not forward arbitrary host shell environment variables into an already-launched Android process, so treat them as process-local/device-harness controls rather than VAR=x trailblaze run … host overrides.
| Variable | Default | Applied | Purpose |
|---|---|---|---|
ADB_SERVER_SOCKET |
unset | launch | Point at a remote adb server (tcp:<host>:<port>), same semantics as the upstream adb binary. Wins over ANDROID_ADB_SERVER_PORT. |
ANDROID_ADB_SERVER_PORT |
5037 |
launch | Port-only adb server override (host stays localhost). |
TRAILBLAZE_ADB_TIMEOUT_MS |
10000 |
launch | Bound for short-lived host-side adb shell calls. Streaming paths (logcat -f, screenrecord) are exempt. Bump for slow CI emulators. |
TRAILBLAZE_DISABLE_ACTION_CLICK_ROUTE |
unset | use | Kill-switch: force every selector-resolved tap back to coordinate gestures instead of accessibility ACTION_CLICK. |
TRAILBLAZE_DISABLE_TAP_OCCLUSION_WARN |
unset | use | Suppress warn-only diagnostics when another visible node covers the selector-resolved tap point; dispatch is unchanged. |
TRAILBLAZE_DISABLE_TARGET_TYPE_WARN |
unset | use | Suppress warnings when a selector resolves to an unrequested or ambiguous text input; resolution and dispatch are unchanged. |
TRAILBLAZE_DISABLE_SETTLE_TREE_STABILITY |
unset | use | Kill-switch for the capture-time settle gate (tree stability + completeness) — capture immediately with no wait. |
TRAILBLAZE_SETTLE_VIA_WAIT_FOR_IDLE |
unset | use | Kill-switch for the post-action event-quiet settle: restore the legacy UiDevice.waitForIdle() (fixed 500 ms quiet window) after every gesture. |
TRAILBLAZE_DISABLE_BATCHED_TOOL_EXECUTION |
unset | use | Kill-switch: give every recorded tool its own execution context instead of sharing one per recording batch. |
TRAILBLAZE_ANDROID_WIRE_TRANSPORT |
auto |
launch | Host↔device RPC / log-upload wire format: auto, protobuf, or json (rollback switch). |
TRAILBLAZE_CAPTURE_SECONDARY_TREE |
false |
session | Capture a secondary view-hierarchy snapshot per selector tool (driver-migration comparison aid). On the iOS host driver the secondary tree is the raw axe describe-ui accessibility tree, captured alongside the XCTest hierarchy and carried on tap/swipe/input logs as well as the snapshots (requires the axe CLI; without it a capture records no secondary tree). |
iOS simulators¶
| Variable | Default | Applied | Purpose |
|---|---|---|---|
TRAILBLAZE_BAGUETTE |
PATH, then Homebrew |
launch | Explicit path to the baguette binary (optional macOS dependency powering live H.264 simulator streaming). Absent → the device viewer falls back to screenshot polling. |
TRAILBLAZE_BAGUETTE_SERVE_PORT |
8421 |
launch | Port for the shared baguette serve process (loopback only). |
TRAILBLAZE_IOS_BAGUETTE_VIDEO |
unset | session | Experimental (config twin: ios-baguette-video): record session video by muxing the live baguette stream (wall-clock-accurate frame timestamps) instead of simctl io recordVideo. Declines per session when baguette is unavailable. |
TRAILBLAZE_IOS_CLEAR_STATE_MODE |
reinstall | use | AXe clearState route. Set container (case-insensitive) to wipe the app data container in place; any other value keeps uninstall-and-reinstall. Both routes fail if the clear does not complete. |
TRAILBLAZE_DISABLE_AXE_WEB_CONTENT |
unset | use | Kill-switch for AXe WKWebView content descent. The default enables descent when the installed axe binary advertises support. Restart after upgrading axe because that capability probe is cached. |
TRAILBLAZE_CAPTURE_SECONDARY_TREE |
false |
session | Host-side driver-migration aid, read by the process that runs the trail (the daemon unless you pass --no-daemon, so set it where the daemon starts): alongside the XCTest hierarchy, record the raw axe describe-ui tree on every capture and Maestro command log. Needs axe 1.8.0 or newer (AxeCli.MIN_VERSION). Each screen capture adds one axe describe-ui, and a recorded tap makes about three captures with the switch on, so a tap pays about three extra reads and the session log grows by more than that — leave it off for ordinary runs. A missing or too-old axe turns capture off for the rest of the run at once; any other failure is retried, and three in a row turn it off. Either way it says so once on stderr. |
Web and Electron apps¶
| Variable | Default | Applied | Purpose |
|---|---|---|---|
TRAILBLAZE_WEB_LOCALE |
unset | session | BCP-47 language (es, es-US) every Playwright browser session opens in: navigator.language, date and number formatting, and Accept-Language. A trail’s own config.locale (or its device entry’s locale:) wins over it. Set by a locale CI lane such as web-browser-es, alongside --device-classifier. Read by the daemon, so restart it (trailblaze --stop) if it was started without the variable. |
TRAILBLAZE_ELECTRON_COMMAND |
unset | session | Launch command for an Electron app under test (fallback when the target declares no Electron config). |
TRAILBLAZE_ELECTRON_ARGS |
empty | session | Space-separated launch args. |
TRAILBLAZE_ELECTRON_CDP_URL |
unset | session | Attach to an already-running Electron app via this CDP endpoint. |
TRAILBLAZE_ELECTRON_CDP_PORT |
9222 |
session | CDP port used when launching. |
TRAILBLAZE_ELECTRON_HEADLESS |
false |
session | Launch Electron headless. |
TRAILBLAZE_PLAYWRIGHT_DRIVER_REPO |
Maven Central | launch | Extra Maven base URL tried first for the Playwright driver-bundle download (air-gapped/mirrored environments). |
Screenshots and capture¶
The stream and proxy capture switches are experimental and off by default; each STREAM_SCREENSHOT env var has AB-mode and config-key companions.
| Variable | Default | Applied | Purpose |
|---|---|---|---|
TRAILBLAZE_CAPTURE_VIDEO |
unset | session | Record device screen video for every session in this process (config twin: capture-video; per-run flag twin: --capture-video). Video is off by default — recordings are large and their timing signatures drift on some hosts. Set this to get the recording (and the report’s video timeline) for a CI lane or a debugging session without a code change. Only a truthy value opts in — a falsey one reads the same as unset, so unset it to turn video off. Outranked by an explicit --capture-video / --no-capture-video, and outranks the saved capture-video config. |
TRAILBLAZE_WEB_VIDEO_QUALITY |
standard |
session | How sharp web session recordings are. standard is about 0.5 MB per minute with clean text. high keeps small text crisp through scrolling and page changes at about 0.9 MB per minute; a long high session can outgrow the 12 MiB the HTML report embeds (around 14 minutes), and the report then shows per-step screenshots instead of the video. Web only: Android and iOS recordings are unaffected. An unrecognized value is logged and reads as standard. Read by the daemon — restart it (trailblaze --stop) for a change to apply. |
TRAILBLAZE_WEB_REPLAY_CAPTURE |
tree |
session | What a recorded web replay keeps of the page before each action. tree logs the accessibility tree with every element’s box (viewport coordinates) from one ARIA snapshot, so visible-strings.ndjson carries every string on the page and where it sat — about 10ms per action, written off the replay’s thread; the report takes each capture’s picture from the session’s video. tree+jpeg adds a browser-encoded JPEG per action (about 40ms and 40KB), for a report read without its video, such as an export. off keeps only the session’s final capture and its video. Web only. An unrecognized value reads as tree. Read by the daemon — restart it (trailblaze --stop) for a change to apply. |
TRAILBLAZE_ANDROID_STREAM_SCREENSHOT / TRAILBLAZE_IOS_STREAM_SCREENSHOT / TRAILBLAZE_WEB_STREAM_SCREENSHOT |
unset | session | Serve agent-loop screenshots from the device’s live video stream instead of per-turn direct captures (config twin: stream-screenshots, one toggle for all three platforms). Unmatched captures fall back to a direct screenshot. |
…_STREAM_SCREENSHOT_AB (same three prefixes) |
unset | session | A/B validation: direct screenshot stays authoritative, stream matcher runs alongside and logs match/unmatch per capture. Run this before trusting the stream path. |
TRAILBLAZE_DISABLE_ANIMATIONS |
unset | session | Disable OS animations for the duration of each session, restoring previous values at session end (config twin: disable-animations). Android: zeroes the three global animation scales; iOS simulators: near-zero UIAnimationDragCoefficient (deliberately not Reduce Motion, which apps branch on). |
TRAILBLAZE_DISABLE_ENSURE_APP_COMPILED |
unset | session | Kill-switch for the session-start check that the Android app under test has compiled ART artifacts. By default every host-driven Android session reads the app’s dexopt state (dumpsys package, one round trip) and, only when the install has no artifacts at all (status=run-from-apk, which makes every cold start re-verify the whole APK — 7s per launch for a large app), runs pm compile -m verify -f once and confirms the state changed. A healthy install is never written to. The same check is available as a trail step, android_ensureAppCompiled, for runs that have no host. Logs under [dexopt]. Read by the daemon, so restart it (trailblaze --stop) if it was started without the variable. |
TRAILBLAZE_TURBO |
unset | session | Turbo mode: let the Android app under test report when it is idle so the accessibility driver waits less after each action, instead of watching for its screen to go quiet (config twin: turbo; per-run flag twin: --turbo / --no-turbo, which outranks both). Each wait ends at whichever answer arrives first, so turbo can only shorten one — same driver, same selectors, same recordings. Requires an app signed with a certificate Trailblaze carries a matching helper for, so a release or beta build declines and runs at normal speed — as does a trail whose target: names no loaded target, since the run then falls back to the workspace default and turbo will not attach to an app the trail does not drive. Turning it on installs the helper into the app and restarts it. Read by the daemon, so restart it (trailblaze --stop) if it was started without the variable — or prefer trailblaze run --turbo, which is per-run and travels with the request. |
TRAILBLAZE_ANDROID_PROXY_CAPTURE |
unset | session | Single switch for Android network capture via a host-side mitmproxy (emulator-only, API 34+, needs mitmproxy installed): routes the emulator through mitmdump, installs the CA, writes network.ndjson into the session. |
TRAILBLAZE_MITMDUMP |
mitmdump on PATH |
session | Explicit path to the mitmdump binary. |
TRAILBLAZE_NETWORK_CAPTURE_DEVICES |
unset (= every bound device) | session | Comma-separated config.devices: names to arm network capture on in a multi-device session. A multi-device session captures each device separately and suffixes its artifacts with the device name (network.<device>.ndjson, events/<stream>.<device>.ndjson), so every device’s evidence lands in one session. A web device is captured through its browser and writes events/network.<device>.ndjson, but only when named here or, with this unset, when the run passes --capture-network: browser capture records request bodies and full URLs, so it is never armed by default. Narrow it when only some of a pair’s displays run a capture-capable app: capture is load-bearing evidence, so a device whose app never dials in fails a run after the discovery timeout. An MCP session, which may never open its target, records the empty capture instead of failing. A name no bound device matches is logged, not ignored. Read by the daemon, so restart it (trailblaze --stop) if it was started without the variable. |
TRAILBLAZE_SNAPSHOT_BASELINE |
unset | session | Diff each run’s takeSnapshot captures against a PREVIOUS run instead of checked-in golden files: an http(s) URL to a session logs zip, a local zip, or an extracted session directory. Snapshots match by name (and occurrence, for repeated names); a snapshot the baseline lacks is skipped, a mismatch above the threshold fails the run, and an unresolvable reference fails it too — an explicitly requested baseline never silently compares nothing. Read by the process that executes the comparison, so for a daemon-delegated run set it on the daemon (or prefer trailblaze run --snapshot-baseline <ref>, which is per-run and travels with the request). Failing snapshots get a 3-panel Baseline | Diff | Actual PNG written beside the screenshot. |
TRAILBLAZE_SNAPSHOT_BASELINE_THRESHOLD |
2.0 |
session | Pass threshold for the baseline comparison: a snapshot passes when its pixel diff percentage is <= this value. Per-run flag twin: --snapshot-baseline-threshold. |
Scripted tools and the analyzer¶
The tool-definition analyzer extracts JSON Schemas from TypeScript scripted tools in a bun subprocess; inline script: MCP tools run as bun subprocesses too.
| Variable | Default | Applied | Purpose |
|---|---|---|---|
TRAILBLAZE_TOOL_ANALYZER_TIMEOUT_SECONDS |
60 |
launch | Per-trailmap analyzer subprocess timeout. |
TRAILBLAZE_TOOL_ANALYZER_NO_CACHE |
unset | launch | Bypass the workspace-local analyzer cache entirely (reads and writes). |
TRAILBLAZE_SDK_DIR |
walk-up resolution | launch | Explicit path to the scripting SDK directory (installed-CLI scenarios where the SDK isn’t a cwd ancestor). The directory must have its dependencies installed (bun install): an SDK whose deps are missing is ignored for on-device tool bundling — bundling against it would fail every scripted tool — and Trailblaze falls back to its own SDK copy. |
TRAILBLAZE_SDK_PACKAGE |
@trailblaze/scripting |
launch | npm package name that defines the recognized authoring surface. |
TRAILBLAZE_MCP_SUBPROCESS_HANDSHAKE_TIMEOUT_MS |
60000 |
launch | Watchdog on the MCP initialize handshake with each tool subprocess — a hung cold start fails that session’s startup fast instead of wedging the daemon. |
TRAILBLAZE_CLIENT_FETCH_TIMEOUT_MS |
32000 standalone |
subprocess | Client-side fetch timeout read when the tool subprocess imports the SDK; normally forwarded automatically from the daemon’s callback timeout (see JVM system properties). |
trailblaze check¶
| Variable | Default | Applied | Purpose |
|---|---|---|---|
TRAILBLAZE_DISABLE_TRAIL_RECORDING_VALIDATION |
unset | command | Skip the recorded-tool type-validation phase (per-trailmap tsc pass) entirely — shaves latency in a tight inner loop. |
TRAILBLAZE_DISABLE_SELECTOR_DIALECT_GATE |
unset | command | Emergency opt-out from the selector/driver compatibility phase. It skips this trailblaze check phase, but not the unconditional Gradle corpus test. |
TRAILBLAZE_DISABLE_DEVICE_PIN_GATE |
unset | command | Emergency opt-out from the device-pin gate, which fails a trail that declares a device driver outside the classifier it pins (config.driver: beside config.devices:, or a devices: entry keyed driver) and warns on the deprecated bare-string device form. Separate from the selector-dialect switch on purpose, so turning one off never silently drops the other. |
TRAILBLAZE_DISABLE_TRAIL_TARGET_GATE |
unset | command | Silences the trail-target check, which warns (it does not fail the build) when a trail’s config.target: — or a per-device target: override — names an id this workspace cannot resolve. Left to run time, such a target silently falls back to the workspace default and the trail still reports PASSED against a different app than it names. Naming a target is optional: a trail with no config.target: uses the workspace default and is never flagged. Its own switch, like the two above. |
TRAILBLAZE_TYPECHECK_TIMEOUT_MS |
300000 |
command | Timeout for the TypeScript typecheck phase (clamped to ≥ 1 min). |
TRAILBLAZE_TEST_TIMEOUT_MS |
300000 |
command | Timeout for the trailmap unit-test runner. |
In-process test APK signing (trailblaze inprocess make-test-apk)¶
make-test-apk signs its output with the target app’s key, so it needs that keystore’s passwords. They are read from the environment or, failing that, prompted for on the terminal — never from the command line, because argv is visible to ps, lands in shell history, and gets echoed by CI log tracing.
| Variable | Default | Applied | Purpose |
|---|---|---|---|
TRAILBLAZE_INPROCESS_KEYSTORE_PASSWORD |
prompt | command | Password for the --keystore file. |
TRAILBLAZE_INPROCESS_KEY_PASSWORD |
the keystore password | command | Password for the key named by --alias, when it differs from the store’s. |
Runtime scripted-tool bundles (allow_runtime_tool_source)¶
An in-process test APK normally replays with the scripted-tool bundles that were packaged into it, which pins its tool vocabulary to whenever it was built. A host that drives the run itself can instead push bundles matching the trails it is about to replay:
/data/local/tmp/trailblaze/tool-bundles/trails/config/trailmaps/<id>/tools/<stem>.bundle.js
Two gates guard that path, both because the files there are unsigned code that executes inside the app’s process:
- The process is instrumented. A production install of an app that ships these classes never reads tools off disk.
- The target config opted in.
--allow-runtime-tool-sourcewritesallow_runtime_tool_source: trueinto the--target-configbefore signing, so the choice is covered by the signature and is reported in the build record beside the output APK. It is off unless the flag is passed, so an APK produced at a signing ceremony replays from its own frozen assets even when a host drives it — the shell cannot tell one host holding the device from another.
/data/local/tmp is drwxrwx--x shell shell, so only a host holding adb can plant a bundle there; the app process can read one by exact path but cannot write or list the directory. Push bundles world-readable (chmod -R a+rX) — the app process shares neither the uid nor the group of whatever wrote them.
Built-in agent tuning¶
These tune the agent. All are applied per agent run.
| Variable | Default | Purpose |
|---|---|---|
TRAILBLAZE_KOOG_DISABLE_HISTORY_COMPRESSION |
unset | Prune-only context management (no summarization of older turns). |
TRAILBLAZE_KOOG_HISTORY_COMPRESSION_THRESHOLD |
30 |
Message count above which older turns are folded into a summary. |
TRAILBLAZE_KOOG_LOOP_DETECT_THRESHOLD |
3 |
Identical back-to-back tool dispatches before a “loop detected” nudge; <= 0 disables. |
TRAILBLAZE_KOOG_DISABLE_SCREENSHOT |
unset | Send view-hierarchy text only (drop the annotated screenshot) — for A/B or token cost. |
TRAILBLAZE_KOOG_DISABLE_VERIFY_SCOPE |
unset | Keep the full tool surface on verify-only steps instead of scoping to assertion tools. |
TRAILBLAZE_KOOG_DISABLE_VERIFY_EVIDENCE |
unset | Let a verify-only step report COMPLETED with no passing assertion behind it. Independent of the scope switch above. |
TRAILBLAZE_KOOG_DISABLE_FAILED_RETRY |
unset | End a direction step on the model’s first FAILED report instead of retrying it once from a rewound conversation. |
TRAILBLAZE_KOOG_VERIFY_FAST_PATH |
unset | Opt-in, experimental: close a verify step with no LLM call when its claim only asks for quoted phrases to be visible and each phrase is already on screen as an exact element label. 1/strict accepts only claims made of the phrases plus filler words; loose also accepts unquoted qualifiers (not checked). Negations, alternatives, conditions, and unquoted numbers always go to the agent, as does any step where a phrase is missing. |
Decision engine (experimental)¶
A decision engine answers a multiple-choice question with a probability for every option, in well
under a second and for a fraction of a cent. With these set, the Koog agent asks one on every step
request: “which single move next?”, over every tap or check of an element on screen, every offered
tool, and done. It can make the moves it is sure of without an LLM call. A move it makes does not
count against max-llm-calls, except in race mode, where an LLM request was sent for the turn
too; engine moves have their own limit of the same size per objective. The engine is any server
speaking the POST /v1/systemone decision contract; the default is the hosted API that defined it.
The environment is read when each step starts, from the process running the agent: a daemon keeps
the environment it was started with.
| Variable | Default | Effect |
|---|---|---|
TRAILBLAZE_DECISION_MOVES |
unset (off) | shadow: ask on every step request, log the engine’s pick and whether it would have acted, always use the LLM’s move. first (also 1/true, and the one to use): ask the engine, and call the LLM only when it does not act. race: ask the engine and the LLM at once, and drop the LLM request when the engine acts; the dropped request is usually still billed, and with the engine answering in about 0.2s it saves no time over first. The engine acts only on a tap or “done” at or above the threshold, on a step that is not a verification and has no branches (“if”, “on iOS…”); it never taps an element the step already tapped, and never says done before the step has made a move. An engine that errors or takes over 3 seconds leaves the turn to the LLM. |
TRAILBLAZE_DECISION_MOVES_THRESHOLD |
0.95 |
Probability a tap needs before it is acted on, and a “done” too unless the done threshold below is set (with the tree, “done” needs 0.9 unless it is set). |
TRAILBLAZE_DECISION_MOVES_DONE_THRESHOLD |
the threshold above (0.9 with the tree) | Probability a “done” needs before it is acted on. A wrong “done” is usually caught by the next step or a later check, while a wrong tap changes the screen; on a trail’s last step with no check after it, nothing catches it. |
TRAILBLAZE_DECISION_MOVES_QUESTIONS |
flat |
tree asks a decision tree in the same one request (finished, or change the screen; then tap, type or scroll; then which element, quoted text or direction) and also acts on scrolls, typing, and taps on conditional steps. A tap needs the threshold on every answer on its path; a scroll and typing need 0.9, and done needs the done threshold. Typing needs a focused field the tree is sure is empty (typing appends), and is not repeated within a step; after three scrolls the same way in a row, the next one is left to the LLM. |
TRAILBLAZE_DECISION_MOVES_HIDE_TOOLS |
unset | true (or 1), with the tree in first mode: on a turn the engine leaves to the LLM, shows the LLM only the general tools (tap, type, scroll, wait, back, checks, done) when the tree rules out every other tool, plus showTools, which lists the rest one line each and makes the ones the LLM names callable in the same turn. Cuts most of the tool descriptions the LLM reads. A target’s platforms.<platform>.always_shown_tools stay shown too. |
TRAILBLAZE_DECISION_MOVES_SHOW_ALL_TOOLS_AFTER |
unset | A number, with TRAILBLAZE_DECISION_MOVES_HIDE_TOOLS: once that many of a step’s turns have gone to the LLM, it is shown every tool for the rest of the step. |
TRAILBLAZE_DECISION_ENGINE_URL |
https://api.typesafe.ai |
Server root; /v1/systemone is appended. |
TRAILBLAZE_DECISION_ENGINE_KEY |
unset | Bearer key for the server. For the default server, TYPESAFE_API_KEY is used when this is unset; with neither, decisions stay off. A self-hosted server may need no key. |
TRAILBLAZE_DECISION_MODEL |
jev-latest |
Model the engine is asked for. |
What the engine is sent, on every step request: the step’s text, the step’s last five moves (tool names and arguments), the current screen as text (element labels and their refs, capped at 12,000 characters; no screenshot), and the offered tools’ names and descriptions. Treat it like the LLM provider: screen text can hold whatever the app shows, including test account details.
Each question is logged to the session as a decision request (engine, options with tool descriptions
shortened, the ten most likely options, and what was done with it), and a move the engine made shows
in the report as answered without an LLM and is left out of LLM call counts and cost. The first step
prints one [DECISION_MOVES] on: mode=… engine=… threshold=… line, or why decisions stayed off.
On-device Android runs read these from their instrumentation arguments, not an environment.
trailblaze run forwards them itself when decisions are on: set the variables where the daemon
starts, and restart the daemon after changing them, since it reads its environment once. A run
started outside the CLI passes them with the same names (-e TRAILBLAZE_DECISION_MOVES first -e
TYPESAFE_API_KEY …), and -e TRAILBLAZE_USE_RECORDED_STEPS false makes every step go to the agent
instead of replaying its recording.
Tracing detail¶
| Variable | Default | Applied | Purpose |
|---|---|---|---|
TRAILBLAZE_TRACE_LEVEL |
normal |
process | How much of a run is recorded into trace.json: off, normal, or verbose. normal records tools, agent phases, LLM calls and HTTP. verbose adds the fine-grained spans underneath — driver operations, screen-capture internals, per-node selector matching — as each of those layers gets instrumented; a layer that has none yet records the same at both levels. On Android runs driven by the accessibility driver this includes the on-device capture, whose spans land on the profiler’s flat Device lane — though this variable reaches the host and its daemon, not the separate instrumentation process on the device, so those spans still record at normal; see Performance Profiling for how to read them. An instrumented test that drives the in-process ANDROID_TEST driver sets its own level with the trailblaze.trace.level instrumentation argument instead, since there the instrumentation is the run. An unrecognized value is reported and treated as normal. Read from the shell that starts the run and applied to that run alone, so a daemon started at one level does not pin every later run to it. |
verbose is for a specific investigation, not a default. Its spans fire hundreds of times per step,
and a span that costs more than the work it measures changes the shape of what you are profiling.
See Performance Profiling.
Reports and results¶
| Variable | Default | Applied | Purpose |
|---|---|---|---|
MAX_PLAYBACK_WAIT_MS |
600000 |
command | Playback ceiling for MP4, GIF, and WebP report exports. Non-numeric or non-positive values fall back to the default; an overrun still writes a best-effort truncated artifact. |
TRAILBLAZE_REPORT_IMAGE_COMPRESSION_PARALLELISM |
min(cores, 4) |
command | Thread count for report image compression. |
TRAILBLAZE_RESULTS_REPO |
unset | command | owner/name of the results-index repository when --repo isn’t passed to trailblaze results. |
CI systems additionally stamp report metadata via TRAILBLAZE_TARGET_APP, TRAILBLAZE_DEVICES, TRAILBLAZE_BUILD_TYPE, TRAILBLAZE_TEST_RETRY_COUNT, TRAILBLAZE_AI_ENABLED, and TRAILBLAZE_PARALLEL_EXECUTION. The report generator reads them all as labels, but CI pipelines commonly bridge TRAILBLAZE_AI_ENABLED (disables the LLM for the run) and TRAILBLAZE_TEST_RETRY_COUNT (drives retry loops) into real run behavior — don’t treat those two as cosmetic.
OpenTelemetry export¶
Off unless you set an endpoint. These are OpenTelemetry’s own variable names, so a collector or local viewer that is already running needs no Trailblaze-specific configuration.
| Variable | Default | Applied | Purpose |
|---|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
unset | run | Base endpoint every signal shares; /v1/traces is appended. Setting it makes a run send its recorded spans there as soon as it writes trace.json. |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT |
unset | run | Full traces endpoint, path included. Takes precedence over the shared variable, and nothing is appended to it. |
OTEL_EXPORTER_OTLP_PROTOCOL |
inferred | run | grpc or http/protobuf. When unset, port 4317 is treated as gRPC and anything else as OTLP/HTTP. |
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL |
inferred | run | Same, for traces only. Takes precedence. |
OTEL_EXPORTER_OTLP_HEADERS |
unset | run | key1=value1,key2=value2 headers on every export request — what an authenticating collector needs. A value may contain =; only the first one separates. |
OTEL_EXPORTER_OTLP_TRACES_HEADERS |
unset | run | Same, for traces only. Merged over the shared variable per key, so a shared header with no override still applies. |
A run with no endpoint configured sends nothing and still writes trace.json; trailblaze otel
converts a recorded session on demand. See Performance Profiling.
Stability kill-switches¶
Rollback levers for specific framework behaviors — reach for one when a change regresses your pipeline and you need a one-line revert while the underlying issue is fixed.
| Variable | Applied | Reverts |
|---|---|---|
TRAILBLAZE_MEMORY_BLANK_UNKNOWN_TOKENS |
use | Unknown {{var}} tokens resolve to empty strings again instead of failing loudly. |
TRAILBLAZE_DISABLE_BOUNDARY_MEMORY_INTERPOLATION |
use | Disable dispatch-boundary memory interpolation. |
TRAILBLAZE_DISABLE_NESTED_DISPATCH_RECORDING_FILTER |
use | Record nested tool dispatches again. |
TRAILBLAZE_DISABLE_SCRIPTED_ARG_TYPE_COERCION |
use | Disable scripted-tool argument type coercion. |
TRAILBLAZE_DEVICE_DISCOVERY_CACHE_TTL_MS / TRAILBLAZE_DISABLE_DEVICE_DISCOVERY_CACHE |
use | Tune (default 1500 ms) or disable the device-discovery cache. |
Set by the framework, not by you¶
The framework exports these into its own subprocesses; don’t set them yourself. Scripted-tool and MCP-server authors may read the device-context ones as a stable contract:
- Read-only contract for tool subprocesses:
TRAILBLAZE_DEVICE_PLATFORM,TRAILBLAZE_DEVICE_DRIVER,TRAILBLAZE_DEVICE_WIDTH_PX,TRAILBLAZE_DEVICE_HEIGHT_PX. - Internal plumbing:
TRAILBLAZE_SESSION_ID,TRAILBLAZE_SESSION_DIR,TRAILBLAZE_TOOLSET_FILE,TRAILBLAZE_BASE_URL,TRAILBLAZE_SHELL_PID,TRAILBLAZE_LAUNCHER,TRAILBLAZE_INTERACTIVE,TRAILBLAZE_TRAIL_CONTEXT,TRAILBLAZE_SETUP_TRAIL_ID.
JVM system properties¶
Tuning knobs for the /scripting/callback endpoint that backs the TypeScript scripting SDK’s client.tools.<name>(args) round-trip. Defaults are production-ready; override only when a slow emulator or unusual composition graph needs more headroom.
-Dtrailblaze.callback.timeoutMs(defaults to600000) — Per-callback dispatch timeout on the daemon side. Raise when a target tool is legitimately slow (e.g. waiting for a screen to settle on a slow emulator); lower it when the harness around the run stops sooner than this, so the timeout you get names the tool instead of killing the process. One override moves the whole ladder inside the daemon: the subprocess’s fetch timeout, the daemon’s own outer tool call, and the cap the daemon puts on any single MCP tool call all resolve this property at runtime, so none of them can cancel a call before the budget you asked for. What it does not move is the caller’s own deadline —TRAILBLAZE_MCP_REQUEST_TIMEOUT_MSis a separate budget, so raising this past it brings back the failure that budget exists to prevent: the CLI gives up and reports an error for a tool the daemon goes on to finish. Raise both together; the second one is an env var read from the shell that invoked the command, so no rebuild is involved.TRAILBLAZE_CLIENT_FETCH_TIMEOUT_MS(env var, defaults to32000standalone) — Client-side fetch timeout in the subprocess. At runtime the daemon forwards its own timeout value + 2 s as this variable, so the daemon is normally the one that surfaces a structured timeout. If you raisetrailblaze.callback.timeoutMs, raise this in lockstep — otherwise the client aborts the HTTP request before the daemon can return. Sampled once at SDK module load.-Dtrailblaze.callback.maxDepth(defaults to16) — Reentrance cap for recursive callback chains (a subprocess tool dispatching another subprocess tool counts as one level).-Dtrailblaze.callback.maxBodyBytes(defaults to1048576/ 1 MB) — Maximum accepted callback request body size; larger declared bodies are rejected with HTTP 413.
Unrelated to callbacks:
-Dtrailblaze.trace.level(defaults tonormal) — Same values asTRAILBLAZE_TRACE_LEVEL, and checked first, so a single run can override the environment it inherits.
The Homebrew-installed launcher also sets -Dtrailblaze.appdata.dir to pin the settings directory — relevant only if you’re building custom launch wrappers.
On-device Android instrumentation arguments¶
trailblaze.aiEnabled(defaults totrue) - This will have the Trailblaze SDK send all requests to the LLM. Whenfalse, only recordings can be used.trailblaze.reverseProxy(defaults tofalse) - This will enable the reverse proxy for all Trailblaze traffic.- When
false, logging traffic is sent tohttps://10.0.2.2:<httpsPort>, the default Android Emulator networking loopback address. - When
true, the logs are sent throughhttps://localhost:<httpsPort>and usingadb reverse tcp:<httpsPort> tcp:<httpsPort>are forwarded to the host running the Trailblaze app.- This means all Trailblaze SDK Traffic is re-routed through
adband then the logs server reverse proxies the traffic to the final host. - This is important because it allows the Trailblaze Agent to run on-device, but not require a network connection.
- It is also helpful/important because in the future it will allow you to not send your API Keys to the device itself, but add the
Authorizationinformation via the reverse proxy.
- This means all Trailblaze SDK Traffic is re-routed through
trailblaze.httpsPort(defaults to52526, i.e.trailblaze.port+ 1) - The HTTPS port the on-device runner sends logging traffic to. Host-driven runs set this for you and ignore any value you pass: the daemon injects the port its own HTTPS server bound, which is also the port itadb reverses, so the two can never disagree. Setting it yourself only takes effect where no Trailblaze daemon launched the instrumentation — a Gradle-launched instrumented test being the case that matters — and there it should match the server you want logs to reach.trailblaze.logsEndpoint- Defaults to the same values as thereverseProxyuses. You can use this value if you want to use a remote logs server. NOTE: Logging timeouts are set to 5 seconds as they are expected to be fast.- Logging traffic to a device-local endpoint (the default
localhostor10.0.2.2) ignores the device’s global HTTP proxy, so a network capture — including one left behind by a daemon that died mid-run — cannot take the log channel down with it. A remote endpoint you set here keeps the proxy, since on many devices that is the only route to it. trailblaze.selfHeal(unset by default) - Stricttrue/false: enable self-heal for on-device runs. Unset (or an invalid value) defers to the host-side resolution, same parser asTRAILBLAZE_SELF_HEAL_ENABLED.trailblaze.driverType(unset by default) - Force override for the on-device driver (e.g.ANDROID_ONDEVICE_ACCESSIBILITY); when set, the per-trailconfig.driverYAML value is skipped entirely.trailblaze.target(unset by default) - Selects which injected target config an in-process test APK runs against, by itsid. Unset, the APK uses the single injected config it carries; an APK carrying more than one refuses to guess and names the ids it has. An id the APK does not carry is an error rather than a silent fall-through to the built-indefault.trailblaze.turbo(defaults tofalse) - Stricttrue/false(anything else is read asfalseand warned about): turbo for a run that has no host. The on-device runner installs the idle-detector APK it carries in its own assets, attaches it to the app under test and switches the settle race on, so waits after each action end as soon as the app reports itself idle rather than when its screen has been quiet long enough. On-device counterpart ofTRAILBLAZE_TURBO, and the two spell the same feature: same detector, same protocol, same[turbo]log lines. Note the env var also accepts1, while this arg does not — passtrue. Requires an idle-detector build signed with the app’s own signing key to be staged in the test APK (the Trailblaze Android Gradle plugin’sinProcessIdle { }block); a run with none, or one whose attach fails for any other reason, says so and replays at normal speed. Turbo never fails a run unless you ask it to withtrailblaze.turbo.required. Only applied when the resolved driver isANDROID_ONDEVICE_ACCESSIBILITY— that driver owns the settle gates that read the detector, so any other driver would pay the attach (including a restart of the app under test) for a signal nothing reads.trailblaze.turbo.required(defaults tofalse) - Stricttrue/false: makes a failed turbo attach fail the run instead of quietly replaying at normal speed. Only meaningful alongsidetrailblaze.turbo. For runs that measure turbo rather than merely benefit from it: the[turbo]lines saying whether the detector attached go to the device log, which a remote device-farm run does not collect, so without this a run that never got turbo is indistinguishable from one that did — both pass. With it, the pass/fail result itself carries the answer. The failure is a distinct exception type (OnDeviceTurbo.TurboRequiredButUnavailableException) so it reads as “this run did not get turbo” rather than as a failing trail. Gates both the attach before the trail and the re-attach around eachlaunchApp, so a trail whose first step force-restarts or clears the app under test — which discards the first attach, since the detector dies with the app’s process — still cannot pass while replaying at heuristic speed. Enforced by the on-device runner, so a run driven by a host instead gets best-effort turbo regardless of this arg.trailblaze.captureSecondaryTree(defaults tofalse) - Stricttrue/false: also dump the legacy UiAutomator view hierarchy on every capture and use it as the capturedviewHierarchy(selector-migration aid; roughly doubles per-step capture latency and session-log size). On-device counterpart ofTRAILBLAZE_CAPTURE_SECONDARY_TREE.
Diagnostic log prefixes¶
Most subsystems tag their diagnostic lines with a bracketed prefix, so you can grep one subsystem out of a verbose run. These go through the console logger, which is suppressed in CLI quiet mode — run with -v or inspect $TRAILBLAZE_HOME/daemon.log for a detached daemon. Desktop logs use $TRAILBLAZE_HOME/desktop-logs/trailblaze.log on the default port and trailblaze-<port>.log on a custom port.
| Prefix | Subsystem |
|---|---|
[AndroidHostAdbUtils] |
Host-side adb (timeouts, env overrides, client eviction) |
[tap-route], [tapByActionClickOnBounds] |
Android selector-resolved tap routing |
[tap-occlusion], [tap-target-type] |
Android tap-overlay and selector-target warnings |
[hideKeyboard] |
IME dismissal routing |
[settle], [capture-coverage] |
Android settle gates and tree-completeness checks |
[ToolBatchScope] |
Batched recorded-tool execution |
[ScriptedToolDefinitionAnalyzer] |
Scripted-tool schema analyzer |
[McpSubprocessSession] |
Tool subprocess handshake watchdog |
[CliMcpClient], [DaemonClient] |
CLI→daemon request timeouts and run polling |
[KOOG], [KOOG_PRUNE], [KOOG_COMPRESS], [KOOG_SCREENSHOT], [KOOG_VERIFY_SCOPE] |
Strategy-graph agent |
[stream-screenshot] |
Stream-sourced screenshots (including AB-mode lines) |
[baguette-video], [IosBaguetteServer], [devices-stream] |
iOS streaming and stream-sourced video |
[disable-animations] |
Session animation disabling |
[turbo] |
Turbo mode attach and its per-session outcome |
[dexopt] |
Session-start check that the Android app under test has compiled ART artifacts, and the one-time repair when it does not |
[mitm-capture] |
Android proxy network capture |
[AxeDeviceManager] |
iOS AXe clear-state routing |
selector-dialect gate (FATAL) |
Selector/driver incompatibilities found by trailblaze check |