Schema Validator Node

The Schema Validator checks the event payload against a set of field definitions you build on screen. A payload that fails stops the pipeline right there — nothing else in the flow is dispatched — and on a webhook in Sync response mode it is also what answers the caller.

Overview

Schema Validators act as structural gatekeepers in your pipeline. They inspect the incoming event payload against a set of field definitions, checking that required fields are present, values match expected types, and constraints (min, max, pattern, etc.) are satisfied. When a payload fails validation the pipeline stops there: neither the validator's own outputs nor any other node in the flow is dispatched. There is no “invalid” branch — put a Conditional in front of the validator if you need one.

⚠The validator gates the pipeline, not the webhook's target URL. The primary delivery to targetUrl is queued when the event arrives, before any validator runs, and a rejection does not cancel it. To put a schema in front of an outbound call, leave targetUrl empty and deliver with an HTTP Action placed after the validator.

In strictMode, any fields present in the payload that are NOT defined in the schema will also cause validation to fail. This prevents unexpected data from passing through. On the canvas, Schema Validator nodes appear in cyan (#06b6d4) with an ID prefix of sv-.


Configuration

FieldTypeDefaultDescription
namestring—Friendly label for the schema validator
modevalidate | replyvalidateWhat the node does when the run reaches it. validate checks the payload against fields; reply checks nothing and answers the caller with reply instead (see Reply only mode below)
reply{ status?, body? } | nullnullThe answer a Reply-only validator gives: an HTTP status from 200 to 599 and a JSON body whose text values are {{payload.…}} templates. Ignored while mode is validate
strictModebooleanfalseWhen true, rejects payloads with fields not defined in the schema
fieldsSchemaField[][]Array of field definitions (see below)
inputNodes{ nodeType, nodeId }[][]Upstream nodes that feed this one. nodeType is the node kind (webhook, scheduledWorkflow, filter, …)
isActivebooleantrueWhether the validator is enabled

SchemaField object

FieldTypeDescription
pathstringDot-notation path into the payload (e.g. data.object.amount)
typestringExpected type: string, number, boolean, object, or array
requiredbooleanWhether the field must be present in the payload
constraintsobjectOptional constraints object (see constraints table)

Constraints

ConstraintApplies toDescription
minnumberMinimum numeric value
maxnumberMaximum numeric value
minLengthstring, arrayMinimum length / element count
maxLengthstring, arrayMaximum length / element count
patternstringRegular expression the value must match
enumstring, numberArray of allowed values
ℹThe visual field builder in the dashboard lets you define schema fields without writing JSON. Each field row has inputs for path, type, required toggle, and expandable constraint fields.

Create a Schema Validator

bashCreate Schema Validator
curl -X POST /api/schema-validators \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "workspaceId": "<your-workspace-id>",
    "name": "Payment Event Schema",
    "strictMode": false,
    "fields": [
      {
        "path": "type",
        "type": "string",
        "required": true,
        "constraints": {
          "enum": ["payment_intent.succeeded", "payment_intent.payment_failed", "charge.succeeded"]
        }
      },
      {
        "path": "data.object.amount",
        "type": "number",
        "required": true,
        "constraints": {
          "min": 0,
          "max": 99999999
        }
      },
      {
        "path": "data.object.currency",
        "type": "string",
        "required": true,
        "constraints": {
          "minLength": 3,
          "maxLength": 3,
          "pattern": "^[a-z]{3}$"
        }
      },
      {
        "path": "data.object.customer",
        "type": "string",
        "required": false
      },
      {
        "path": "data.object.metadata",
        "type": "object",
        "required": false
      }
    ],
    "inputNodes": [{ "nodeType": "webhook", "nodeId": "6654a1b2c3d4e5f6a7b8c9d0" }],
    "isActive": true
  }'

Response

json201 Created
{
  "_id": "6655a7b8c9d0e1f2a3b4c5d6",
  "name": "Payment Event Schema",
  "strictMode": false,
  "fields": [
    {
      "path": "type",
      "type": "string",
      "required": true,
      "constraints": {
        "enum": ["payment_intent.succeeded", "payment_intent.payment_failed", "charge.succeeded"]
      }
    },
    {
      "path": "data.object.amount",
      "type": "number",
      "required": true,
      "constraints": { "min": 0, "max": 99999999 }
    },
    {
      "path": "data.object.currency",
      "type": "string",
      "required": true,
      "constraints": { "minLength": 3, "maxLength": 3, "pattern": "^[a-z]{3}$" }
    },
    {
      "path": "data.object.customer",
      "type": "string",
      "required": false
    },
    {
      "path": "data.object.metadata",
      "type": "object",
      "required": false
    }
  ],
  "inputNodes": [{ "nodeType": "webhook", "nodeId": "6654a1b2c3d4e5f6a7b8c9d0" }],
  "isActive": true,
  "ownerId": "org_abc123",
  "createdAt": "2025-05-01T12:00:00.000Z",
  "updatedAt": "2025-05-01T12:00:00.000Z"
}

Update a Schema Validator

bashUpdate Schema Validator
curl -X PATCH /api/schema-validators/6655a7b8c9d0e1f2a3b4c5d6 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "strictMode": true,
    "fields": [
      {
        "path": "type",
        "type": "string",
        "required": true,
        "constraints": {
          "enum": ["payment_intent.succeeded"]
        }
      },
      {
        "path": "data.object.amount",
        "type": "number",
        "required": true,
        "constraints": { "min": 1 }
      },
      {
        "path": "data.object.currency",
        "type": "string",
        "required": true
      }
    ]
  }'
⚠Enabling strictMode can be disruptive — any fields in the payload not explicitly defined in the schema will cause validation to fail. Test thoroughly before enabling on production webhooks.

Canvas Integration

Schema Validator nodes use the ID prefix sv- on the canvas and are rendered in cyan (#06b6d4). They connect to webhooks and run early in the onEventCreated phase — before the rest of the pipeline, and after the primary delivery to the webhook's target URL has already been queued.

jsonCanvas node data
{
  "id": "sv-6655a7b8c9d0e1f2a3b4c5d6",
  "type": "schema-validator",
  "position": { "x": 250, "y": 200 },
  "data": {
    "label": "Payment Event Schema",
    "schemaValidatorId": "6655a7b8c9d0e1f2a3b4c5d6",
    "strictMode": false,
    "fieldCount": 5,
    "isActive": true
  }
}

Payload Examples

Valid payload — passes validation

jsonValid payload
{
  "type": "payment_intent.succeeded",
  "data": {
    "object": {
      "amount": 5000,
      "currency": "usd",
      "customer": "cus_abc123"
    }
  }
}
// Result: PASSES — all required fields present, types match, constraints satisfied

Invalid payload — blocked

jsonInvalid payload
{
  "type": "invoice.finalized",
  "data": {
    "object": {
      "amount": -500,
      "currency": "US"
    }
  }
}
// Validation errors, in the wording the validator actually produces:
// - "type": must be one of: payment_intent.succeeded, charge.refunded
// - "data.object.amount": must be >= 0
// - "data.object.currency": must be at least 3 characters

What a rejection leaves behind

A rejection is the node doing its job, not a breakdown, so it is recorded as a warning rather than an error — an event stopped in its tracks is not just another line in the noise either. The log entry carries the errors in its metadata, and the node's lastInvalidPayload keeps the body that failed, next to the lastValidPayload of the last one that passed.

jsonTelemetry entry for a rejection (trimmed)
{
  "level": "warn",
  "source": "schema_validator",
  "message": "Schema \"Payment Event Schema\" INVALID — 2 error(s)",
  "statusCode": 422,
  "nodeName": "Payment Event Schema",
  "success": false,
  "metadata": {
    "valid": false,
    "strictMode": false,
    "fieldCount": 5,
    "errors": [
      { "path": "data.object.amount", "message": "must be >= 0" },
      { "path": "data.object.currency", "message": "must be at least 3 characters" }
    ]
  }
}

Answering a Sync webhook

The Schema Validator answers a webhook in Sync response mode — the only other node that can is the AI Node, and Sync cannot be switched on for a flow that carries neither. It answers at its own step, before dispatching anything downstream, so the caller's connection is held for one validation rather than for the length of the workflow.

The two branches
payload fails  →  this validator's error response (422 by default)
payload passes →  held until the last validator on the path has passed,
                  then 200  { "accepted": true, "eventId": "…" }
                  and the rest of the flow keeps running behind it
⚠That 200 says the payload was accepted, not that the workflow finished. Everything after the validator is still running when the caller reads it. How much of a failure the caller is told, the status codes, the message wording and whether the payload is echoed are configured on each validator, in its Response tab — with two validators in a row, whichever one rejects answers with its own. The full contract, with copyable examples, is on Sync responses.

In None response mode nothing changes: the validator blocks the pipeline exactly as described above and nobody is waiting on an answer.


Reply only mode

A Schema Validator has two modes. Validate — mode: "validate", the default — is everything described above. Reply only — mode: "reply" — validates nothing: when the run reaches the node, whoever is holding the connection open (the caller of a webhook in Sync response mode) gets the response you configured on it, and the connection closes. The payload goes through untouched and everything downstream is dispatched as usual.

It is meant for the end of an error branch. A Conditional or a Filter decides the event is one you will not process — an order that was already handled, a customer you do not know, an API version you no longer accept — and the branch it routes to ends in a Reply-only validator that tells the caller so, with a status and a body of your own. A validating Schema Validator can only say “the payload does not match the schema”; a Reply-only one says whatever you decide, on whichever branch you put it.

Where it goes
Webhook, Sync mode
  └── Schema Validator (Validate)
        │   fails  → 422, pipeline stops
        │   passes → noted, connection still open
        └── Conditional   payload.status == "processed"
              ├── true → Schema Validator (Reply only)
              │            409 { "error": "Order … was already processed" } — final,
              │            and whatever hangs below it still runs
              └── else → HTTP Action → …
                           nothing else answers: the Validate step's 200, as usual

What you configure

Two fields, sent on POST /api/schema-validators and PATCH /api/schema-validators/:id, and edited in the validator's panel once the mode is switched. Both parts of reply are optional.

FieldRangeDefaultWhat it does
modevalidate, replyvalidateWhich of the two the node is. Switching to reply does not touch fields, strictMode or syncResponse — they are kept, and ignored until you switch back.
reply.status200–599200The HTTP status the caller gets. Any 2xx, 4xx or 5xx — this is your answer, not a verdict on the payload, so the 2xx-only and 4xx-only ranges of Validate mode do not apply. Out of range is refused with a 400 at save time; a value that reaches the response builder anyway comes out as 200, not clamped.
reply.bodyJSON object{ "accepted": true, "eventId": "…" }The body, sent as JSON. Every string value in it is a template: {{payload.orderId}} is replaced with the event's value, pipes and all, exactly as in the other nodes — see Expressions & pipes. Strings at any depth are rendered; numbers and booleans are left alone.

In the panel you do not type that JSON: the body box takes lines. One key: value per line; indent under a bare key: to nest — key: value lines make an object, - value lines make a list, and - key: value starts an object inside a list. Numbers and true/false are typed; wrap a value in quotes to keep it as text. Pasting JSON works too. The same box is used for the custom input under Run Test → Advanced.

yamlBody written as lines
accepted: true
order:
  id: {{payload.orderId}}
  tags:
    - checked
    - {{payload.source}}
items:
  - sku: A1
    qty: 2
json…and what is sent
{
  "accepted": true,
  "order": { "id": "ord_1042", "tags": ["checked", "stripe"] },
  "items": [{ "sku": "A1", "qty": 2 }]
}

What the caller receives is the status, the rendered body with content-type: application/json; charset=utf-8, and the X-HostWebhook-Event-Id header that every sync response carries — you do not need to put the event id in the body yourself.

Final, like a rejection — but nothing stops

A pass from a validating Schema Validator is provisional: the connection stays open in case a later validator on the path says no. A Reply-only validator is that later word. Its reply goes out the moment the run reaches it and the provisional pass is discarded — the caller never sees the 200 the earlier validator would have given. Unlike a rejection, though, it does not stop the pipeline: the payload is handed on and the nodes after it run behind the answer, just as they do behind a pass.

⚠In None response mode a Reply-only validator does nothing at all. There is no connection to answer, so it neither answers nor closes anything — the payload passes through and the run carries on. In particular it does not end the event's live stream in the dashboard, which is still following a run that has not finished.

Example

Switch an existing validator to Reply only and give it a 409 with a message built from the payload:

bashSwitch a validator to Reply only
curl -X PATCH /api/schema-validators/6655a7b8c9d0e1f2a3b4c5d6 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "reply",
    "reply": {
      "status": 409,
      "body": { "error": "Order {{payload.orderId}} was already processed" }
    }
  }'

A caller whose event, { "orderId": "ord_8841", … }, is routed to that validator gets:

httpWhat the caller receives
HTTP/1.1 409 Conflict
content-type: application/json; charset=utf-8
X-HostWebhook-Event-Id: 6f3a91c2b45d8e0197ac2d64

{
  "error": "Order ord_8841 was already processed"
}

Leave reply out, or send it as null, and the same validator answers 200 with { "accepted": true, "eventId": "…" } — the plain acknowledgement, given from wherever on the canvas you put the node.


Use Cases

  • Contract enforcement — Ensure incoming webhooks always match a defined schema before any processing occurs.
  • Data quality gates — Block events with missing or malformed fields from polluting downstream databases.
  • API versioning — Validate that payloads match a specific API version's expected structure using enum constraints on version fields.
  • Strict mode for security — Enable strictMode to reject payloads with unexpected fields, preventing injection of unwanted data.
  • Custom error answers — End an error branch with a validator in Reply only mode to answer a Sync caller with a status and body of your own — a 409 for an order already processed, a 410 for a retired API version.
  • Debugging — Attach a schema validator to catch malformed events early and see detailed validation error reports in the dashboard.

API Reference

MethodWebhookDescription
GET/api/schema-validatorsList all schema validators
POST/api/schema-validatorsCreate a new schema validator
GET/api/schema-validators/:idGet a specific schema validator
PATCH/api/schema-validators/:idUpdate a schema validator
DELETE/api/schema-validators/:idDelete a schema validator
✦Use the visual field builder in the dashboard to define your schema interactively. You can add fields, set types, toggle required, and configure constraints without writing raw JSON.