Problem statement
MoSCoW, EARS and Gherkin all make requirements clearer, but none makes them checkable. Your Jira board can’t tell you whether a rule held last March, or which rules have never been tested.
That was fine while a human read every ticket before coding. Now agents do much of that reading. As I argued in The Pull Request Future in the Agentic Era, the leverage is moving upstream to the requirement itself.
Code is becoming a commodity. Leverage is moving upstream, to the requirements layer.
Looking at the data won’t tell you whether you’re compliant. For that, each requirement has to exist as something you can run.
In this post I take two ordinary requirements from financial services and write each one in three different frameworks. Then I turn the requirements into controls, and the controls into stored procedures that re-check them on-demand, with results you can audit.
Three notations, three audiences
Let’s take MoSCoW, EARS, and Gherkin requirement frameworks. They answer three different questions, for three different people.
MoSCoW — Decide what ships
MoSCoW came out of Oracle UK, authored by Dai Clegg in 1994. It classifies requirements in Must, Should, Could, Won’t have this time.
It says nothing about how a requirement gets met. Its real job is to make a room full of people admit that not everything is a Must, before a deadline makes them admit it the hard way. That’s a business conversation, not an engineering one.
And “Won’t” doesn’t mean nobody decided. It’s a decision, written down with a date on it.
EARS — Build it exactly right
Plain prose fails in a predictable way. "The system shall respond quickly" gets through review without a comment, and then five engineers read it five different ways.
The Easy Approach to Requirements Syntax (EARS), developed by Mavin, Wilkinson, Harwood, and Novak at Rolls-Royce fixes that by constraining the grammar rather than the vocabulary.
Five templates cover almost every requirement shape:
Event-driven (WHEN)
State-driven (WHILE),
Optional feature (WHERE)
Unwanted behaviour (IF/THEN)
They do compose, so the fullest form of the sentence reads:
WHILE <precondition>, WHEN <trigger>, the <system> SHALL <response>.
The sentence has no ambiguity, and that’s the point: EARS is aimed at the person who has to implement and test the sentence, not the stakeholder deciding whether it’s worth building.
Gherkin — Proving that it works
Gherkin takes a different starting point: instead of constraining a single sentence, it structures a worked example.
Given/When/Then reads as prose but parses as a test. It names no row and no date: the scenario states the behaviour, and binding it to data is the procedure’s job.
Scenario: The initiator cannot release their own transfer
Given a transfer initiated by an employee
When that same employee submits the release
Then the release is refused and the transfer is not transmitted
Where EARS produces a sentence an engineer implements, Gherkin produces a scenario a test suite runs.
Notice that it is also the only one of the three that produces evidence.
Whether something is specified well and whether it actually worked are different questions, and a dated scenario run on every build answers the second in a way an EARS sentence never can.
The three notations are not three views of one thing: MoSCoW establishes scope, EARS establishes design, and Gherkin establishes evidence. A requirement written in only one of them is weak in the other two.
MoSCoW establishes scope, EARS establishes design, and Gherkin establishes evidence.
Two use cases
Let’s use two simple, unglamorous use cases that are related to controls defined in financial services industry (FSI).
The following examples can be distilled from the COSO framework as part of operational controls to align with SOX regulation, a common regulation body in FSI.
The following are two rules that almost every company above a certain size has written down somewhere, whichever regulator happens to be asking:
Maker-checker validation. Whoever initiates a certain order cannot be the person who approves it — and if they try, the approval is refused rather than logged and allowed. This is the
REQ-1xxblock below.Mandatory leave. Anyone holding a decision-making role takes at least 20 consecutive calendar days of leave each year, on the theory that most concealment needs daily maintenance and will not survive three uninterrupted weeks of somebody else doing the job. This is the
REQ-2xxblock.
The database schema
Let’s define a database schema to support our examples.
Requirementholds one row per rule in plain language, with animportancecolumn carrying its MoSCoW priority and avalidationProcedurenaming the one procedure that runs all of that rule’s controls at once.Controlholds the controls over it, one per EARS sentence or Gherkin scenario.StoredProcedurespoints the other way: each procedure carries thecontrolIdof the control it checks, so a control that nothing checks is simply a control nothing points at.ControlResulthas a dated line for every time a check ran and what it found.
Cilla Ugarte and colleagues open their Compliance-as-Code architecture on something very similar — governance frameworks “specify what to assure but provide no executable format for how” — and answer it by emitting evidence as a byproduct of the work being governed.
The following diagram brings the whole schema together; notice how the requirements tables (left) are separated from the operational data being evaluated (right).
The operational controls are defined as immutable stored procedures, which means they can be recreated anytime with infrastructure-as-code (IaaC) and audited for modifications.
This is the part where AI agents come to scene, creating accurate SQL queries out of requirements and data schemas.
AI Agents implement Text-to-Sql, generating the procedures, then a human validates them.
Stored procedures can be drafted by LLMs through natural-language-to-SQL (text-to-SQL), but review stays human. What accumulates is a library of pre-approved functions, each holding SQL an expert has checked — moving expert attention to validation rather than creation.
This is SME gate is critical in high-risk industries, not just finance (see de Costa et al, 2025 for a concrete example on nuclear power).
An LLM can propose the procedure, an expert approves it, and what the register points at afterwards is an immutable check in the form of a SQL query.
Our test data looks like this:
WT-2002 transfer was initiated and released by the same person. Mei’s leave adds up to twenty days — the number the rule asks for — but across four separate weeks, the longest being five days. Dana took twenty-two consecutive days so the entry is compliant.
Use case 1: maker-checker, in three notations
MoSCoW produces no procedure of its own, just flags the importance of the requirement.

EARS and Gherkin: each one produces a control over the requirement, with its own id and its own statement of how the requirement is pinned down.
We have defined four controls over one requirement: two EARS sentences and two Gherkin scenarios.
EARS: an invariant over the business, stored as a procedure. The following SQL query should return empty.
CREATE MACRO check_req_102() AS TABLE (
SELECT t.transfer_id, t.amount_eur, e.name AS initiated_and_released_by
FROM Transfer t
JOIN TransferRelease r ON r.transfer_id = t.transfer_id
JOIN Employee e ON e.emp_id = t.initiated_by
WHERE r.released_by = t.initiated_by
);Gherkin: an assertion over named rows, carrying its own expected answer. The Given pins the data, Then pins the verdict, so the procedure returns a pass or a fail per scenario rather than a list of offenders to review. Each case names a transfer and expects it to be compliant; the verdict comes from the rule itself, comparing the releasing identity to the initiating one.
The equivalent SQL stored procedure looks like this:
CREATE MACRO check_scenario_102() AS TABLE (
WITH scenario(name, given_transfer, expected) AS (
VALUES ('initiator-cannot-release-own-transfer', 'WT-2002', 'compliant'),
('a-second-identity-releases-the-transfer', 'WT-2001', 'compliant')
)
SELECT s.name AS scenario, s.expected,
CASE WHEN r.released_by = t.initiated_by
THEN 'non-compliant' ELSE 'compliant' END AS actual,
CASE WHEN (CASE WHEN r.released_by = t.initiated_by
THEN 'non-compliant' ELSE 'compliant' END)
= s.expected THEN 'pass' ELSE 'FAIL' END AS result
FROM scenario s
JOIN Transfer t ON t.transfer_id = s.given_transfer
LEFT JOIN TransferRelease r ON r.transfer_id = s.given_transfer
);Neither query could stand in for the other: EARS tells you what the business did, Gherkin is the only one that knows what should have happened.
Use case 2: mandatory leave, in three notations
The mandatory leave receives the same treatment as before. It’s considered a MUST.

And four controls over it.
REQ-201-C4 is an ubiquitous template — no trigger, true of everyone in scope at all times — and the cheapest control in this scenario: an employee who took no leave never appears on a leave-balance exception report, having no balance to be exceptional about.
CREATE MACRO leave_blocks(yr) AS TABLE (
WITH clipped AS (
SELECT employeeId,
greatest(startDate, make_date(yr, 1, 1)) AS startDate,
least( endDate, make_date(yr, 12, 31)) AS endDate
FROM Leaves
WHERE startDate <= make_date(yr, 12, 31)
AND endDate >= make_date(yr, 1, 1)),
ordered AS (
SELECT employeeId, startDate, endDate,
max(endDate) OVER (PARTITION BY employeeId ORDER BY startDate
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS prevEnd
FROM clipped),
blocks AS (
SELECT employeeId, startDate, endDate,
sum(CASE WHEN prevEnd IS NULL OR startDate > prevEnd + 1 THEN 1 ELSE 0 END)
OVER (PARTITION BY employeeId ORDER BY startDate) AS blockId
FROM ordered)
SELECT employeeId, datediff('day', min(startDate), max(endDate)) + 1 AS days
FROM blocks GROUP BY employeeId, blockId
);
CREATE MACRO check_req_201(yr) AS TABLE (
SELECT e.name, e.job_role,
coalesce(sum(b.days), 0) AS total_leave_days,
coalesce(max(b.days), 0) AS longest_consecutive_block
FROM Employee e
LEFT JOIN leave_blocks(yr) b ON b.employeeId = e.emp_id
WHERE e.decision_maker
GROUP BY e.name, e.job_role
HAVING coalesce(max(b.days), 0) < 20
);Both columns are part of the output on purpose. One of them is what a leave-balance report shows; the other is what the requirement actually asked for.
Ten SQL controls now exist for two business rules, and they divide cleanly by notation rather than by use case.
I’ll save you the stored procedures. You can find them on Github.
Running them
The schema, the sample rows, every procedure and the queries that follow live in one script, requirements-validation.sql, which prints these result tables in the order this section shows them.
The two requirements give us eight controls, and each one names a stored procedure. You may wonder where to find such stored procedures. This is easily solved by looking into the catalog:
SELECT sp.controlId, sp.procedureName,
CASE WHEN f.function_name IS NULL THEN 'MISSING' ELSE 'ok' END AS in_catalog
FROM StoredProcedures sp
LEFT JOIN duckdb_functions() f
ON f.function_name = sp.procedureName
AND f.function_type = 'table_macro'
ORDER BY sp.controlId;Use case 1: maker-checker
Let’s see know how to run the procedures and validate our requirements. First, EARS.
SELECT * FROM check_req_102():

Not empty, so not a pass. Mei initiated an 88,000 EUR transfer and released it herself.
SELECT * FROM check_scenario_102():

The same fact, reported differently, and the output is the point: EARS hands back a row to interpret, Gherkin hands back the word FAIL next to the name of the control that was supposed to pass. Again, evidence and expected behaviour.
Use case 2: mandatory leave
We start with EARS checks, remember that we expect it to return an empty output.
SELECT * FROM check_req_201(2026):

Those two numbers are the whole post in one row. Twenty and five, same person, same year: total_leave_days is what the HR report shows, longest_consecutive_block is what the requirement actually asked for.
We check the Gherkin scenarios for this requirement:
SELECT * FROM check_scenario_201(2026):

Both pass, and this is good. We expected a non-compliant leave to be detected, and the scenarios correctly detected a non compliant situation.
Rolling up to the requirements
Running controls one at a time answers one question at a time.
But compliance requires results at requirement level, so we need to evaluate all the controls related to a specific requirement.
This is easily accomplished through a stored procedure to check requirement status:
CREATE MACRO validate_req_102() AS TABLE (
SELECT 'REQ-102-C1' AS controlId, 'EARS' AS methodology,
CASE WHEN (SELECT count(*) FROM check_req_102()) = 0
THEN 'pass' ELSE 'FAIL' END AS result
UNION ALL SELECT 'REQ-102-C2', 'Gherkin',
CASE WHEN (SELECT count(*) FROM check_scenario_102() WHERE result = 'FAIL') = 0
THEN 'pass' ELSE 'FAIL' END
UNION ALL SELECT 'REQ-102-C4', 'EARS',
CASE WHEN (SELECT count(*) FROM check_req_102_attribution()) = 0
THEN 'pass' ELSE 'FAIL' END
UNION ALL SELECT 'REQ-102-C5', 'Gherkin',
CASE WHEN (SELECT count(*) FROM check_scenario_102_attribution() WHERE result = 'FAIL') = 0
THEN 'pass' ELSE 'FAIL' END
);The following validates the maker-checker requirement.
SELECT * FROM validate_req_102()

We validate the mandatory leave requirement in the same way:
validate_req_201()

Neither procedure reports a clean requirement. Every control ran and returned a verdict — a statement supported by the run itself rather than by the document.
Recording what happened
The stored procedures generate output messages of the findings.
This is useful: a run that leaves no trace proves nothing.
The Compliance report
So far we have seen everything that happens under the hood to validate the requirements.
From an auditing perspective, the conversation typically starts from the compliance report overview, so we need to gather all the findings and report them aggregated at requirement level.
Our last stored procedure provides the overview of the requirements, with total number of controls executed and the verdict.
WITH latest AS (
SELECT controlId, arg_max(result, timestamp) AS result, max(timestamp) AS ts
FROM ControlResult GROUP BY controlId
)
SELECT r.reqId, r.importance,
count(DISTINCT c.controlId) AS controls,
count(DISTINCT sp.controlId) AS with_procedure,
coalesce(CAST(max(l.ts) AS VARCHAR), '(never run)') AS last_run,
CASE WHEN count(DISTINCT l.controlId) = 0 THEN '(no evidence)'
WHEN NOT bool_and(l.result) THEN 'FAIL'
WHEN count(DISTINCT l.controlId)
< count(DISTINCT c.controlId) THEN 'incomplete'
ELSE 'pass' END AS verdict
FROM Requirement r
LEFT JOIN Control c ON c.reqId = r.reqId
LEFT JOIN StoredProcedures sp ON sp.controlId = c.controlId
LEFT JOIN latest l ON l.controlId = c.controlId
GROUP BY r.reqId, r.importance
ORDER BY r.reqId;
Both scenarios failed so this verdict is not a surprise.
Conclusions
The three notations complement each other. MoSCoW compiles to nothing at all, importance is an attribute. EARS provides an invariant check where an empty result is the pass. Gherkin gives us assertions carrying its own expected answer.
LLMs convert requirements to SQL code at no cost. Text-to-SQL makes the translation cheap, which moves the cost onto the two things it does not do: writing a sentence precise enough to translate, and reviewing what comes back. The procedures here are small enough to read in full, which is what makes that review possible.
Human in the loop: a subject matter expert is still required to validate the output of the checks and expected behaviour. We didn’t cover this but the review can be assisted by an AI Agent as well (a.k.a. LLM as a Judge).
Compliance is repeatable and auditable. Few tables, consolidated stored procedures, and a handful of queries allow implementing continuous, repeatable compliance.









