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

# Bulk stories schema

> Every column in a downloaded story file, in Parquet and CSV.

Bulk downloads hand you the same coded stories the API serves, as files
instead of pages of JSON.

## Getting the files

Sign in and open [Bulk Data Download](https://gdeltcloud.com/downloads). **Every plan can see
the page**: every file for every period, with its `sha256`, byte size and row count, a
per-period completeness label so you can tell which dates are settled before you load one,
and a summary of what each file contains. **Downloading the files is included on the
Intelligence, Enterprise and Academic plans.**

If you need this automated rather than downloaded by hand, tell us — a programmatic manifest
exists and we will publish it as a supported contract once every period exports cleanly.

## Files

|              |                                                                                               |
| ------------ | --------------------------------------------------------------------------------------------- |
| Formats      | Parquet (`.parquet`) and gzipped CSV (`.csv.gz`) — identical rows, identical column names     |
| Partitioning | One file per calendar month, named `<dataset>_YYYY-MM`                                        |
| Integrity    | Every file publishes a `sha256`, a byte size and a row count. Check the digest before loading |
| Ordering     | Deterministic. Re-exporting an unchanged month reproduces the same bytes and the same digest  |

## Reading the two formats

Parquet carries real types: a null is a null, an array is a `LIST`, and a timestamp is a
`TIMESTAMP`. Prefer it if your tooling can read it.

CSV has neither nulls nor arrays, so two conventions apply and they are the only differences
between the formats:

* **Array columns are JSON arrays**, written as a single quoted field — parse them with your
  JSON reader, not by splitting on a delimiter. A title containing a comma would break the row
  otherwise, and titles contain commas.
* **An empty field is a null.** CSV cannot distinguish "no value" from "the empty string", and
  in this data the distinction matters: see the note on metrics below.

## Coverage

Each file records the settled dates it covers against the calendar days in its period. A month
is only complete when those two agree; the manifest publishes both, so a partial month is
visible rather than silently short.

## Nulls are information, not gaps

A null metric does not mean zero. It means the column does not apply to that row: the conflict
family carries no `magnitude`, `systemic_importance`, `propagation_potential` or
`market_sensitivity`, the CAMEO+ family carries no `fatalities`, and a story with no coded
events has no fatality count at all. Coalescing these to `0` on load will invent measurements
that were never taken, and any average you compute afterwards will be wrong in a direction you
cannot recover.

The same applies to three-valued booleans such as `civilian_targeting`: `true` where a coder
judged civilians were targeted, `false` where a coder judged they were not, and null where no
coder evaluated the question at all.

## Event counts are as of the export, not as of your query

`linked_event_count`, `fatalities`, `civilian_targeting_event_count`,
`max_linked_event_significance`, `linked_event_ids` and the three `has_*` flags are counted
over **all** of a story’s events at the moment the file was cut.

The API does something different, deliberately: it recomputes them under your filters, so
`?category=Protests` returns each story’s PROTEST event count rather than its total. It also
excludes events that have since been merged away. Two consequences worth knowing before you
reconcile a file against a response:

* **A story with no events has `fatalities` as null here, where the API serves `0`.** Null is
  the honest value: there were no events to count deaths from. Do not coalesce it to zero, or
  any average you compute afterwards is wrong in a direction you cannot recover.
* **`linked_event_ids` can name an event the API no longer returns**, if an adjudicator merged
  it into another after this file was cut. Join on `event_uid` against the events file for the
  same period and treat a miss as a merge, not as corruption.

`as_of` on every row tells you what that row knew and when.

## A story is identified by its date AND its id

`story_id` is unique **within** a day, not across days. A story that continues into a later
day carries the same id under that day’s date — measured over March 2026, 544 ids appear on
more than one date, one of them on five. That is the system working: a story is a daily
cluster of articles, and the same cluster continuing is the same id.

So when you concatenate monthly files, **join and de-duplicate on `(story_date, story_id)`**.
De-duplicating on `story_id` alone silently drops days of a continuing story, and the row you
keep is whichever one your tool happened to see first.

## There is no country display name, only the code

`country_iso3` is the country the story is **about**: its own attribution when it has one, and
otherwise the country of its events. That is the same precedence `?country=` and
`group_by=country` use, so a filter you write against this column returns what the API would.

The warehouse also stores an event-derived display name, and it is deliberately **not**
published: for a story whose attribution is not its event location the two name different
countries, and whichever column you reached for first would decide whether your numbers
reconcile with ours. `story_country_iso3s` carries every country the story is about, most
central first — `?country=` matches any of them while `group_by=country` counts the first.

## Stories

One row per story, ordered by `story_date, story_id`. Every column below is a field
[`/api/v2/stories`](/api-reference) returns — the file and the endpoint read the same table, so a
row in the file and the same row from the API agree field for field.

| Column                           | Parquet     | CSV                       | Nullable | Description                                                                                                                                                                                                                                                               |
| -------------------------------- | ----------- | ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `story_id`                       | UTF8        | text                      | no       | Story identifier, and the id /api/v2/stories/\{id} serves. UNIQUE WITH story\_date, not on its own — a story that continues into a later day carries the same id under that day’s date, so join and de-duplicate on the pair.                                             |
| `story_date`                     | DATE        | YYYY-MM-DD                | no       | The day the story is filed under.                                                                                                                                                                                                                                         |
| `title`                          | UTF8        | text                      | yes      | Story title.                                                                                                                                                                                                                                                              |
| `url`                            | UTF8        | text                      | yes      | Public GDELT Cloud page for this story — open it to see the articles and events behind the row.                                                                                                                                                                           |
| `category`                       | UTF8        | text                      | yes      | Friendly label for the story’s own broad category.                                                                                                                                                                                                                        |
| `category_code`                  | UTF8        | text                      | yes      | The story’s own raw category code ("cameoplus\_crime"), and exactly what ?story\_category= accepts.                                                                                                                                                                       |
| `matched_category_codes`         | LIST\<UTF8> | JSON array                | no       | Linked-EVENT taxonomy codes present in this story. This is what ?category= matches on — it can differ from the story’s own category\_code.                                                                                                                                |
| `matched_category_labels`        | LIST\<UTF8> | JSON array                | no       | Human-readable names for matched\_category\_codes, positionally aligned.                                                                                                                                                                                                  |
| `country_iso3`                   | UTF8        | text                      | yes      | ISO 3166-1 alpha-3 for the country this story is about: its own attribution when it has one, otherwise the country of its events. This is what ?country= and group\_by=country resolve to. Join on this.                                                                  |
| `story_country_iso3s`            | LIST\<UTF8> | JSON array                | no       | Every country the story is about, most central first, capped at three. ?country= matches ANY of these while group\_by=country counts the first, so a story is findable under each and counted once. Empty is a correct answer for a story about no country in particular. |
| `region`                         | UTF8        | text                      | yes      | Sub-region label for the event geography below.                                                                                                                                                                                                                           |
| `admin1`                         | UTF8        | text                      | yes      | First-level administrative division (state, province, oblast) of the event below.                                                                                                                                                                                         |
| `location`                       | UTF8        | text                      | yes      | Most specific place name resolved for the event below.                                                                                                                                                                                                                    |
| `latitude`                       | DOUBLE      | decimal                   | yes      | Latitude, WGS84, of the event below.                                                                                                                                                                                                                                      |
| `longitude`                      | DOUBLE      | decimal                   | yes      | Longitude, WGS84, of the event below.                                                                                                                                                                                                                                     |
| `geo_source_event_id`            | UTF8        | text                      | yes      | The linked event region, admin1, location, latitude and longitude were taken from. NULL where the story has no located event — those five columns are then empty together, never mixed from different events.                                                             |
| `actor_origin_country_iso3s`     | LIST\<UTF8> | JSON array                | no       | ISO3 countries the actors in this story’s events come from, as distinct from where the events happened.                                                                                                                                                                   |
| `significance`                   | DOUBLE      | decimal                   | no       | Composite importance of the story, 0–1.                                                                                                                                                                                                                                   |
| `article_count`                  | INT         | integer                   | no       | Articles clustered into this story.                                                                                                                                                                                                                                       |
| `linked_event_count`             | INT         | integer                   | no       | Events coded from this story, as of as\_of.                                                                                                                                                                                                                               |
| `max_linked_event_significance`  | DOUBLE      | decimal                   | yes      | The highest significance among those events. NULL where the story has none.                                                                                                                                                                                               |
| `civilian_targeting_event_count` | INT         | integer                   | yes      | How many of those events the coder judged targeted civilians. NULL where the story has no events — never 0.                                                                                                                                                               |
| `fatalities`                     | INT         | integer                   | yes      | Reported deaths summed over those events. NULL where the story has no events, which is NOT zero deaths.                                                                                                                                                                   |
| `has_events`                     | BOOLEAN     | true / false              | no       | Whether any event was coded from this story. Four in five stories carry none — that is the corpus, not a gap.                                                                                                                                                             |
| `has_fatalities`                 | BOOLEAN     | true / false              | yes      | True where the linked events report deaths, false where they report none, NULL where there are no events to report any.                                                                                                                                                   |
| `has_civilian_targeting`         | BOOLEAN     | true / false              | yes      | Same three-valued rule as has\_fatalities, for civilian targeting.                                                                                                                                                                                                        |
| `linked_event_ids`               | LIST\<UTF8> | JSON array                | no       | Every event coded from this story. Join to event\_uid in the events file.                                                                                                                                                                                                 |
| `entity_names`                   | LIST\<UTF8> | JSON array                | no       | Resolved entities linked to this story, after the identity arbiter collapses duplicates.                                                                                                                                                                                  |
| `top_article_urls`               | LIST\<UTF8> | JSON array                | no       | The story’s leading article URLs, most representative first. The evidence, so a row can be checked.                                                                                                                                                                       |
| `top_article_titles`             | LIST\<UTF8> | JSON array                | no       | Titles for top\_article\_urls, positionally aligned.                                                                                                                                                                                                                      |
| `top_article_domains`            | LIST\<UTF8> | JSON array                | no       | Publisher domains for top\_article\_urls, positionally aligned.                                                                                                                                                                                                           |
| `language_codes`                 | LIST\<UTF8> | JSON array                | no       | Languages present in the story’s articles.                                                                                                                                                                                                                                |
| `language_counts`                | LIST\<INT>  | JSON array                | no       | Article counts for language\_codes, positionally aligned.                                                                                                                                                                                                                 |
| `top_language`                   | UTF8        | text                      | yes      | The most common language among the story’s articles.                                                                                                                                                                                                                      |
| `served_in_list`                 | BOOLEAN     | true / false              | no       | Whether this story is reachable by paging /api/v2/stories. Every story is retrievable by id; the list additionally folds near-duplicates, and this records that decision so the list is reproducible from the file.                                                       |
| `updated_at`                     | TIMESTAMP   | YYYY-MM-DD hh:mm:ss (UTC) | yes      | When the story itself was last written — the clock ?observed\_start / ?observed\_end bound on Stories.                                                                                                                                                                    |
| `as_of`                          | TIMESTAMP   | YYYY-MM-DD hh:mm:ss (UTC) | no       | What this row knew, and when. Stories are NOT vintaged — the next settle overwrites — so this is a currency ceiling, not a point-in-time snapshot.                                                                                                                        |

37 columns, 18 of them nullable.

## Summary statistics

Every published file carries a summary of its own contents, computed by the run that cut it
and shown on [Bulk Data Download](https://gdeltcloud.com/downloads). It is there so you can
see what is in a period before downloading it.

**Value counts** — the most frequent values, with how many distinct ones exist and how many
rows carry none. An absent value is reported separately and never counted as a category.

| Dimension                    | Column                       | Values published |
| ---------------------------- | ---------------------------- | ---------------- |
| Story category               | `category_code`              | top 15           |
| Event categories present     | `matched_category_codes`     | top 25           |
| Country                      | `country_iso3`               | top 25           |
| Countries mentioned          | `story_country_iso3s`        | top 25           |
| Actor origin countries       | `actor_origin_country_iso3s` | top 15           |
| Entities                     | `entity_names`               | top 25           |
| Publishers                   | `top_article_domains`        | top 25           |
| Languages                    | `language_codes`             | top 15           |
| Has coded events             | `has_events`                 | top 2            |
| Reachable by paging the list | `served_in_list`             | top 2            |

**Distributions** — count of rows carrying the metric, minimum, the 10th, 25th, 50th, 75th
and 90th percentiles, maximum and mean. Quantiles are exact and inclusive-interpolated, the
same convention pandas and Excel use, so you can reproduce them from the file you downloaded.

| Metric                        | Column                          | Total published |
| ----------------------------- | ------------------------------- | --------------- |
| Significance                  | `significance`                  | no              |
| Articles per story            | `article_count`                 | yes             |
| Events per story              | `linked_event_count`            | yes             |
| Top linked-event significance | `max_linked_event_significance` | no              |
| Fatalities                    | `fatalities`                    | yes             |

Percentiles are computed over the rows where the metric is PRESENT, so a metric that applies
to part of the file describes that part — it is never padded with zeroes to fill the rest.

## How the export differs from the warehouse

Most columns ship exactly as stored. Two conventions apply to all of them, and a handful of
columns are computed at export time.

**An absent value is null, never an empty string.** Wherever a value is missing the file
carries a true null in Parquet and an empty field in CSV, so an `IS NULL` check finds every
absent value. **Column names are the API's names**, so a row in the file and the same row
from `/api/v2/stories` agree field for field.

| Column                   | What to know                                                                                                                                                                                                                                        |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `country_iso3`           | DERIVED, and NOT the stored `country` column — that one is the event-derived display name, which disagrees with this for any story whose attribution is not its event location. Same precedence the API filters and groups on (storyCountryKeySql). |
| `geo_source_event_id`    | The stored column is empty on rows with no located event; a bulk consumer reading geography without its witness cannot tell a composite from an atomic one.                                                                                         |
| `has_events`             | Stored as UInt8. The CAST is load-bearing — a bare `!= 0` yields UInt8, which Parquet writes as an INTEGER, so the file would ship 1/0 against a dictionary promising BOOLEAN.                                                                      |
| `has_fatalities`         | Stored as a tri-state UInt8; the CAST keeps Parquet writing BOOLEAN rather than 1/0.                                                                                                                                                                |
| `has_civilian_targeting` | Stored as a tri-state UInt8; the CAST keeps Parquet writing BOOLEAN rather than 1/0.                                                                                                                                                                |
| `linked_event_ids`       | The FULL list. The API caps its `linked_events` block at ten, so a story with more events has more ids here than the endpoint shows.                                                                                                                |
| `entity_names`           | Renamed from entity\_ref\_names — "ref" is internal vocabulary. The parallel id/type/id\_space/wikipedia arrays are API-only; a flat file wants the names.                                                                                          |
| `served_in_list`         | Stored as UInt8. Filter on it to reproduce the endpoint’s pagination; leave it alone for the whole settled frame.                                                                                                                                   |
| `updated_at`             | Renamed from source\_updated\_at — the stored name says where it came from, not what it means.                                                                                                                                                      |
