> ## Documentation Index
> Fetch the complete documentation index at: https://docs.modulex.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Guardrails node: validation, PII & routing

> Validate run data with JSON Schema, regex, and PII detection, then mask, block, warn, or route on the result. Full GuardrailsNodeConfig reference, the result schema written to state, pass/fail routing model, errors, credit impact, and a worked example.

export const MediaEmbed = ({id, type = 'screenshot', caption = '', ext, ratio = '16 / 9'}) => {
  const isVideo = type === 'video' || type === 'app_video';
  const resolvedExt = ext || (isVideo ? 'mp4' : type === 'screenshot' ? 'webp' : 'svg');
  const src = 'https://media.modulex.dev/' + id + '.' + resolvedExt;
  const [status, setStatus] = useState('loading');
  const [isDev, setIsDev] = useState(false);
  const [inView, setInView] = useState(false);
  const boxRef = useRef(null);
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const h = window.location.hostname;
    setIsDev(h === 'localhost' || h === '127.0.0.1' || h.endsWith('.mintlify.app'));
  }, []);
  useEffect(() => {
    if (inView) return;
    if (typeof IntersectionObserver === 'undefined') {
      setInView(true);
      return;
    }
    const el = boxRef.current;
    if (!el) return;
    const io = new IntersectionObserver(entries => {
      if (entries.some(e => e.isIntersecting)) {
        setInView(true);
        io.disconnect();
      }
    }, {
      rootMargin: '300px'
    });
    io.observe(el);
    return () => io.disconnect();
  }, [inView]);
  if (status === 'missing') {
    if (!isDev) return null;
    return <div style={{
      display: 'flex',
      flexDirection: 'column',
      justifyContent: 'center',
      gap: '0.4rem',
      padding: '1rem 1.25rem',
      margin: '1.25rem 0',
      width: '100%',
      aspectRatio: ratio,
      boxSizing: 'border-box',
      border: '1px dashed rgba(128,128,128,0.45)',
      borderRadius: '0.75rem',
      background: 'rgba(128,128,128,0.06)',
      color: 'currentColor',
      fontSize: '0.85rem',
      lineHeight: 1.45
    }}>
        <div style={{
      display: 'flex',
      alignItems: 'center',
      gap: '0.5rem',
      opacity: 0.75
    }}>
          <span aria-hidden="true">🎬</span>
          <code style={{
      fontSize: '0.75rem'
    }}>{id}</code>
          <span style={{
      fontSize: '0.65rem',
      textTransform: 'uppercase',
      letterSpacing: '0.04em',
      padding: '0.1rem 0.4rem',
      borderRadius: '0.4rem',
      background: 'rgba(128,128,128,0.18)'
    }}>
            {type}
          </span>
        </div>
        <div style={{
      opacity: 0.9
    }}>{caption || 'Media not uploaded yet.'}</div>
        <div style={{
      fontSize: '0.7rem',
      opacity: 0.5
    }}>
          Upload to R2 as <code>{id}.{resolvedExt}</code> — preview only, hidden in production.
        </div>
      </div>;
  }
  const mediaStyle = {
    display: status === 'loaded' ? 'block' : 'none',
    width: '100%',
    height: 'auto',
    borderRadius: '0.75rem'
  };
  const media = isVideo ? <video src={inView ? src : undefined} autoPlay loop muted playsInline preload="metadata" onLoadedData={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} /> : <img src={inView ? src : undefined} alt={caption} onLoad={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} />;
  return <figure style={{
    margin: '1.25rem 0'
  }}>
      <div ref={boxRef} style={status === 'loaded' ? {
    width: '100%'
  } : {
    width: '100%',
    aspectRatio: ratio,
    borderRadius: '0.75rem',
    background: 'rgba(128,128,128,0.06)'
  }}>
        {media}
      </div>
      {status === 'loaded' && caption ? <figcaption style={{
    marginTop: '0.5rem',
    textAlign: 'center',
    fontSize: '0.85rem',
    opacity: 0.7
  }}>
          {caption}
        </figcaption> : null}
    </figure>;
};

The `guardrails` node validates a value already in run [state](/concepts/workflow-engine) and records the outcome. It runs up to four checks over a single source value — JSON Schema validation, regex validation, PII detection, and a hallucination check — then takes one of four actions: block, warn, transform (mask PII), or route. Unlike most node types, it produces no new business data of its own; its job is to inspect existing data and write a structured verdict that a later node can act on.

A guardrails node does **not** call a language model or any managed service, so it consumes no [credits](/billing/credits) and never hits the billing gate. It is a pure, in-process validation step.

For the engine that compiles and runs nodes (the state graph, run state, and edges), see [Workflow engine & nodes](/concepts/workflow-engine). For the `{{node_id.field}}` reference syntax used throughout this page, see [Variables & references](/workflow-builder/variables-and-references).

<MediaEmbed id="MX-MEDIA-3150" type="screenshot" caption={"the guardrails node configuration panel in the workflow builder"} />

## What the node does

When a run reaches a guardrails node, the engine:

1. Resolves the single `source` reference (for example `{{node_extract.json}}`) against run state to get the value to inspect.
2. Runs each **enabled** check in turn — JSON validation, regex validation, PII detection, hallucination check — recording a per-check result.
3. Decides whether the node passed. By default, the node passes only if **every** enabled check passed. With `continue_on_partial_failure` set, the node passes if **at least one** enabled check passed.
4. Applies the configured `on_failure` action and writes a single result object into state under the node's own `id`.

The node writes its verdict to state but does **not** itself stop the run or change the graph path. Routing is a separate step you wire downstream — see [Pass/fail routing](#passfail-routing). This split is the most important thing to understand about this node, so it is called out again below.

The guardrails node is **retry-wrapped** like most node types: if the node function raises, it retries per the node's `retry_config` (default 3 attempts) and emits the standard `node_started`, `node_retry`, and `node_error` events on the run stream — see [SSE run streaming](/realtime/sse-streaming). A *failed* validation is not an exception, though: a check that returns `valid: false` is a normal result, not a retryable error, so a blocked or routed verdict never triggers a retry.

<Warning>
  The `on_failure: block` action does **not** halt the run. It records `blocked: true` and an `error` string in the node's result; the graph continues to the next edge as written. To actually stop or divert a run on a failed guardrail, read the node's result with a downstream [conditional node](/workflow-builder/nodes/conditional). See [Pass/fail routing](#passfail-routing).
</Warning>

## GuardrailsNodeConfig reference

Every guardrails node carries a `guardrails_config` object. A node with no `guardrails_config` raises a build-time `ValueError`. Each of the four checks is optional and independently toggled with its own `enabled` flag — a check object that is present but `enabled: false` is skipped, and so is a check that is absent.

<ParamField path="source" type="string" required>
  The value to validate, written as a single `{{node_id.field}}` reference — for example `{{node_extract.json}}` or `{{input.payload}}`. Resolved against run state to its native typed value before any check runs. If it resolves to `null` (missing reference), the node returns `valid: false` with an error and no checks run. Non-string values are coerced to a string for the text-based checks (regex), while JSON validation and PII detection operate on the native value, including nested objects and arrays.
</ParamField>

<ParamField path="json_validation" type="JsonValidationConfig">
  JSON Schema (Draft 7) validation of the resolved source value. Optional. See [JSON Schema validation](#json-schema-validation).
</ParamField>

<ParamField path="regex_validation" type="RegexValidationConfig">
  Regular-expression validation of the source value as text. Optional. See [Regex validation](#regex-validation).
</ParamField>

<ParamField path="pii_detection" type="PIIDetectionConfig">
  Detection of personally identifiable information, with block, mask, or flag handling. Optional. See [PII detection](#pii-detection).
</ParamField>

<ParamField path="hallucination_check" type="HallucinationCheckConfig">
  A grounding/hallucination check. Optional. **Currently a placeholder** — see [Hallucination check](#hallucination-check-placeholder).
</ParamField>

<ParamField path="on_failure" type="string" default="block">
  The action when the node does not pass. One of `block`, `warn`, `transform`, `route`. Defaults to `block`. This action only annotates the node's result object — it does not change the graph path on its own. See [The `on_failure` action](#the-on_failure-action).
</ParamField>

<ParamField path="failure_route" type="string">
  The target node id to record when `on_failure` is `route`. Written into the result as `route_to`; a downstream conditional reads it to actually branch. Has no effect for other `on_failure` values.
</ParamField>

<ParamField path="continue_on_partial_failure" type="boolean" default="false">
  When `false` (default), the node passes only if **all** enabled checks pass. When `true`, the node passes if **at least one** enabled check passes. Use it when several checks are advisory and you only want to fail when everything fails.
</ParamField>

### JSON Schema validation

`JsonValidationConfig` validates the resolved source value against a JSON Schema using a Draft 7 validator.

<ParamField path="enabled" type="boolean" default="false">
  Turn the check on. When `false`, JSON validation is skipped entirely.
</ParamField>

<ParamField path="schema" type="object">
  The JSON Schema to validate against. Also accepted under the field name `schema_def`. When no schema is provided the check passes with a "no schema provided" message rather than failing.
</ParamField>

<ParamField path="strict_mode" type="boolean" default="false">
  When `true`, the validator adds `additionalProperties: false` to the top-level schema before validating, so any property not declared in the schema is a failure.
</ParamField>

On a schema violation the check result is `valid: false` with an `errors` array (capped at the first 5), each entry carrying `message`, `path`, and `schema_path`. An invalid schema or other internal error returns `valid: false` with a single `error` string.

### Regex validation

`RegexValidationConfig` matches the source value, rendered as text, against a regular expression.

<ParamField path="enabled" type="boolean" default="false">
  Turn the check on.
</ParamField>

<ParamField path="pattern" type="string">
  The regular expression to apply. If empty, the check passes with a "no pattern provided" message.
</ParamField>

<ParamField path="flags" type="string">
  Optional regex flags as a string of letters: `i` (ignore case), `m` (multiline), `s` (dotall). Any combination, for example `im`.
</ParamField>

<ParamField path="match_mode" type="string" default="contains">
  How the pattern is applied: `full_match` (the pattern must match the entire string), `contains` (the pattern is found anywhere), or `search` (first match anywhere). `contains` and `search` behave identically. Defaults to `contains`.
</ParamField>

<ParamField path="negate" type="boolean" default="false">
  Invert the result. With `negate: false` (default), the check passes when the pattern is found and fails when it is not. With `negate: true`, the check passes when the pattern is **not** found and fails when it is — the standard way to enforce a "must NOT contain" content-safety rule.
</ParamField>

<ParamField path="error_message" type="string">
  A custom message to use on failure instead of the generated default.
</ParamField>

On a passing match the result includes the matched substring and its `start`/`end` offsets. On failure it carries `valid: false` and the `error` (your `error_message` if set). An invalid pattern returns `valid: false` with an "Invalid regex pattern" error.

### PII detection

`PIIDetectionConfig` scans the source value — including nested objects and arrays — for personally identifiable information and handles any findings.

<ParamField path="enabled" type="boolean" default="false">
  Turn the check on.
</ParamField>

<ParamField path="pii_types" type="string[]" default="['email', 'phone', 'credit_card']">
  The PII categories to scan for. Defaults to `email`, `phone`, and `credit_card`. Supported values: `email`, `phone`, `credit_card`, `ssn`, `ip_address`, `mac_address`, `iban`, `url`, `date_of_birth`, `passport`, `drivers_license`, `tax_id`, and `custom`. Each maps to a built-in regular expression; `custom` is matched against `custom_patterns` instead.
</ParamField>

<ParamField path="action" type="string" default="mask">
  What to do when PII is found: `block` (the check fails, so the node does not pass), `mask` (replace matches in `transformed_data`, the check still passes), or `flag` (record findings as a warning and continue, the check still passes). Defaults to `mask`.
</ParamField>

<ParamField path="custom_patterns" type="object">
  A map of `pattern_name` to regex string, applied only when `custom` is in `pii_types`. Findings are typed as `custom:<pattern_name>`. An invalid custom pattern is logged and skipped rather than failing the check.
</ParamField>

<ParamField path="language" type="string" default="en">
  Locale for locale-specific patterns. Defaults to `en`. Setting `tr` swaps in Turkish phone and TCKN (national-ID / tax-id) patterns. Other values fall back to the default patterns.
</ParamField>

<ParamField path="mask_char" type="string" default="*">
  The character used when masking. Defaults to `*`.
</ParamField>

<ParamField path="mask_preserve_length" type="boolean" default="true">
  When `true` (default), a masked value is replaced character-for-character so its length is unchanged. When `false` and the value is longer than 4 characters, the first and last characters are kept and the middle is masked (for example `j*****e`); shorter values are fully masked.
</ParamField>

Each finding records its `type`, the matched `value`, the `start`/`end` offsets, and — for matches inside nested data — a `path` (for example `customer.email` or `items[0]`). Masking with `action: mask` writes the cleaned value to `transformed_data` and leaves the check passing; the masked output is what you reference downstream.

<Warning>
  The built-in PII patterns are pragmatic regular expressions, not a certified PII engine. They can miss values (false negatives) and match non-PII (false positives) — for example the generic `tax_id` pattern matches any 10–11 digit run. Treat PII masking as defense-in-depth, not a guarantee, and validate against your own data before relying on it for compliance.
</Warning>

### Hallucination check (placeholder)

`HallucinationCheckConfig` is **not yet implemented**. When `enabled`, the node records a result of `valid: true`, `checked: false`, and the message `Hallucination check requires knowledge base configuration (coming soon)` — it never fails the node and never inspects content. The config fields below are accepted and stored for forward compatibility but have no runtime effect today.

<ParamField path="enabled" type="boolean" default="false">
  Turn the check on. Because the check is a placeholder, enabling it adds the stub result above but does not validate anything.
</ParamField>

<ParamField path="knowledge_base_id" type="string">
  Intended knowledge base for grounding (future). If unset while the check is enabled, a warning is logged.
</ParamField>

<ParamField path="model" type="string">
  Intended detection model (future). No effect today.
</ParamField>

<ParamField path="confidence_threshold" type="number" default="0.7">
  Intended confidence floor, `0.0`–`1.0` (future). No effect today.
</ParamField>

<ParamField path="method" type="string" default="self_check">
  Intended detection method: `self_check`, `knowledge_base`, or `hybrid` (future). No effect today.
</ParamField>

<Note>
  Do not rely on the hallucination check to catch anything yet. For grounding answers against your documents today, retrieve with a [Knowledge node](/workflow-builder/nodes/knowledge) and validate the result with the JSON or regex checks here.
</Note>

### The `on_failure` action

When the node does not pass, `on_failure` decides what extra fields are written to the result object. None of these actions changes the graph path by themselves — they annotate the verdict so a later node can act on it.

| `on_failure`      | Extra fields written when the node fails                 | Meaning                                                                                                                                    |
| ----------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `block` (default) | `blocked: true`, `error: "Guardrails validation failed"` | Marks the data as rejected. The run still proceeds to the next edge — gate it with a downstream conditional.                               |
| `warn`            | `warning: "Guardrails validation failed but continuing"` | Records a warning and logs it; the run continues.                                                                                          |
| `route`           | `route_to: <failure_route>`                              | Records the intended target node. A downstream conditional reads `route_to` to branch.                                                     |
| `transform`       | `transformed: true`                                      | Signals that downstream steps should use `transformed_data`. Most useful alongside PII `mask`, which already populates `transformed_data`. |

## Inputs and outputs

**Input.** A guardrails node reads exactly one value from run state — the resolved `source` reference. It does not take an `input_mapping`.

**Output.** Like every node, it writes its result to state under its own `id`, so a later step references it as `{{<guardrails_node_id>.field>}}`. The result object has this shape:

<ResponseField name="valid" type="boolean">
  The overall verdict. `true` if the node passed (all enabled checks passed, or — with `continue_on_partial_failure` — at least one did); otherwise `false`.
</ResponseField>

<ResponseField name="source" type="string">
  The `source` reference that was validated, echoed back.
</ResponseField>

<ResponseField name="validations" type="object">
  Per-check results keyed by `json_validation`, `regex_validation`, `pii_detection`, and `hallucination_check`. A check that did not run is `null`; a check that ran holds its own result object (each with its own `valid` plus check-specific fields described above).
</ResponseField>

<ResponseField name="original_data" type="any">
  The source value exactly as resolved, before any masking.
</ResponseField>

<ResponseField name="transformed_data" type="any">
  The value after PII masking, when masking changed it; otherwise `null`. Reference this (not `original_data`) downstream when you want the cleaned value.
</ResponseField>

<ResponseField name="pii_findings" type="array">
  The list of PII findings (each with `type`, `value`, `start`, `end`, and optional `path`), or `null` when none were found.
</ResponseField>

<ResponseField name="blocked" type="boolean">
  Present and `true` only when the node failed and `on_failure` is `block`. Accompanied by `error`.
</ResponseField>

<ResponseField name="warning" type="string">
  Present only when the node failed and `on_failure` is `warn`.
</ResponseField>

<ResponseField name="route_to" type="string">
  Present only when the node failed and `on_failure` is `route` — equals `failure_route`.
</ResponseField>

<ResponseField name="transformed" type="boolean">
  Present and `true` only when the node failed and `on_failure` is `transform`.
</ResponseField>

<ResponseField name="error" type="string">
  Present when the node was blocked, or when the `source` reference could not be resolved (`Source data not found: <source>`).
</ResponseField>

## Pass/fail routing

The guardrails node decides pass or fail, but it never creates a branch in the graph on its own. To act on the verdict you read the node's result with a downstream [conditional node](/workflow-builder/nodes/conditional), exactly as you would read any other node's output. This is the one workflow that turns a guardrail into a real gate.

A common pattern:

1. The guardrails node `node_check` validates `{{node_extract.json}}` with `on_failure: route` and `failure_route: "node_reject"`.
2. A conditional node after it branches on `{{node_check.valid}}` — route `false` to your rejection path and `true` to your happy path.

Because `route_to` and `blocked` are plain data fields, you can branch on whichever you prefer:

```json Conditional that gates on the guardrail theme={null}
{
  "id": "node_gate",
  "type": "conditional",
  "x": 480, "y": 0,
  "conditional_config": {
    "condition_type": "expression",
    "expression_branches": [
      { "id": "b_fail", "source": "{{node_check.valid}}", "operator": "equals", "value": "false", "target": "node_reject" }
    ],
    "default_target": "node_continue"
  }
}
```

<Note>
  Setting `on_failure: route` and `failure_route` alone does **not** divert the run — the engine has no built-in edge that reads a guardrails node's `route_to`. Only a conditional node converts the verdict into a branch. Treat `failure_route` as a hint you then honor with a conditional that routes to the same target.
</Note>

## Credit impact

The guardrails node performs only local validation — no LLM call, no managed retrieval, no external request — so it **consumes no credits** and is never subject to the [usage gate](/billing/usage-gating). It is safe to use liberally for input checking and output validation without affecting your [credit](/billing/credits) balance. (The hallucination check would change this once implemented, since grounding would call a model; today it is a no-op.)

## Errors

| Situation                                          | When       | What you see                                                                                              |
| -------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------- |
| `guardrails_config` missing                        | Build time | `ValueError: Guardrails node <id> missing guardrails_config` — the run fails to start                     |
| `source` resolves to `null`                        | Run time   | Result `{ "valid": false, "error": "Source data not found: <source>", "validations": {} }`; no checks run |
| JSON Schema violation                              | Run time   | `validations.json_validation.valid = false` with an `errors` array (first 5)                              |
| Invalid JSON Schema or internal error              | Run time   | `json_validation.valid = false` with an `error` string                                                    |
| Regex does not match (or matches in `negate` mode) | Run time   | `validations.regex_validation.valid = false` with `error`                                                 |
| Invalid regex pattern                              | Run time   | `regex_validation.valid = false`, `error: "Invalid regex pattern: ..."`                                   |
| PII found with `action: block`                     | Run time   | `pii_detection.valid = false`; node fails; `pii_findings` populated                                       |
| Node function raises unexpectedly                  | Run time   | Retried per `retry_config`; `node_retry` then `node_error` on final failure                               |

A failed validation is data, not an exception — it never raises and never retries. The node only retries on a genuine runtime exception. The validation routes (workflow save/update) return validation problems as the `{detail}` `HTTPException` shape; see [Errors & status codes](/api-reference/errors). The guardrails node itself never emits a `DenialEnvelope`, because it makes no metered call.

## Worked example

Validate an upstream extraction against a schema, mask any leaked PII, and route to a rejection path if the data is invalid. An [LLM node](/workflow-builder/nodes/llm) `node_extract` produces a JSON object under `{{node_extract.json}}`; the guardrails node `node_check` validates it and masks emails and phone numbers; a [conditional node](/workflow-builder/nodes/conditional) `node_gate` branches on the verdict.

<CodeGroup>
  ```json Workflow definition (excerpt) theme={null}
  {
    "metadata": { "name": "Extract & guard", "version": "1.0" },
    "state_schema": {
      "fields": {
        "raw": { "type": "string", "required": true }
      }
    },
    "nodes": [
      {
        "id": "node_extract",
        "type": "llm",
        "x": 0, "y": 0,
        "llm_config": {
          "llm": { "integration_name": "modulexai", "provider_id": "modulexai", "model_id": "claude-haiku-3.5" },
          "system_prompt": "Extract the contact as JSON with keys name, email, phone.",
          "user_prompt": "{{input.raw}}",
          "structured_output_schema": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "email": { "type": "string" },
              "phone": { "type": "string" }
            },
            "required": ["name", "email"]
          }
        }
      },
      {
        "id": "node_check",
        "type": "guardrails",
        "x": 240, "y": 0,
        "guardrails_config": {
          "source": "{{node_extract.json}}",
          "json_validation": {
            "enabled": true,
            "schema": {
              "type": "object",
              "properties": {
                "name": { "type": "string" },
                "email": { "type": "string", "format": "email" }
              },
              "required": ["name", "email"]
            },
            "strict_mode": false
          },
          "pii_detection": {
            "enabled": true,
            "pii_types": ["email", "phone"],
            "action": "mask",
            "mask_char": "*",
            "mask_preserve_length": true
          },
          "on_failure": "route",
          "failure_route": "node_reject",
          "continue_on_partial_failure": false
        }
      },
      {
        "id": "node_gate",
        "type": "conditional",
        "x": 480, "y": 0,
        "conditional_config": {
          "condition_type": "expression",
          "expression_branches": [
            { "id": "b_fail", "source": "{{node_check.valid}}", "operator": "equals", "value": "false", "target": "node_reject" }
          ],
          "default_target": "node_continue"
        }
      }
    ],
    "edges": [
      { "source": "__start__", "target": "node_extract" },
      { "source": "node_extract", "target": "node_check" },
      { "source": "node_check", "target": "node_gate" }
    ]
  }
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.modulex.dev/workflows/run" \
    -H "Authorization: Bearer mx_live_XXXXXXXXXXXXXXXX" \
    -H "X-Organization-ID: org_XXXXXXXXXXXX" \
    -H "Content-Type: application/json" \
    -d '{
      "workflow_id": "wf_XXXXXXXXXXXX",
      "input": { "raw": "Reach Jane Doe at jane.doe@example.com or 415-555-0134." }
    }'
  ```

  ```python Python theme={null}
  from modulex import ModuleX

  client = ModuleX(
      api_key="mx_live_XXXXXXXXXXXXXXXX",
      organization_id="org_XXXXXXXXXXXX",
  )

  run = await client.workflows.run(
      workflow_id="wf_XXXXXXXXXXXX",
      input={"raw": "Reach Jane Doe at jane.doe@example.com or 415-555-0134."},
  )
  print(run)
  ```

  ```javascript JavaScript theme={null}
  import { ModuleX } from "modulex";

  const client = new ModuleX({
    apiKey: "mx_live_XXXXXXXXXXXXXXXX",
    organizationId: "org_XXXXXXXXXXXX",
  });

  const run = await client.workflows.run({
    workflowId: "wf_XXXXXXXXXXXX",
    input: { raw: "Reach Jane Doe at jane.doe@example.com or 415-555-0134." },
  });
  console.log(run);
  ```
</CodeGroup>

For that input the extraction is schema-valid, so JSON validation passes, and PII detection finds the email and phone and masks them. The guardrails node's state entry looks like this:

```json theme={null}
{
  "node_check": {
    "valid": true,
    "source": "{{node_extract.json}}",
    "validations": {
      "json_validation": { "valid": true },
      "regex_validation": null,
      "pii_detection": {
        "valid": true,
        "findings": [
          { "type": "email", "value": "jane.doe@example.com", "start": 18, "end": 38 },
          { "type": "phone", "value": "415-555-0134", "start": 42, "end": 54 }
        ],
        "count": 2
      },
      "hallucination_check": null
    },
    "original_data": { "name": "Jane Doe", "email": "jane.doe@example.com", "phone": "415-555-0134" },
    "transformed_data": { "name": "Jane Doe", "email": "********************", "phone": "************" },
    "pii_findings": [
      { "type": "email", "value": "jane.doe@example.com", "start": 18, "end": 38 },
      { "type": "phone", "value": "415-555-0134", "start": 42, "end": 54 }
    ]
  }
}
```

Because `valid` is `true`, `node_gate` falls through to `node_continue`, which reads the cleaned record from `{{node_check.transformed_data}}`. If the extraction had been missing `email`, JSON validation would fail, `valid` would be `false`, `route_to` would be `node_reject`, and `node_gate` would branch there. To stream and observe the verdict live, see [SSE run streaming](/realtime/sse-streaming); to run a workflow end to end, see [Run a workflow](/guides/run-a-workflow).

## Related

<CardGroup cols={2}>
  <Card title="Conditional node" icon="git-branch" href="/workflow-builder/nodes/conditional">
    Read a guardrail's verdict and branch the run on it — the routing half of pass/fail gating.
  </Card>

  <Card title="Node types reference" icon="grip" href="/workflow-builder/nodes/overview">
    All nine node types and the state convention they share.
  </Card>

  <Card title="Function node" icon="function" href="/workflow-builder/nodes/function">
    The built-in schema-validation function, an alternative when you want validation outside a guardrail.
  </Card>

  <Card title="Variables & references" icon="braces" href="/workflow-builder/variables-and-references">
    The `{{node_id.field}}` reference system the `source` field uses.
  </Card>
</CardGroup>
