DEV Community

Babar Hayat for OpsVeritas

Posted on

The Validation Layer Nobody Talks About: Cardinality Checks in Workflows

Your workflow just completed. Every node executed. The logs are green. But did it produce the data you actually needed?

This is the gap between "execution succeeded" and "execution worked."

Most workflow monitoring—whether you're on n8n, Make, Zapier, or a custom script—answers one question: did the workflow run without errors? It tracks node completion, logs, error states. But there's a silent failure class it misses entirely: a workflow that executes perfectly, returns status 200, and produces zero useful output.

The pattern is simple. A query runs but returns no rows. An API call succeeds but doesn't create the record. A loop runs but finds nothing to process. The workflow succeeded—every node fired, no exceptions—but it failed to do the thing it was meant to do.

That gap is cardinality validation.

What cardinality actually means

Cardinality is just a fancy word for "how much data." It answers three questions:

  • Zero-row cardinality: did the workflow return nothing when it should have returned something?
  • Single-row cardinality: did it return exactly one record when it should have?
  • Multi-row cardinality: did it return a set of records in the expected range?

In hiring, for example: a resume-screening workflow should return 1+ qualified candidates. If it returns zero, the workflow executed fine, but the hiring goal it serves just broke. In a data pipeline: a nightly sync should move 100-10,000 records. If it moves zero, something silently failed upstream, and your data warehouse woke up stale.

The execution succeeded. The cardinality check catches the fact that it was meaningless.

Why this matters

A workflow without cardinality validation is flying blind in a specific, common way. You'll see:

  • Stale data pipelines: a sync runs nightly and reports success, but nobody realizes it's returning zero records until the dashboard gets flagged by a human weeks later.
  • Broken hiring triage: a resume-screening workflow runs cleanly, but returns no qualified candidates. The posting sits untouched until a hiring manager checks manually.
  • Silent data loss: a CDC (change data capture) pipeline exports data successfully but is actually exporting zero rows because the upstream table got truncated.
  • Notification failures: a send-emails workflow runs, logs green, but processed zero recipients because the query condition was wrong.

None of these are errors. All of them are failures. The workflow did exactly what its code told it to do—it just didn't do what the business needed.

Cardinality checks in n8n

In n8n, the pattern is straightforward. After your data-producing node (a query, an API call, a database read), add a validation step that checks the actual output cardinality.

Here's what a single-row expectation looks like:

// Assume the previous node (e.g., 'Query User') returns data in node.data
// This example checks that exactly 1 user was found

const userData = $('Query User').first();

if (!userData) {
  throw new Error('Expected exactly 1 user; found 0');
}

return {
  status: 'valid',
  rows: 1,
  data: userData
};
Enter fullscreen mode Exit fullscreen mode

For a multi-row range (e.g., "expect 10–1000 qualified leads"):

const leads = $('Filter Qualified Leads').all();
const count = leads.length;

if (count < 10) {
  throw new Error(`Expected 10–1000 leads; found ${count} (too few)`);
}
if (count > 1000) {
  throw new Error(`Expected 10–1000 leads; found ${count} (too many)`);
}

return {
  status: 'valid',
  rows: count,
  data: leads
};
Enter fullscreen mode Exit fullscreen mode

And for zero-row detection (a workflow that should find nothing, but you want to know if it did):

const results = $('Search for Duplicates').all();
const count = results.length;

if (count > 0) {
  throw new Error(`Expected 0 duplicates; found ${count}`);
}

return {
  status: 'valid',
  rows: 0,
  data: null
};
Enter fullscreen mode Exit fullscreen mode

The key insight: you're not throwing an error because the workflow failed. You're throwing it because the workflow's output violated its contract—it produced the wrong cardinality.

Once you throw that error, n8n will mark the workflow as failed, and your monitoring (like https://app.opsveritas.com) will catch it as an actual failure, not a silent success.

The broader pattern

This pattern isn't unique to n8n. The same logic applies to:

  • Make: after a search or query module, add a router condition that checks the item count.
  • Zapier: use a conditional step to verify the number of records in the payload before proceeding to the action.
  • Custom scripts or AWS Step Functions: add an assertion after any data-producing step that validates count against expectation.

The point is: don't assume your workflow's output is valid just because it executed. Cardinality is one of the cheapest, highest-signal validations you can add. It catches the failures that status codes miss.

How to think about it

When you're designing a workflow, ask yourself:

  1. What is the normal cardinality of this step's output? (0 rows, 1 row, 10–100 rows?)
  2. If it produced the wrong cardinality, is that a silent failure I'd only catch weeks later?
  3. Can I add a validation check that takes <10 seconds to write?

If the answer to 2 is yes and 3 is yes, add the check. It costs almost nothing upfront and saves you the debugging nightmare of discovering a silently broken workflow months in.

The gap between "execution succeeded" and "execution worked" is where silent failures live. Cardinality validation is how you close it.

Top comments (1)

Collapse
 
mickyarun profile image
arun rajkumar •

Zero-row is the easy half and worth shipping today. The half that got us is plausible cardinality.

A settlement file with the right number of rows and yesterday's rows in them passes every check in this article. So does a sync that moves 4,200 records when it should have moved 4,200 different ones. Cardinality is a proxy for did work happen, and the failures that survive longest are the ones that satisfy the proxy.

The other thing I would change is the constant. 10-1000 leads is a number somebody guessed once. Volume grows, the check fires, someone widens the range because the alert was wrong, and after the third widening the bound is outside anything that can ever happen. The check is now a no-op that still shows green, which is worse than no check because it occupies the slot.

What survived for us was comparing to the same window last period rather than to a constant. Today against the last four same-weekdays, alert on a ratio. It moves with the business, nobody has to remember to update it, and the widening conversation stops happening because there is no constant to widen. Cost is that it needs history and it is blind on day one, so you do want your fixed floor as well. Just be honest that the floor is catching the hard-down case and the ratio is catching everything else.