Scenario: TC_PAY_017 - verify payment API returns 201
Given I set base URI to "{{env.base_url}}"
And I set header "Authorization" to "Bearer ${TOKEN}"
And I have a request body from "payloads/create_payment_valid.json"
When I send a POST request to "/api/v2/payments"
Then the response status code should be 201
And the response JSON path "$.data.status" should equal "SETTLED"
And the DB table "payments" should have 1 row with status "SETTLED"
That's Gherkin. It's Given/When/Then. It sits in a .feature file, it runs in Cucumber, and by every ceremonial measure it is Behaviour-Driven Development.
Now hand it to the person who actually knows what a settled payment means to the business — and watch them stop reading at line two.
What BDD actually promised
BDD's original pitch was not a file format and not a test runner. It was a shared understanding: the business, the developer, and the tester describe the behaviour together, in one vocabulary everyone owns, before the code exists — and the development is driven by that description.
Dan North started reframing TDD around the word behaviour in 2003, and "Introducing BDD" appeared in Better Software in March 2006 — twenty years ago. So this isn't a young idea we're still figuring out. BDD has had twenty years to take hold, and for most of the last ten, getting it wrong has been the norm rather than the exception — long enough that plenty of working developers have never seen a .feature file a business person actually read.
The description was supposed to be the primary artefact. Executable specification was the bonus, not the point. Read the early writing and the emphasis is unmistakable: the conversation first, the automation second.
That idea was right. It's still right. We just stopped doing it.
How the .feature file drifted
Here's the honest mechanism, and it isn't anyone's laziness.
It starts with one detail, and that detail explains everything after it. Those Given/When/Then lines are not just prose. Every sentence has to be connected to a piece of programming code that knows how to carry it out. Write "Given the customer has a saved card" and somebody must write the code that actually creates that customer and that card. The connection is matched on the words themselves, so the sentence and the code are locked together: change the wording and you change what the software looks for. A sentence with no code behind it does not run — it fails.
So the document meant to hold the business conversation is also the document the automation is bolted to. It sits alongside the source code, engineers edit it, and it breaks the build whenever a sentence and its code fall out of step.
One document, two jobs. And when a document serves two masters, the master that can break the build wins. So the file drifts, predictably, in one direction — toward whatever makes the code behind it easier to write:
- The title stops naming a behaviour and starts naming a test. "A declined card is refused" becomes
TC_PAY_017, because that's the reference the test-tracking tool wants. Givenstops describing a business situation and starts describing machine setup — put this row in the database, attach this security token, load this file of prepared data.Whenstops being what the customer does and becomes what the computer sends — no longer "the customer pays", but "send this message to this address".Thenstops being the promise made to the customer and becomes a list of technical checks — this numeric code came back, this field buried in the response equals that value.- Variations stop being interesting cases and become tables of parameters. Not "a declined card, an expired card, a card in an unsupported currency", but rows of values that exist mainly so nobody has to write the underlying code twice.
Every one of those moves is locally reasonable — each solves a real engineering problem. Together, over two decades, they turn a shared specification into a test script wearing English keywords.
This is why the .feature file is the evidence. Nobody announced that BDD had been abandoned; the file simply records, line by line, which master it ended up serving. Open one and you can read the outcome directly.
The tell is simple: the business stopped coming to the review. Not because they stopped caring — because the document stopped being about them. And once they leave the room, nothing pulls the file back toward their language ever again.
The failure was never Cucumber's
It's worth being fair here, because the usual conclusion is "Gherkin is bad" and that's the wrong lesson.
Cucumber, Reqnroll, Behave, JBehave — these are competent tools that do exactly what they say. What went wrong is what we did with them: we adopted the technology and skipped the practice. We installed a runner, created a features/ folder, wrote step definitions, and told management BDD was done. Nothing in the tooling forced us to hold the conversation — and the conversation was the method. So when time got tight, it was the first thing dropped.
That's the pattern, twenty years running, and it isn't the tools' fault. We did it in the wrong order: we started with the technology and assumed the shared understanding would follow. It doesn't. A test runner can't give you a conversation you never had.
The fix: give the knowledge its own artefact
If one file can't serve both masters, stop asking it to. Separate the knowledge document from the executable test, and author the knowledge first.
Concretely, that means a document with these properties:
- It is not executable. Nothing in it binds to a step definition, so nothing can drag it toward the automation layer. There is no gravity to resist.
- It is written in domain language, by rule. No tooling vocabulary, no endpoints, no status codes, no framework nouns — those belong to the layer below. That rule is an authoring constraint, not something a linter can measure for you; what the tooling enforces is that the file has no reason to reach for those nouns in the first place.
- Every behaviour has a stable ID, so it can be referenced, reviewed, and traced.
- It is validated against a schema. A requirement that's broken, or written where it doesn't belong, fails the build. One that's still an untouched placeholder shows up as a warning. The file can't quietly go stale, because something reads it every time the checks run.
- It is versioned in git and travels with the integration, not stapled to a wiki that was true for a week.
You could build this in-house. Most teams do — and that reflex is why every team has its own framework, its own folder layout, and one maintainer who can't take a holiday. The properties are worth having; rebuilding them team by team, out of the product budget, is not.
Ready-made, not rebuilt
In SYZYGY the knowledge layer is a DOMAIN.yaml for behaviour that spans services, and a PLUGIN-GUIDE.yaml per integration — schema, validation and traceability already there, the same shape in every repo. Two teams who have never met write it the same way, and each can read the other's. Same given/when/then instinct BDD always had, with the automation pressure off:
requirements:
- id: REQ-PAYMENTS-PG-004
behaviour:
given: a customer pays with a card their bank declines
when: the payment is attempted
then: the payment is refused, the customer is told why, and nothing is charged
rules:
- a declined payment never reserves funds
- the customer-facing reason must not expose the bank's raw decline code
errors:
- { code: CARD_DECLINED, meaning: the issuing bank refused the payment }
notes: >
Declines are the most common non-happy path in the payment journey. Retries are the
customer's decision, never automatic — an automatic retry has previously triggered
duplicate-charge complaints.
A business person can read that. Not "could, with training" — can, today. There's nothing in it that requires knowing what a bearer token is, because there's nothing in it that has to compile.
"Isn't that just Gherkin in a different file extension?"
Fair question, and the answer is the direction of the arrow.
In Cucumber, the specification is dragged along by the automation: the file must satisfy the step definitions, so the step definitions shape the file. In a knowledge-first model, the test points back at the knowledge and the knowledge never points forward:
# in the scenario — the test declares what it proves
covers: [REQ-PAYMENTS-PG-004]
Then the loop is closed mechanically, not by good intentions:
syz lint --knowledge # validate the knowledge layer alone — no environment, no tests needed
syz rtm --strict # requirement → test matrix; exit 1 on any uncovered requirement
syz rtm --lastrun # same matrix, annotated with each covering test's latest outcome
That inversion is the whole trick. The requirement doesn't need to know a test exists. The test can't claim coverage it doesn't have. And a requirement nobody has tested shows up as a gap in a traceability matrix that CI can fail on — instead of as a green suite that proves nothing in particular.
The knowledge also outlives the tests that currently cover it. Rewrite the automation, change the framework, replace the endpoint — REQ-PAYMENTS-PG-004 is still the same sentence about the same business rule.
Knowledge first, genuinely first
The strongest version of this isn't "write the doc alongside the test". It's writing the behaviour down before the integration exists at all — no endpoint, no spec, no plugin, nothing to automate against:
syz generate plugin --name payments --guide-only
That scaffolds a knowledge-only shell — the PLUGIN-GUIDE.yaml and an identity plugin.yaml, and none of the automation that usually comes with it — so a BA, PO, or domain expert can record what the integration must do while it's still a conversation. The mechanics get attached later, by whoever builds them. This is the sequence BDD always described and tooling never supported: the .feature file could never come first, because it needed step definitions to be worth anything.
What this doesn't fix — said plainly
We'd rather be useful than persuasive, so:
This does not write your knowledge for you. Someone still has to sit with the domain expert and find out what a declined payment actually means to the business. No format creates that conversation. If your team never had it, a schema won't conjure it.
This does not make Cucumber wrong. If your .feature files are genuinely read by business people, you've already solved the problem and you should keep going. Most teams' aren't, and most teams know it.
What changes is the incentive. The knowledge document is validated, ID'd, traced, and gated in CI — so it can't silently decay into a test script, because the pressure that caused that decay isn't applied to it anymore. That's a structural fix, not a discipline fix — and on the evidence of the last twenty years, the structural kind is the only kind that lasts.
Try it
If your .feature folder stopped being readable to the business a long time ago, this takes five minutes to evaluate:
npm install -g syzs-cli
syz init
Write one requirement — the one you'd most want a new hire to understand — before you write a single test. Then run syz rtm and see what's actually covered.
The tutorial walks the whole path hands-on, and github.com/syzs-code/syz-cli-demo is a complete, runnable workspace you can clone. If BDD failed on your team the way it has failed on most, we'd genuinely like to hear how — email support syz.sh.