Guides / Monitoring guides

Validating workflow output: empty results and invalid JSON

Updated · 5 min read

Treat empty or malformed results as failures. Check for emptiness, valid JSON and required keys on every run.

Most automation monitoring asks one question: did the run finish? That misses a large share of real incidents. The run finished, the dashboard is green, and the output was an empty array, a null field, or an HTML error page where JSON should be. Here's how to treat bad output as a failure, with checks you can set up in a few minutes.

Where empty and invalid output comes from

  • Upstream APIs returning success with nothing in it. A search with an expired filter, a paginated endpoint returning page 0 of 0, a report API that answers {"data": []} while the warehouse load is late.
  • Auth and proxy errors returned as 200. Some services return an HTML login or maintenance page with a 200 status. The next step tries to parse it, or worse, stores it.
  • Mapping drift. A field gets renamed upstream (email → emailAddress), and your mapping now produces empty strings for every record.
  • "Continue on error" settings. In n8n, On Error: Continue and Always Output Data keep a workflow going after a failed node, which is sometimes what you want, but the final output may be empty.
  • LLM output. Truncated responses, JSON wrapped in Markdown fences, refusals. See the AI agent guide.

None of these produce a failed execution. You need a check on the data itself.

Three levels of checking

  1. Not empty. The output has some non-whitespace content. This catches blank responses and empty fields sent as the whole result.
  2. Valid JSON. The output parses as JSON. This catches truncation, HTML error pages and code-fenced text.
  3. Required keys. Specific fields exist and have a value. This catches empty arrays, nulls and renamed fields.

You can do these inside the workflow with IF nodes or filters, and for critical steps you should. Doing them at the monitor as well has two advantages. The checks live in one place for all your workflows. And a failed check produces the same alert, with the same recovery notification, as a run that didn't happen at all.

Step 1: set the rules on a monitor

In Flowhush, create or edit a monitor and open Output checks (Pro and Team):

[x] Body must not be empty
[x] Body must be valid JSON
Required JSON keys (comma separated, dot paths ok): data.items, data.items.0.id, meta.generated_at

How the rules are evaluated, from the implementation:

Body sentRulesResult
(empty body)not emptyFails: Output is empty
<html>Service Unavailable</html>valid JSONFails: Output is not valid JSON
{"data": {"items": []}}key data.itemsFails: Missing or empty JSON key: data.items
{"data": {"items": [{"id": ""}]}}key data.items.0.idFails: empty string
{"count": 0}key countPasses: 0 is a value
{"ok": false}key okPasses: false is a value
[{"id": 7}]key 0.idPasses: top-level arrays work

Details worth knowing:

  • A key counts as missing if it's absent, null, an empty or whitespace-only string, [] or {}. Numbers and booleans always count as present, so check values like "count must be above zero" in the workflow itself.
  • Required keys imply valid JSON, and you can set up to 20.
  • Validation applies to success pings sent with POST or PUT. A plain GET ping has no body, so on a monitor with rules it fails as empty. Switch your ping step to POST when you add rules.
  • Up to 64 KB of the body is read. A larger JSON body gets cut off and fails as invalid, so send a summary rather than the full dataset.
  • A failed check returns HTTP 200 with OK (recorded as failure: …), so the workflow itself doesn't error. The monitor goes down and you get one alert with that reason. The first 1 KB of the body is kept in Recent pings.

Step 2: send the output

n8n

Put an Edit Fields (Set) node before the ping if you only want to send a summary, then an HTTP Request node:

Method: POST
URL: https://flowhush.com/p/YOUR-MONITOR-UUID
Send Body: on
Body Content Type: JSON
Specify Body: Using JSON
JSON: {{ JSON.stringify($json) }}
Settings → Execute Once: on
Settings → On Error: Continue

With Execute Once on, $json is the first item. To check that the whole run produced items, send all of them as an array and require 0.id:

JSON: {{ JSON.stringify($input.all().map(item => item.json)) }}

If the previous node can output nothing at all, n8n stops the branch there and the ping never runs. That's also a failure, caught by the deadline. To have it reported immediately instead, turn on Always Output Data on that node so the ping still runs, and the empty body fails the check.

Make

Add HTTP → Make a request as the last module:

URL: https://flowhush.com/p/YOUR-MONITOR-UUID
Method: POST
Body content type: application/JSON
Body input method: JSON string
Body: (see below)

Building JSON by hand in a text field breaks as soon as a mapped value contains a quote or a newline. Use the JSON → Create JSON module first, with a simple data structure (items_count, first_id, generated_at), and map its output into the body.

Any script

# jq builds valid JSON safely; curl posts it
jq -n --argjson items "$(cat items.json)" '{data: {items: $items}, meta: {generated_at: now|todate}}' |
  curl -fsS -m 10 --retry 3 -X POST -H 'Content-Type: application/json' \
       --data-binary @- https://flowhush.com/p/YOUR-MONITOR-UUID

Because a failed check still returns 200, curl -f won't exit non-zero. If a script needs to know, check the response text:

resp=$(curl -sS -m 10 -X POST -H 'Content-Type: application/json' \
       --data-binary @result.json https://flowhush.com/p/YOUR-MONITOR-UUID)
case "$resp" in "OK (recorded as failure"*) echo "output check failed: $resp" >&2 ;; esac

Step 3: test each rule

U=https://flowhush.com/p/YOUR-MONITOR-UUID
curl -sS -X POST --data-binary '' $U                     # OK (recorded as failure: Output is empty)
curl -sS -X POST --data-binary '<html>' $U               # OK (recorded as failure: Output is not valid JSON)
curl -sS -X POST -d '{"data":{"items":[]}}' $U           # OK (recorded as failure: Missing or empty JSON key: data.items)
curl -sS -X POST -d '{"data":{"items":[{"id":1}]},"meta":{"generated_at":"2026-11-03"}}' $U   # OK

The first failing ping sends the down alert, and the last one sends the recovery. Flowhush alerts once per incident, so repeated failures don't flood your channel.

Choosing what to require

Require the fields a downstream person or system depends on, not every field. Good candidates: the record ID you write to, the list you iterate over, a timestamp that proves the data is fresh, and a total or summary. If a list can legitimately be empty on some runs, such as "no new orders overnight", don't require it. Send a count instead and check the business rule in the workflow.

Flowhush's Free plan includes 5 heartbeat monitors. Output checks (not empty, valid JSON, required keys) are on Pro at $12/month, or $120/year.

Monitor your first workflows free

The Free plan includes 5 heartbeat monitors with alerts to email, Slack, Discord or Telegram. No credit card required.

Start free — 5 monitors   See setup snippets

Sources

Third-party features and prices were checked on 3 November 2026 and may have changed since.