---
title: stories validate
description: Checks the content of every story in a Storyblok space against a local TypeScript schema.
url: https://www.storyblok.com/docs/tooling/cli/stories-validate
---

# stories validate

Checks the content of every story in a Storyblok space against a local TypeScript schema. The command is read-only: it modifies nothing in the space.

Use it to find content that no longer fits the schema: a block removed from the code, a field renamed, a required field never filled in, or an option value that no longer exists.

Option values are only checked for fields whose options are defined in the schema itself (self-sourced). A field with a `source` (an `internal` datasource, `internal_stories`, `internal_languages`, or `external`) resolves its options inside the space, and a datasource definition carries no entries, because entries are content rather than schema. Those values are therefore not knowable from local code.

> [!WARNING]
> Validation runs against **draft** content, which is what the Management API returns for a story. A story that is published with valid content but has an invalid unpublished draft fails validation. There is no published-only mode: the Management API does not serve the published version of a single story.

## Prerequisites

-   An entry file that exports at least one block, authored with [`@storyblok/schema`](/docs/libraries/js/schema). Story content cannot be validated against a schema that defines no blocks, so such an entry file is rejected with exit code `2` instead of reporting `unknown_component` on every story.

## Usage

```bash
storyblok stories validate [flags]
```

## Flags

| Flag | Type | Description |
| --- | --- | --- |
| `--space`, `-s` | integer | _Required._ The ID of the Storyblok space whose stories are validated. |
| `--schema` | string | _Required._ Path to the TypeScript schema entry file. |
| `--starts-with` | string | _Optional._ Only validate stories whose path starts with this prefix, for example `en/blog/`. |
| `--level` | string | _Optional._ Display threshold: `error` hides warnings, `warning` shows everything. Defaults to `warning`. |
| `--format` | string | _Optional._ Output format: `pretty` or `json`. Defaults to `pretty`. |

`--level` only filters which issues are displayed. It never changes the exit code or the reported totals.

Folders are skipped, since they carry no content, and are excluded from the story total. When `--starts-with` matches no stories, the command says so rather than reporting a clean run over nothing.

## Examples

The following examples assume that a `space` has been defined in a configuration file.

```bash
# Validate every story against the local schema
storyblok stories validate --schema src/schema/schema.ts
# Validate only the stories below a path prefix
storyblok stories validate --schema src/schema/schema.ts --starts-with "en/blog/"
# Fail only on errors and hide warnings
storyblok stories validate --schema src/schema/schema.ts --level error
# Emit a machine-readable report
storyblok stories validate --schema src/schema/schema.ts --format json > stories-report.json
```

Output:

```plaintext
home (story #12345)
  ✖ missing_required_field   content.headline: Missing required field "headline" on component "page".
  ⚠ unknown_field            content.legacy_cta: Unknown field "legacy_cta" on component "page".
✖ 1 error, 1 warning across 1 of 17 stories
```

Groups are sorted by story path, so the output of two runs over identical content is diffable.

## JSON output

With `--format json`, the command writes a single JSON object to stdout and nothing else, so it can be piped or redirected. All human-facing output (the title, progress, and errors) goes to stderr.

```json
{
  "ok": false,
  "unit": "stories",
  "unitsTotal": 17,
  "unitsWithIssues": 1,
  "errors": 1,
  "warnings": 1,
  "fetchFailures": 0,
  "listFailed": false,
  "groups": [
    {
      "header": "home (story #12345)",
      "ref": { "kind": "story", "id": 12345, "slug": "home", "name": "Home" },
      "issues": [
        {
          "severity": "error",
          "code": "missing_required_field",
          "path": ["content", "headline"],
          "entity": "block:page",
          "message": "Missing required field \"headline\" on component \"page\"."
        }
      ]
    }
  ]
}
```

`ok` is `true` only for a **complete** run with no errors. Warnings alone keep it `true`. But a failed listing (`listFailed`) or any story that could not be fetched (`fetchFailures`) makes it `false`, even with zero issues, so a CI consumer never reads success over an incomplete run. In those cases, `listError` and `fetchErrors` carry the reasons, which a consumer reading only stdout would otherwise never see.

Every group carries a `ref` with the machine-readable identity of the story the issues belong to. Use it instead of parsing `header`. The counts are always true totals, unaffected by `--level`.

## Pagination

-   [Previous: stories push](/docs/tooling/cli/stories-push)
-   [Next: types generate](/docs/tooling/cli/types-generate)
