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 = $oopstflw check says unexpected character "$"
test "example" let n = 1e3tflw 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 stringTF003 — 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
TF045 — brackets do not balance
Section titled “TF045 — brackets do not balance”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
TF046 — a tag with no usable name
Section titled “TF046 — a tag with no usable name”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.
@ smoketest "t" api GET /atflw 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
TF048 — a line is indented with tabs
Section titled “TF048 — a line is indented with tabs”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 /atflw 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 ittest "t" api GET /atflw 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
Parser
Section titled “Parser”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 GETtflw 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 200tflw 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 /healthtflw 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 200tflw 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 vissibletflw 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 filetflw 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 200tflw 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.
defaults timeout step 5xtflw check says unknown time unit `x`
test "example" api GET /a expect duration is less than 2sectflw 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 200tflw 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 = ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------1tflw check says too many nested `-` signs
Checker
Section titled “Checker”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/logintflw 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 /healthtflw 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 /atflw 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 200tflw 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 /aaction fetch it() api GET /btflw check says duplicate action "fetch it"
the same name arriving via import "./shared/orders.tflw"
import "./shared/orders.tflw"action fetch it() api GET /atflw 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"
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 /atest "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 /atest "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 200tflw 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 visibletflw 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 visibletflw 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 /atflw 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 /healthtflw 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 rtflw check says does not support `request`
test "example" api GET /a capture status of request to "/a" as stflw 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 1tflw 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 60stflw 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 /healthtflw 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/orderstflw 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/orderstflw 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/xtflw 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 violationstflw check says needs an `authorized target` declaration
test "example" api GET /orders expect response has no security violationstflw 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.
defaults authorized target "https://*.example.com" reason "staging"tflw check says cannot contain a wildcard
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 violationstflw 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 violationstflw check says needs an owner
before file api GET /orders/1 expect response has no authorization violationstflw 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 violationstflw 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 violationstflw 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 violationstflw 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 violationstflw check says takes only `violations` assertions
crawl "the v1 surface" seed traffic expect status equals 200tflw 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”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.
defaults workers 0tflw check says below the smallest value
defaults viewport 0 0tflw check says viewport width 0
test "t" retry 2.5 api GET /a expect status equals 200tflw 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 200tflw 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
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
TF078 — {env(NAME)} inside a string
Section titled “TF078 — {env(NAME)} inside a string”, 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
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 200tflw check says outside the directories
TF084 — a skip whose reason is blank
Section titled “TF084 — a skip whose reason is blank”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 200tflw check says gives no reason
TF085 — a body graphql on a GET
Section titled “TF085 — a body graphql on a GET”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 200tflw 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 strpetflw 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 200tflw 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 cartBadgtflw 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 201tflw 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
TF092 — together where no rows can meet
Section titled “TF092 — together where no rows can meet”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 /atflw 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 /arows expect exactly 1 row status equals 201tflw 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 visibletflw check says can read a row's last response and its bindings
Config
Section titled “Config”TF020 — an unrecognised key inside a config block
Section titled “TF020 — an unrecognised key inside a config block”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”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”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.
workers 3tflw 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”two env … default blocks in one tflw.config
env staging default api "https://a"env prod default api "https://b"tflw check says more than one env is marked `default`
TF025 — a key used in the wrong block
Section titled “TF025 — a key used in the wrong block”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”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
session admin api POST /loginsession admin api POST /logintflw check says duplicate session `admin`
session anonymous api POST /logintflw 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”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
env staging default api "https://a"session admin for env stagng api GET /logintflw 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”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
env one default api shop "https://a" header "X-Tenant" is "acme" for shptflw 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”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
defaults timeout step 10s timeout step 30stflw 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”(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
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”— 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.
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”— 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
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 /logintflw 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”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
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”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
require env API_TOKENenv 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”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 2stflw check says `pause` is only legal inside a workload-bearing `test`
test "example" think 2stflw 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”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 250mstflw check says threshold `for "checkotu"` matches no step in this test
Runtime
Section titled “Runtime”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”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 violationstflw 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”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 violationstflw check says nothing to mutate
TF068 — a crawl with nothing to crawl
Section titled “TF068 — a crawl with nothing to crawl”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 violationstflw 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”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”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