Skip to content

Diagnostic codes reference

Every stable TF0xx code tflw can print — generated from packages/lang/src/spec-data.ts, the same manifest that regenerates SPEC.md §17 and powers hover-on-error in the editor. A shipped code is never renumbered or reused — gaps in the numbering are reserved, not skipped by accident.

Codes print in every error[TFxxx]: … line, so they're what a CI grep filter, a bug report, or a search anchors on. This page exists so looking one up doesn't require reading the source.

Reuse-hint RF0xx ids are deliberately not listed here. The reuse pass numbers the hints tflw check prints RF001, RF002, … in order of first occurrence within a single scan — add a file that sorts earlier and every id after it shifts. An RF0xx is a handle you type straight back into tflw refactor apply RF001 in the seconds after the scan that printed it, not a stable identity, which is why refactor apply tells you to re-run tflw check for fresh ids rather than trusting an old one. A lookup table of them would be wrong by the next scan, so there isn't one — this page's promise is about TF0xx, and TF0xx alone.

TF001 — a character that cannot begin any token

Section titled “TF001 — a character that cannot begin any token”

Also reported for number notations tflw does not have — 1e3, 0xff, 0b1010, 0o17 and 1_000. Each would otherwise read as a number followed by a name (1e3 as 1 then e3), so the message names the decimal value to write instead. Durations are unaffected: 30s and 500ms are a number followed by a unit, as intended. .5 is not covered, because a dot followed by a number is legal in a path.

test "example"
let y = $oops

tflw check says unexpected character "$"

test "example"
let n = 1e3

tflw check says exponent notation is not supported — this reads as `1` followed by the name `e3`

TF002 — a string literal has no closing quote before end of line

Section titled “TF002 — a string literal has no closing quote before end of line”
test "open string

TF003 — indentation does not line up with any enclosing block

Section titled “TF003 — indentation does not line up with any enclosing block”

A line indented with tabs is TF048.

a line dedented to a column that matches no enclosing block — 4 spaces, then 2, inside a body opened at 4

test "misaligned"
log "a"
log "b"

tflw check says indentation does not match any enclosing block

brackets do not balance — a {/[ that is never closed, or a }/] that closes nothing. An unclosed bracket is reported at the bracket, and only the innermost one: while a bracket is open every following line joins the same logical line, so the outer ones are consequences of the same typo.

test "example"
api POST /o body {

tflw check says this `{` is never closed

a stray }

test "example"
api POST /o body { a: 1 } }

tflw check says `}` closes a bracket that was never opened

a tag with no usable name — a bare @, @ smoke with a space, or @123 starting with a digit. A tag like that could never be written in a --tag expression, so the test could be neither selected nor excluded.

@ smoke
test "t"
api GET /a

tflw check says a tag needs a name after the `@`

TF047 — a string escape outside the supported set

Section titled “TF047 — a string escape outside the supported set”

a string escape outside the supported set — \", \\, \n, \r, \t and \u{XXXX}. "^\d+$" is an error rather than a kept backslash, so that adding an escape later can never change what an existing string means. In a regular expression, write the backslash twice: "^\\d+$". Unicode escapes take braces only: \u0041, \u{}, an unclosed brace, a value above \u{10FFFF} and a surrogate half are all this code.

test "example"
api GET /a
expect body.id matches "^\d+$"

tflw check says unknown escape `\d` in a string

a line is indented with tabs; use spaces. Reported once per file, at the first such line, with the number of other lines in the help — one editor setting fixes them all.

a file indented with tabs

test "t"
api GET /a

tflw check says tabs are not allowed in indentation; use spaces

TF049 — a hidden character that makes the source display differently from how it runs

Section titled “TF049 — a hidden character that makes the source display differently from how it runs”

a hidden character that makes the source display differently from how it runs — a bidi control (U+202A–U+202E, U+2066–U+2069), a zero-width character (U+200B–U+200D), or a U+FEFF anywhere but the very start of the file (the "Trojan Source" attack, CVE-2021-42574). A bidi override in a comment can display as an assertion that is not the one being run, and a zero-width space in a string looks the same as the string without it. An error, because a reviewer reading a .tflw file has only the rendered text to go on. Reported in comments, strings, indentation and between tokens; anywhere else the character is already TF001. Inside a string, write the character as \u{…} if it is really meant.

a comment containing U+202E

# a comment with ‮ in it
test "t"
api GET /a

tflw check says hidden character U+202E RIGHT-TO-LEFT OVERRIDE in a comment

TF050 — a word inside a string that mixes Latin letters with a script that has Latin lookalikes (Cyrillic, Greek, Cherokee, Armenian)

Section titled “TF050 — a word inside a string that mixes Latin letters with a script that has Latin lookalikes (Cyrillic, Greek, Cherokee, Armenian)”

"аdmin" with a Cyrillic а looks exactly like "admin" and compares unequal to it — so in not equals or not contains, the shape a leak check takes, the test passes without asserting anything. The unit is one word, not one string: "Willkommen — добро пожаловать" mixes no single word and is fine, and "東京Tower" is fine because Han has no Latin lookalikes. Not covered: a word written entirely in one non-Latin script. If a mixed word is really meant, write the letter as \u{…}.

expect body.status not equals "оk"

test "example"
api GET /a
expect body.status not equals "оk"

tflw check says the word `оk` mixes Latin with Cyrillic — U+043E

TF010 — a token appeared where the grammar does not allow one

Section titled “TF010 — a token appeared where the grammar does not allow one”

a token appeared where the grammar does not allow one — the catch-all "unexpected token" code, covering many shapes: a missing path after api GET, a multi-word call missing its parens, a table row with the wrong number of cells. When something is left over at the end of a line, the message names what just ended: a step in a test body, a directive in tflw.config, a declaration at a file's top level.

test "example"
api GET

tflw check says expected a path like `/orders`, found end of line

TF011 — an unrecognised keyword where a step was expected, with a did-you-mean for a near miss (expct → expect)

Section titled “TF011 — an unrecognised keyword where a step was expected, with a did-you-mean for a near miss (expct → expect)”

A retired keyword is named with its replacement — uncheck is now untick — and is never offered back as a suggestion. A line of two or more words that reaches no ( is read as a call missing its parens, and told so.

test "example"
expct status equals 200

tflw check says did you mean `expect`?

test "example"
uncheck field "Terms"

tflw check says `uncheck` was renamed to `untick`

TF012 — an unknown HTTP method after api

Section titled “TF012 — an unknown HTTP method after api”
test "example"
api FETCH /health

tflw check says did you mean `PATCH`?

TF013 — an unrecognised expect/capture subject

Section titled “TF013 — an unrecognised expect/capture subject”
test "example"
expect statuss equals 200

tflw check says did you mean `status`?

TF014 — an unrecognised matcher after a subject, or none at all

Section titled “TF014 — an unrecognised matcher after a subject, or none at all”

A bare check <locator> gets its own message naming both readings: check is the soft assertion and needs a matcher, and ticking a checkbox is tick.

test "example"
expect text "x" is vissible

tflw check says did you mean `visible`?

test "example"
check field "Terms"

tflw check says `check <locator>` needs a matcher

TF015 — a block that needs an indented body does not have one

Section titled “TF015 — a block that needs an indented body does not have one”

a block that needs an indented body does not have one — test, action, the hooks, session, session … oauth2, crawl, within, download, switch to new tab, fill form, a with each table, and a defaults/env block in tflw.config. Reported on the block's own header line.

a before file block with no steps under it

before file

tflw check says this `before file` has no steps

TF016 — top-level content that isn't a test/crawl/action/element/import/use/before/after

Section titled “TF016 — top-level content that isn't a test/crawl/action/element/import/use/before/after”
expect status equals 200

tflw check says expected a `test`, `crawl`, `action`, `element`, `import`, `use`, `before`, or `after`, found `expect`

TF023 — a duration whose unit is missing, misspelled, mis-cased, or spaced off its number

Section titled “TF023 — a duration whose unit is missing, misspelled, mis-cased, or spaced off its number”

Three cases, because their fixes differ: an abbreviation written with a space (250 ms → 250ms), a unit tflw spells differently (sec → s, MS → ms), and a word that is no unit at all. Every duration takes ms, s and m, and the words seconds, minutes, hours, days and weeks; a word may stand apart from its number, an abbreviation may not.

tflw.config
defaults
timeout step 5x

tflw check says unknown time unit `x`

test "example"
api GET /a
expect duration is less than 2sec

tflw check says tflw's abbreviated time units are `ms`, `s` and `m` — write `2s`

TF072 — the same column name twice in one with each header

Section titled “TF072 — the same column name twice in one with each header”

A row binds each name once, so the second column would silently replace the first. Reported at the second occurrence, which is the one to rename.

with each
| name | name |
| "a" | "b" |
test "t {name}"
api GET /a
expect status equals 200

tflw check says duplicate table column

TF075 — input nested more deeply than the parser will go

Section titled “TF075 — input nested more deeply than the parser will go”

— more than 256 nested unary minus signs. The message names the limit; at this depth the file was almost certainly generated.

a let whose value carries 300 unary minuses

test "example"
let a = ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------1

tflw check says too many nested `-` signs

TF026 — an api <service>/wait until api <service> name not declared in the active env

Section titled “TF026 — an api <service>/wait until api <service> name not declared in the active env”

an api <service>/wait until api <service> name not declared in the active env — checked in test, action and hook bodies and inside session blocks.

test "example"
api billng POST /auth/login

tflw check says did you mean `billing`?

TF027 — a {col} reference in a test's name that is not among its inline with each table's columns

Section titled “TF027 — a {col} reference in a test's name that is not among its inline with each table's columns”

Only the name is checked here: a bad {col} in the test body is an unbound variable, TF030. File-backed tables (with each from "…") are skipped — their columns are not known until the file is read at run time (TF043 covers the file itself going missing).

test "checkout {prcie}" over a with each table whose only column is price

with each
| price |
| 10 |
test "checkout {prcie}"
api GET /health

tflw check says unknown table column "prcie" referenced in the test name

TF028 — a test … as <session>[, <session>...] name not declared by any session block

Section titled “TF028 — a test … as <session>[, <session>...] name not declared by any session block”

a test … as <session>[, <session>...] name not declared by any session block — one diagnostic per unknown name.

test "x" as ghost
api GET /a

tflw check says unknown session "ghost"

TF030 — a {var}/bare-identifier reference that is never bound anywhere reachable in its scope

Section titled “TF030 — a {var}/bare-identifier reference that is never bound anywhere reachable in its scope”

Conservative: it only flags a name that is definitely unbound, never one that merely might be.

test "example"
api POST /orders
capture body.ok as orderId
api GET /orders/{orderid}

tflw check says unknown variable "orderid"

TF031 — a request assertion (connects/fails) combined with a response assertion (status/header/body/duration) on the same request, or used at all inside wait until api

Section titled “TF031 — a request assertion (connects/fails) combined with a response assertion (status/header/body/duration) on the same request, or used at all inside wait until api”

A request judged on whether it connects has no response to judge as well.

test "example"
api GET /a
expect request connects
expect status equals 200

tflw check says can't be combined with `request connects`/`fails` on the same request

TF032 — an upload … type "…" literal not shaped like type/subtype

Section titled “TF032 — an upload … type "…" literal not shaped like type/subtype”

A light check rather than a list of real media types — it catches an obvious typo before the run.

test "example"
api POST /u upload "./f.png" as "avatar" type "imagepng"

tflw check says invalid content type "imagepng", expected a "type/subtype" shape like "image/png"

TF035 — a name is declared as an action more than once in the namespace a file runs in

Section titled “TF035 — a name is declared as an action more than once in the namespace a file runs in”

a name is declared as an action more than once in the namespace a file runs in — twice in one file, once here and once through an import, or by two imports. Imported duplicates are reported only when the imports were read. element names follow the same rule.

element cartBadge declared twice

element cartBadge = css ".badge"
element cartBadge = css ".count"

tflw check says duplicate element "cartBadge"

action fetch it() declared twice

action fetch it()
api GET /a
action fetch it()
api GET /b

tflw check says duplicate action "fetch it"

the same name arriving via import "./shared/orders.tflw"

import "./shared/orders.tflw"
action fetch it()
api GET /a

tflw check says duplicate action "fetch it" (imported from "./shared/orders.tflw")

TF036 — the active env's own api, api <service> or web base URL has a host that its own allow hosts list (from defaults plus the env) does not match, so every step against it would be refused at run time

Section titled “TF036 — the active env's own api, api <service> or web base URL has a host that its own allow hosts list (from defaults plus the env) does not match, so every step against it would be refused at run time”

Only the active env is checked — a suite may keep a deliberately blocked env as a negative case. The hint says what that key reaches: the default api base takes the whole suite down, a named service only its own calls, web the browser steps. A base URL containing {…} is skipped.

api "http://127.0.0.1:9099" alongside allow hosts "example.com"

tflw.config
env local
api "http://127.0.0.1:9099"
allow hosts "example.com"

tflw check says env `local`'s `api` base URL is "http://127.0.0.1:9099", whose host "127.0.0.1" is not in its own `allow hosts` (example.com)

TF037 — a call names neither an action nor a JS helper, so the run would stop at that step with unknown call

Section titled “TF037 — a call names neither an action nor a JS helper, so the run would stop at that step with unknown call”

Reported only where that is certain: every import was read, the file has no use (a JS module's exports are not known without running it), and the call is in a test or hook body. An action body is skipped, because calls bind against the entry file, so a shared action may call a name only its importer defines.

creat order("Widget") beside action create order(name)

action create order(name)
api GET /a
test "t"
creat order("Widget")

tflw check says did you mean `create order`?

TF038 — a call resolves to a known action but passes the wrong number of arguments

Section titled “TF038 — a call resolves to a known action but passes the wrong number of arguments”

Reported whether or not the file has a use: an action name is unique, and actions are resolved before helpers.

create order("Widget", "extra") against action create order(name)

action create order(name)
api GET /a
test "t"
create order("Widget", "extra")

tflw check says action "create order" expects 1 argument, got 2

TF039 — an expect/check on a response subject (status, duration, header, body …, request), or any capture, comes before the first api/wait until api step in its own response scope

Section titled “TF039 — an expect/check on a response subject (status, duration, header, body …, request), or any capture, comes before the first api/wait until api step in its own response scope”

A test, action or hook body is one scope, and so is each nested within, switch to new tab or download body. An action gets its own, so calling one never hands its response to the caller, and a before hook's response is not visible to the test. UI subjects (a locator, page), request to "…" observations and {variable} subjects need no response and are not checked.

expect status equals 200 as a test's first step

test "example"
expect status equals 200

tflw check says no response yet — an `api` step must run before this assertion/capture

TF040 — a call is written somewhere its value is never computed

Section titled “TF040 — a call is written somewhere its value is never computed”

A call runs in two places only — as a step of its own, and as the whole right-hand side of a let. Anywhere else it would yield nothing: body { id: create thing() } would send {}, and [create thing()] would send [null]. Bind it first — let result = create thing() — then use {result}. TF037 and TF038 are not reported for such a call; its position is the thing to fix first.

test "example"
api POST /orders body { id: create thing() }

tflw check says bind it first — `let result = create thing(…)` — then use `{result}` here

TF041 — a {variable} subject stands where a value cannot

Section titled “TF041 — a {variable} subject stands where a value cannot”

Two cases. A live-handle matcher — is visible/hidden/enabled/disabled/checked, has value, matches snapshot, has no … a11y violations, connects/fails, was made — needs a browser element, a page, a connection or an observed request, and a bound value has none. Type mismatches (contains on a number) are left to the run, as they are for body.<path>. Inside wait until api, a value cannot change between polls, so the wait could only pass at once or time out. Distinct from TF014: is visible is a known matcher, just misplaced.

expect {orderId} is visible

test "example"
api GET /a
capture body.id as orderId
expect {orderId} is visible

tflw check says `is visible` needs a live browser element, page, or request — not a value

TF042 — a matcher used where its subject cannot be read, or an any/all quantifier on a matcher that cannot apply element by element

Section titled “TF042 — a matcher used where its subject cannot be read, or an any/all quantifier on a matcher that cannot apply element by element”

The rule is over the subject's kind — a value, a UI locator, page, request, request to "…" — and follows the matcher table. Shape is not checked: whether body.msg is a string or a list is not known until the response arrives. The quantifier half covers the two matchers that read an outside document, matches schema and matches file. Also checked in wait until <subject> <matcher>. Distinct from TF041, the same rule for a {variable} subject.

test "example"
api GET /a
expect status is visible

tflw check says `is visible/hidden/enabled/disabled/checked` can't be used on a value

test "example"
api GET /a
expect any body.items matches schema "W" from "/o.json"

tflw check says `any` can't be combined with `matches schema "Name" from "src"`

TF043 — a path names a file that is not there

Section titled “TF043 — a path names a file that is not there”

a path names a file that is not there — in import, use, with each from, body from, upload, matches file or drop file, resolved against the directory of the file that names it. Only literal paths are checked; upload "./fixtures/{name}.png" is skipped. import and use are an error, because tflw check opens them itself. The other five are a warning: an earlier step, a hook or a helper may create the file before the step that reads it runs. cert/key in tflw.config are not checked, nor are a CSV file's columns.

import "./nowhere.tflw"
test "t"
api GET /a

tflw check says `import` names a file that does not exist: "./nowhere.tflw"

TF044 — an action that can reach itself, directly (a → a) or through others (a → b → a). tflw has no conditionals, so such a cycle always runs forever and the run can only fail

Section titled “TF044 — an action that can reach itself, directly (a → a) or through others (a → b → a). tflw has no conditionals, so such a cycle always runs forever and the run can only fail”

Checked across imports too, the way a run resolves names; a cycle lying entirely inside imported files is left to those files' own check. Only calls that run count — let x = f() + "y" never calls f. One diagnostic per cycle.

action a() calling b(), action b() calling a()

action a()
b()
action b()
a()

tflw check says this call completes a cycle: `a → b → a`

TF051 — a step needs a base URL the active env does not declare

Section titled “TF051 — a step needs a base URL the active env does not declare”

a step needs a base URL the active env does not declare — open needs web, and an api request with no service name needs the default api. A request through a named service (api orders GET /health) needs only that service. A relative matches schema … from "…" path needs api too; an http(s):// one needs nothing. An error: unlike a missing file, a base URL cannot appear once the run has started.

test "example"
api GET /health

tflw check says needs an `api` base URL

test "example"
open "/login"

tflw check says needs a `web` base URL

TF052 — mask <locator> written after a matcher other than matches snapshot "…"

Section titled “TF052 — mask <locator> written after a matcher other than matches snapshot "…"”

A mask blanks part of a snapshot before comparing it, so with any other matcher it does nothing. One diagnostic per mask.

test "example"
api GET /a
expect status equals 200 mask field "Email"

tflw check says only applies alongside `matches snapshot…

TF053 — capture from a subject that can be asserted on but not stored

Section titled “TF053 — capture from a subject that can be asserted on but not stored”

capture from a subject that can be asserted on but not stored — page, request, a UI locator, or an observed request to "…". capture status as n is fine; capture status of request to "/x" as n is not, because an observed request is read from the browser's network log, not from the last api response. The hint names what the subject does support.

test "example"
api GET /a
capture request as r

tflw check says does not support `request`

test "example"
api GET /a
capture status of request to "/a" as s

tflw check says does not support a `request to…

TF054 — an operand written in the file that the step will reject when it runs

Section titled “TF054 — an operand written in the file that the step will reject when it runs”

an operand written in the file that the step will reject when it runs — random number 5 to 1 (an empty range), random password 2 (too short for the four character classes it guarantees), hex/base64/url decode("…") of a literal that does not decode, or a matches/fails matching pattern that is not a valid regular expression. Literals only: random number {lo} to {hi} is not known until the run.

test "example"
let bad = random number 5 to 1

tflw check says `to` must be ≥ `from`

test "example"
let x = hex decode("not-hex!")

tflw check says is not valid hex

test "example"
api GET /a
expect body.name matches "("

tflw check says invalid regex in matcher

TF055 — wait until <locator> … for <duration> whose hold window is at least as long as the wait's timeout

Section titled “TF055 — wait until <locator> … for <duration> whose hold window is at least as long as the wait's timeout”

The condition is asked to stay true for longer than the step may run, so the step can only time out. A warning: the timeout comes from tflw.config and may differ per env. A step with its own timeout wait <duration> is compared against that; otherwise the active env's timeout wait is used, and with no env resolved the step is skipped.

test "example"
open "/x"
wait until button "Hidden" is hidden for 60s

tflw check says can never be satisfied

TF056 — with each from "…" naming a file that is neither .csv nor .json

Section titled “TF056 — with each from "…" naming a file that is neither .csv nor .json”

Rows are read from CSV (with a header row) or JSON (an array of row objects), chosen by extension. An error, unlike TF043's warning: an extension cannot change between check and run. Interpolated paths are skipped.

with each from "./rows.txt"
test "t"
api GET /health

tflw check says must be `.csv` or `.json`

TF057 — an api, wait until api or open step whose target is an absolute URL rather than a path under the env's base

Section titled “TF057 — an api, wait until api or open step whose target is an absolute URL rather than a path under the env's base”

That is allowed, and this warning says what it costs: the step goes to the same place whatever --env selects. Reported when the env has an allow hosts list, or when no config was resolved; when the resolved env has no allow hosts, TF058 is reported instead.

test "example"
api GET https://api.example.com/orders

tflw check says `--env` will not move it

test "example"
open "https://example.com/checkout"

tflw check says absolute URL

TF058 — an absolute URL in a suite whose env declares no allow hosts

Section titled “TF058 — an absolute URL in a suite whose env declares no allow hosts”

an absolute URL in a suite whose env declares no allow hosts — the run will refuse to send it. Normally no allow hosts means every host is allowed, because every request goes to the env's own base URLs. An absolute URL can reach a host tflw.config never mentions, so using one means declaring where the suite may go: add the host to allow hosts. A warning here, because another env may declare the list; the run itself refuses.

test "example"
api GET https://api.example.com/orders

tflw check says the run will refuse to send it

TF059 — a named service and an absolute URL on the same step

Section titled “TF059 — a named service and an absolute URL on the same step”

a named service and an absolute URL on the same step — api billing GET https://other.example/x. Each one names where to send the request, so one of them would be ignored. An error: no config can make the pair mean one thing. Drop the service, or write a path.

test "example"
api billing GET https://other.example/x

tflw check says names a service and an absolute URL

TF060 — expect/check response has no … security violations against an env whose api base URL

Section titled “TF060 — expect/check response has no … security violations against an env whose api base URL”

expect/check response has no … security violations against an env whose api base URL — or any declared service's URL — no authorized target declaration names. Matching is by origin (scheme, host and port): https://x.example.com does not cover https://x.example.com:8443, which is a different listener. Loopback is not exempt. Only literal URLs the env declares are checked; api "https://{API_HOST}/v1" is skipped, as is a step's own absolute URL.

test "example"
api GET /orders
expect response has no security violations

tflw check says needs an `authorized target` declaration

test "example"
api GET /orders
expect response has no security violations

tflw check says service `@billing`

TF061 — an authorized target that contains a wildcard, or is not an absolute URL

Section titled “TF061 — an authorized target that contains a wildcard, or is not an absolute URL”

Unlike allow hosts, which bounds where a suite may send, this declaration affirms that you may point a scanner at one named host — and nobody is authorized to scan *.com. A bare hostname ("staging.example.com") has no origin, so it would authorize nothing.

tflw.config
defaults
authorized target "https://*.example.com" reason "staging"

tflw check says cannot contain a wildcard

tflw.config
defaults
authorized target "staging.example.com" reason "staging"

tflw check says must be an absolute URL with a scheme

TF062 — the api step an authorization violations assertion judges sets its own Authorization or Cookie header

Section titled “TF062 — the api step an authorization violations assertion judges sets its own Authorization or Cookie header”

The scan replaces the identity headers with each principal's own, so a credential written on the step belongs to no principal it can name, and its findings would be wrong either way. Checked again at run time against what the sessions actually sent. Not detectable: a credential in a query string, a body, or an app-specific header — the run's summary names these blind spots.

test "t" as shopper
api GET /orders/1
header "Authorization" is "Bearer x"
expect response has no authorization violations

tflw check says names its own `Authorization` header

TF063 — an authorization violations assertion with no principal behind it

Section titled “TF063 — an authorization violations assertion with no principal behind it”

Two cases, one repair — declare an identity. (1) The assertion is in a test with no as <session>, or in a before file/after file hook, which belongs to no test; a plain before/after hook runs with each test and is fine. (2) tflw.config marks every session privileged, leaving only the built-in anonymous to probe with, which tests authentication rather than authorization. The scan compares responses across principals, so with no owner, or no one to compare against, it has nothing to compare. Inside an action body the check waits for the run, which knows the calling test.

test "t"
api GET /orders/1
expect response has no authorization violations

tflw check says needs an owner

before file
api GET /orders/1
expect response has no authorization violations

tflw check says needs an owner

TF064 — an authorization violations or input handling violations assertion inside wait until api

Section titled “TF064 — an authorization violations or input handling violations assertion inside wait until api”

That block re-sends its request until its expects pass, so a real finding would be re-probed on every poll and then reported as a wait timeout instead of a finding. Move the assertion to a plain api step after the block. Inside a workload-bearing test the same mistake is TF033.

test "t" as shopper
wait until api GET /orders/1
expect status equals 200
expect response has no authorization violations

tflw check says can't be asserted inside `wait until api`

test "t"
wait until api GET /orders/1
expect status equals 200
expect response has no input handling violations

tflw check says can't be asserted inside `wait until api`

TF066 — an --allow-public-target that matches nothing this run would scan

Section titled “TF066 — an --allow-public-target that matches nothing this run would scan”

an --allow-public-target that matches nothing this run would scan — not an absolute URL, naming an origin no env base URL or service reaches, or naming one no authorized target declares. The flag names an origin so that the permission and the target have to agree; a flag left over after the config changed would otherwise permit a host nobody chose. Repeatable, one origin each, no wildcards. Its reason lives in the authorized target it matches.

test "t" as shopper
api GET /orders/1
expect response has no authorization violations

tflw check says matches nothing this run would scan

TF070 — a step in a crawl body that is not one of the three violations assertions

Section titled “TF070 — a step in a crawl body that is not one of the three violations assertions”

a step in a crawl body that is not one of the three violations assertions — security, authorization or input handling. A crawl is a source of requests, one per discovered route per principal, and each assertion in it judges all of their responses; an api step, or expect status equals 200, has no single response to mean. Put the step in a test. The code TF069 is not used.

crawl "the v1 surface"
seed traffic
api GET /products
expect response has no critical security violations

tflw check says takes only `violations` assertions

crawl "the v1 surface"
seed traffic
expect status equals 200

tflw check says a crawl has many

TF071 — a setting whose value is outside what it can act on

Section titled “TF071 — a setting whose value is outside what it can act on”

parser/checker

workers 0, viewport 0 0, timeout step 0s and retry 2.5 would run something nobody wrote — no workers, a page with no area, an instant abort on every request, a fraction of a retry. Covers workers, viewport, timeout <target>, a test's retry N and an api step's retry honoring "…" up to N. Zero is allowed where it means something: timeout expect 0s and timeout wait 0s evaluate once without polling, and retry 0 is the default written out. The budget timeouts — step, api and browser — refuse 0. Also reported for an unknown tflw:// address (only tflw://demo exists), with a nearest-spelling hint.

tflw.config
defaults
workers 0

tflw check says below the smallest value

tflw.config
defaults
viewport 0 0

tflw check says viewport width 0

test "t" retry 2.5
api GET /a
expect status equals 200

tflw check says is not a whole number

TF073 — an import naming a file that exists but does not parse

Section titled “TF073 — an import naming a file that exists but does not parse”

The run would crash on it. The hint gives the command that shows that file's own errors at their own lines; checking the imported file directly, or a whole directory, reports them too. A missing file is TF043.

import "./broken.tflw"
test "t"
api GET /a
expect status equals 200

tflw check says does not parse

TF077 — an env(NAME) that no require env line declares for where it is read

Section titled “TF077 — an env(NAME) that no require env line declares for where it is read”

require env checks its names before the first request, so a name it does not list would fail mid-suite instead. Checked in tflw.config and in every test file. A test file and anything in the config that applies under every env read the top-level lines only; a line inside an env block may also read that block's own require env, and a session or signer scoped for env one that every env in its scope declares. An error, even though the suite runs while NAME happens to be set; whether it is set is reported by tflw check as a note that does not change the exit code.

a secret read by a test that the config never declares

test "example"
api GET /health
expect body.status equals env(P_ROGUE)

tflw check says no `require env` line declares it

an env reading a secret only another env requires

tflw.config
env staging
require env STAGING_TOKEN
api "https://staging.example.com"
env local default
api "http://localhost:3000"
header "Authorization" is env(STAGING_TOKEN)

tflw check says only `env staging` requires it

, which tflw does not interpolate — the request would send the text {env(NAME)} itself, and the server would answer something like a 401 with nothing naming the cause. Write env(NAME) as the value on its own, outside the quotes. A warning: what a string contains is the author's business, so the check only points it out.

a header written as a braced interpolation of a secret

tflw.config
defaults
header "X-Token" is "{env(P_TOKEN)}"

tflw check says is literal text, not a secret

TF083 — a use that resolves outside the directories helpers allows

Section titled “TF083 — a use that resolves outside the directories helpers allows”

A use runs arbitrary code, so tflw.config's helpers names the directories it may come from, relative to the config; with none declared, they are ./helpers and ./tests/helpers. The path resolves from the checked file, as the run resolves it: use "../../helpers/x.ts" from tests/api/a.tflw lands in helpers/ and passes. tflw run --no-helpers reports every use under this code. A missing file is TF043; here the repair is to move the module or widen helpers.

a use in tests/a.tflw naming ../lib/sign.ts, in a project whose tflw.config declares no helpers

use "../lib/sign.ts"
test "signed"
api GET /health
expect status equals 200

tflw check says outside the directories

The reason is the only record of why the test stopped running and when it comes back. Any non-blank text passes, and an interpolated reason is accepted. An error, as TF082 is.

a test header carrying skip ""

test "t" skip ""
api GET /health
expect status equals 200

tflw check says gives no reason

This body kind is GraphQL-over-HTTP's POST shape — {"query", "variables", "operationName"} as JSON — and a GET carries its query in the URL instead, so on a GET the query is a body most servers ignore and the response answers nothing that was asked. An error: the request cannot do what it says. The repair is the method, api POST ….

an api GET whose body is body graphql

test "t"
api GET /graphql body graphql "{ orders { id } }"
expect status equals 200

tflw check says this request is a `GET`

TF086 — a sign with <name> or session … signed with <name> naming no signer

Section titled “TF086 — a sign with <name> or session … signed with <name> naming no signer”

A step's name is checked against the signers the active env has, the way TF028 checks as <session>; a session's is checked in tflw.config against every declaration. A signer declared for env other envs only is the same code with a hint naming them. An error: the request would go out unsigned, and the server's 401 would not say why.

test "example"
api POST /webhooks/stripe body { id: "evt_1" }
sign with strpe

tflw check says did you mean `stripe`?

TF088 — a skip … on env naming an env tflw.config does not declare

Section titled “TF088 — a skip … on env naming an env tflw.config does not declare”

The skip would hold nowhere, so the test runs in the very env it was written to stay out of, and the run would not say why. Checked against every env block, not only the active one — naming another env is the clause's purpose. One diagnostic per unknown name. An error, matching TF028.

test "refunds settle" skip "no sandbox in CI" on env cii
api GET /health
expect status equals 200

tflw check says did you mean `ci`?

TF089 — an element reference naming no element

Section titled “TF089 — an element reference naming no element”

A bare name in a locator position — click cartBadge, expect cartBadge is visible — is looked up in the file's own element declarations and then each imported file's, the action rule; like an unknown call it is decided only when the imports were read. In a subject position a bare name may also be a value meant as {name}, and the hint says so. An error: the step would have nothing to find.

a file declaring cartBadge and clicking cartBadg

element cartBadge = css "[data-test=cart-count]"
test "t"
open "/"
click cartBadg

tflw check says did you mean `cartBadge`?

TF090 — with each concurrently on an inline table of one row

Section titled “TF090 — with each concurrently on an inline table of one row”

One row has nothing to run beside, so the clause promises an overlap the run cannot produce, and a test written to prove a race would pass without one ever happening. File-backed tables are not judged — their rows are read at run time. A warning: the test still means something; only the clause does not.

a one-row table marked concurrently

with each concurrently
| sku |
| "A-1" |
test "reserve {sku}"
api POST /reserve body { sku: {sku} }
expect status equals 201

tflw check says has one row, so nothing runs beside it

TF091 — a test rebinds a name before file made

Section titled “TF091 — a test rebinds a name before file made”

What before file binds is shared, read-only, by every test, each-scope hook, row and after file in the file — the coupon a race is run over, the product whose stock it drains. A let, a capture or an inline table column of the same name would make one name mean two values depending on where it is read, and under with each concurrently on which row read it. An error: the file runs either reading, and the author meant one.

a test rebinding a before file value

before file
let coupon = "RACE-1"
test "t"
let coupon = "OTHER"
api GET /coupons/{coupon}

tflw check says is made once in `before file` and shared read-only

The barrier holds the rows of a with each concurrently test until every row still running has reached it. In a test whose rows run in turn, in a hook or in an action there is nothing to wait for; inside a block (within, a tab, a download) some rows could pass it while others never reach it. An error: a race written with a barrier that does nothing passes without the race.

together in a test with no concurrent table

test "t"
together
api GET /a

tflw check says whose rows do not run at once

TF095 — a rows block under a test with no with each table

Section titled “TF095 — a rows block under a test with no with each table”

One run has nothing to count across; the judgement belongs in the test body as an ordinary expect. An error.

a rows block under a plain test

test "t"
api GET /a
rows
expect exactly 1 row status equals 201

tflw check says which has no `with each` table

TF096 — a rows line whose subject a finished row cannot answer

Section titled “TF096 — a rows line whose subject a finished row cannot answer”

A row is judged after it ends, from its last response and its bindings; its page is closed by then, so a locator, page, a dialog or a network observation has nothing left to read. An error.

a rows line about a locator

with each
| n |
| 1 |
| 2 |
test "t {n}"
open "/"
rows
expect every row text "Done" is visible

tflw check says can read a row's last response and its bindings

TF020 — an unrecognised key inside a config block

Section titled “TF020 — an unrecognised key inside a config block”

parser (config)

tflw.config
defaults
headr "Accept" is "application/json"

tflw check says did you mean `header`?

TF021 — a test appears in the declaration-only config dialect

Section titled “TF021 — a test appears in the declaration-only config dialect”

parser (config)

tflw.config
test "not allowed here"

tflw check says `test` is not allowed in tflw.config

TF022 — top-level config content that isn't one of defaults, env, session, signer, require, exclude, helpers, or runs

Section titled “TF022 — top-level config content that isn't one of defaults, env, session, signer, require, exclude, helpers, or runs”

parser (config)

Also reported for a session … oauth2 block missing a required line (token url, client id, client secret); the message names only the lines that are missing.

tflw.config
workers 3

tflw check says expected `defaults`, `env`, `session`, `signer`, `require`, `exclude`, `helpers`, or `runs`, found `workers`

TF024 — more than one env marked default, or a duplicate env name

Section titled “TF024 — more than one env marked default, or a duplicate env name”

checker (config)

two env … default blocks in one tflw.config

tflw.config
env staging default
api "https://a"
env prod default
api "https://b"

tflw check says more than one env is marked `default`

checker (config)

tflw.config
defaults
web "https://example.com"

tflw check says `web` is not allowed in defaults

TF029 — a session name that is not the session's alone

Section titled “TF029 — a session name that is not the session's alone”

checker (config)

a session name that is not the session's alone — a duplicate, or the reserved name anonymous. anonymous is the built-in principal every has no authorization violations assertion probes with, so a session by that name would shadow it or be shadowed by it. The repair is the same either way: rename the session.

two session admin blocks in one tflw.config

tflw.config
session admin
api POST /login
session admin
api POST /login

tflw check says duplicate session `admin`

tflw.config
session anonymous
api POST /login

tflw check says is a reserved principal name

TF074 — a session … for env <name> naming an env this config does not declare

Section titled “TF074 — a session … for env <name> naming an env this config does not declare”

checker (config)

The session would exist in no env, so every test … as <name> would fail with TF028, and the session would silently drop out of every authorization scan.

a for env clause naming an env this config does not declare

tflw.config
env staging default
api "https://a"
session admin for env stagng
api GET /login

tflw check says unknown env "stagng"

TF076 — a header "X" is "Y" for <service> naming a service no env declares

Section titled “TF076 — a header "X" is "Y" for <service> naming a service no env declares”

checker (config)

The header would be attached to no request at all, while the config says it is sent. Checked against every service declared anywhere in the file, since a header in defaults may scope to a service only one env declares.

a header scope clause naming a service this config does not declare

tflw.config
env one default
api shop "https://a"
header "X-Tenant" is "acme" for shp

tflw check says unknown service "shp"

TF081 — a single-valued config key set twice in the same block

Section titled “TF081 — a single-valued config key set twice in the same block”

checker (config)

The second would silently replace the first. header, allow hosts, authorized target and redact add up by design and are exempt. Keys with a target are told apart — timeout step from timeout expect, api from api payments — and a key set in defaults and again in an env is how overrides work. Reported at each extra occurrence, the line to delete.

a defaults block setting timeout step twice

tflw.config
defaults
timeout step 10s
timeout step 30s

tflw check says duplicate config key `timeout step`

TF082 — an authorized target whose reason is blank

Section titled “TF082 — an authorized target whose reason is blank”

checker (config)

(empty, or only spaces). The reason is printed in the run summary and the report as the record of why the scan is allowed, and a blank one would look like a considered answer. Any non-blank text passes, and an interpolated reason is accepted. A target that is also malformed reports TF061 too.

an authorized target whose reason is the empty string

tflw.config
defaults
authorized target "https://localhost:8443" reason ""

tflw check says has no reason

TF087 — a signer that cannot sign as written

Section titled “TF087 — a signer that cannot sign as written”

checker (config)

— a {placeholder} the signer does not fill (signs fills {body}, {method}, {path}, {query}, {timestamp} and {body sha256}; a header fills {signature} and {timestamp}), {signature} inside the string being signed, no header line carrying {signature}, or two signers of one name. Each is a request signed over the wrong text or not signed at all.

tflw.config
signer stripe hmac sha256 hex secret env(STRIPE_WEBHOOK_SECRET)
signs "{timestmp}.{body}"
header "Stripe-Signature" is "t={timestamp},v1={signature}"

tflw check says did you mean `{timestamp}`?

TF093 — a step in the sign-in of a session … oauth2 code that sends or reads a request of its own

Section titled “TF093 — a step in the sign-in of a session … oauth2 code that sends or reads a request of its own”

checker (config)

— api, wait until api or capture. The body runs in the browser the flow opened, the way a person signs in on the consent page; the token comes from the code exchange tflw makes after the redirect. An error: a request there would go out without the session it is part of establishing.

an api step in the sign-in

tflw.config
session sso oauth2 code
authorize url "http://localhost:4001/oauth/authorize"
token url "http://localhost:4001/oauth/token"
client id "cli"
redirect "http://127.0.0.1:0/callback"
api POST /login

tflw check says in the sign-in of

TF094 — a session … oauth2 code with no redirect, or one that is not a loopback http URL

Section titled “TF094 — a session … oauth2 code with no redirect, or one that is not a loopback http URL”

checker (config)

The code comes back to a listener tflw binds on this machine, so the redirect names it — 127.0.0.1, localhost or [::1] over plain http, port 0 for one the OS chooses. A redirect written through env(…) is refused the same way at run time. An error: any other host would receive the code where tflw is not listening.

an https redirect to another host

tflw.config
session sso oauth2 code
authorize url "http://localhost:4001/oauth/authorize"
token url "http://localhost:4001/oauth/token"
client id "cli"
redirect "https://app.example.com/callback"
click button "Allow"

tflw check says a loopback listener speaks plain

TF097 — an env block's require env naming a variable the top-level line already requires

Section titled “TF097 — an env block's require env naming a variable the top-level line already requires”

checker (config)

The top-level line requires it under every env, so the block's line changes nothing and reads as if only this env needed it. A warning.

an env block repeating a top-level require env name

tflw.config
require env API_TOKEN
env local
require env API_TOKEN
api "http://localhost:3000"

tflw check says is already required under every env

TF033 — a workload-bearing test is not a valid shape

Section titled “TF033 — a workload-bearing test is not a valid shape”

parser/checker (load)

The cases: its workload or threshold lines are malformed; two such tests in one file share a name (each name keys its own metrics); retry or with each sits beside a workload; a browser step sits inside one (load tests are API-only); pause sits outside one; it has no threshold at all, so it could never fail; it thresholds duration without an unscoped error rate threshold beside it (a duration threshold reads only the requests that succeeded, so alone it passes a target that fails half its requests fast); or it carries an authorization violations assertion, which would multiply that scan's probes by the load. Also reported for the removed keywords scenario (write test "…" with a workload) and think (now pause). The pause and browser-step rules follow calls into actions and report at the call site. To wait for a condition, use wait until …; elapsed time with no condition to poll, such as a cache expiry, belongs in a JS helper.

test "example"
pause 2s

tflw check says `pause` is only legal inside a workload-bearing `test`

test "example"
think 2s

tflw check says `think` was renamed to `pause`

TF034 — a threshold … for "label" names no api step in the same workload-bearing test

Section titled “TF034 — a threshold … for "label" names no api step in the same workload-bearing test”

checker (load)

a threshold … for "label" names no api step in the same workload-bearing test — neither an as "label" tag nor an untagged step's automatic METHOD path name.

threshold p95 duration for "checkotu" is less than 250ms with only an as "checkout"-tagged step in scope

test "t"
hold 5 users for 10s
api GET /a as "checkout"
threshold p95 duration for "checkotu" is less than 250ms

tflw check says threshold `for "checkotu"` matches no step in this test

TF065 — a scan that sends its own traffic (authorization violations) would reach an origin outside the private address ranges, and no --allow-public-target <origin> on the command line names it

Section titled “TF065 — a scan that sends its own traffic (authorization violations) would reach an origin outside the private address ranges, and no --allow-public-target <origin> on the command line names it”

checker and runtime

No tflw.config key can give this permission, so a committed config cannot make CI scan the internet by itself. security violations needs no flag: it only reads responses the suite already asked for. The address class is judged from the URL as written, with no DNS lookup. Private means loopback, RFC 1918, IPv6 unique-local, link-local, CGNAT and localhost; every other hostname counts as public, including api.internal.corp. 0.0.0.0 and :: are refused outright. The run checks again against the origin each probe is actually sent to.

test "t" as shopper
api GET /orders/1
expect response has no authorization violations

tflw check says needs `--allow-public-target https://staging.example.com`

TF067 — an input handling violations assertion on a step whose request has nothing to mutate

Section titled “TF067 — an input handling violations assertion on a step whose request has nothing to mutate”

checker and runtime

an input handling violations assertion on a step whose request has nothing to mutate — no id segment in the path, no query parameter, no JSON body. The scan would send nothing, so the assertion could not fail. The checker reports the case it can be sure of, such as api GET /health; a path with a {var}, body from "…" and raw body "…" text are left to the run, which reports the same code. Assert it on a step that takes an id, a query parameter or a JSON body.

test "t"
api GET /health
expect response has no input handling violations

tflw check says nothing to mutate

checker and runtime

With no seed it sends no request, so no assertion in it could fail. The checker reports a crawl with no seed line; the run reports the same code when the seeds turn up no routes — an OpenAPI document that answers 404, seed traffic when the run captured none, an exclude that covers every route — and when every request was turned away before it reached the application. For that last case the hint points at addressing: a document's paths resolve against its own servers, so an api base carrying the same prefix dials it twice. Reported on the crawl header.

crawl "the v1 surface"
expect response has no critical security violations

tflw check says has nothing to crawl

TF079 — an accept dialog or dismiss dialog that no dialog ever answered

Section titled “TF079 — an accept dialog or dismiss dialog that no dialog ever answered”

runtime (browser)

The step waits for the next native dialog; if none fires before the test ends, the step did nothing. Reported at that step's own line. A warning, and only when the test otherwise passed: after a failure, the step that would have raised the dialog may simply never have run. beforeunload may honestly never appear in a headless run, since browsers raise it only after a real user gesture.

Example a dismiss dialog in a test where no dialog is ever raised — raised at run time, not by tflw check

TF080 — an accept dialog with "<text>" whose answer went to a dialog that takes none

Section titled “TF080 — an accept dialog with "<text>" whose answer went to a dialog that takes none”

runtime (browser)

Only a prompt has a field to fill; an alert or confirm would silently drop the text. A warning, and one tflw check cannot give: which kind of dialog a page raises is only known when it runs. The dialog is still accepted.

Example accept dialog with "Blue" before a click that raises an alert — raised at run time, not by tflw check