Reference · SOM 1.0

The seven message families

Everything normative in SOM is seven JSON Schemas: the envelope, the story, and five families for links, tellings, media arrivals, audit and skill warnings. Each is served at a URL that won’t change for the life of 1.x.

Schemas

Everything on the wire

Most families are opt-in: implement the envelope and whichever families you have something to say about. Topic names are yours to lay out — the only rule is the som. prefix, and consumers dispatch on message_type, never on the topic.

Familymessage_typePublished bySchema
Message envelope
The closed wrapper every message carries. Its payload is polymorphic on message_type.
(every message)
every publisherenvelope.schema.json ↗
story.context
The complete state of one story at a moment in time — a snapshot, never a delta.
story.context
the story ownerstory-context.schema.json ↗
som.telling.*
When an asset met an audience. On-air state is derived from these, never stored on the asset.
telling.started
telling.ended
telling.exposed
the exposure publishertelling-event.schema.json ↗
som.link.*
An asset committed to a destination, with a compliance gate per destination. assets[].usage[] is maintained from these alone.
link.committed
link.gate_changed
link.withdrawn
the committing systemlink-event.schema.json ↗
delivery.media_available
Media has arrived in a store — by TAMS Source or by a non-TAMS locator. No bytes, no story_id.
delivery.media_available
the media store (MAM / TAMS)delivery-media-available.schema.json ↗
som.system.audit
The governance trail: CLEARED, SUPPRESSED, WITHHELD or OVERRIDDEN, against a link, asset or telling.
system.audit
any governance actorsystem-audit.schema.json ↗
skill.warning.raised
What a skill declared about a story, published by the executor inside the tool that recalled it.
skill.warning.raised
a skills executorskill-warning.schema.json ↗
# check one message from the command line
pip install jsonschema rfc3339-validator
curl -sO https://storyobjectmodel.com/schema/1.0/story-context.schema.json

The family you implement first

story.context

The complete state of one story at one moment. Required: story_id, slug, headline, story_type, sequence_number, updated_at — plus lifecycle when the story is ACTIVE. Everything else is optional, and the object is closed.

{
  "story_id": "hurricane-2026-0911",
  "slug": "HURRICANE-GULF",
  "headline": "Hurricane upgraded to Cat 4 after landfall",
  "story_type": "ACTIVE",
  "lifecycle": { "phase": "BREAKING",
                 "phase_entered_at": "2026-09-11T16:24:00Z" },
  "sequence_number": 4,
  "updated_at": "2026-09-11T16:26:00Z",
  "tags": [
    { "scheme": "iptc-mediatopic", "value": "17000000",
      "label": "weather" },
    { "scheme": "newsroom", "value": "us-desk" }
  ],
  "editorial_source": [ { "source_id": "src-ap-wire",
    "source_type": "WIRE", "provider": "AP",
    "credibility": "TRUSTED", "received_at": "…" } ]
}
What else a story can carry
  • tags[] — what the story is about. Ordered (the first is primary) and scheme-qualified: newsroom, iptc-mediatopic or com.{vendor}.{name}.
  • priority, premise (expected outcome, confidence, whether it changed and which assets that affects), story_meaning (a capped 5W1H kernel).
  • assets[] with media_refs[], provenance, standards_clearance, authenticity_credential and the usage[] back-index.
  • assertions[], compliance[] flags (with an optional media_range pinning the restricted frames) and editorial_gates[].
  • government_approval — a publication gate the executor must not publish past while it blocks.
  • skills_config, content_refs[], relations[], collaboration, extensions.

Media arrivals

delivery.media_available

A store announcing that media has arrived. That is all it does: no bytes, no credentials and deliberately no story_id — you resolve asset_id → Asset → Story from the story stream.

// a TAMS store
{
  "message_type": "delivery.media_available",
  "delivery_id":  "0190a000-0000-7000-8000-00000000aaa1",
  "asset_id":     "asset-presser-feed",
  "source":       "tams://tams-gcp-store/9f2e7c1a",
  "time_range":   "[0:0_1260:0)",
  "arrived_in":   "tams-gcp-store",
  "arrived_at":   "2026-09-11T16:30:00Z"
}

// any other store
{
  "message_type": "delivery.media_available",
  "delivery_id":  "0190a000-0000-7000-8000-00000000ccc1",
  "asset_id":     "asset-still-embassy",
  "locator": { "store": "image-mam",
               "ref":   "stills/2026/07/embassy-front-04.jpg" },
  "arrived_in":   "image-mam",
  "arrived_at":   "2026-07-09T15:00:00Z"
}
The rules
  • One of source or locator is required. A source is a fully-qualified TAMS Source URI, tams://store/id — Sources, never Flows.
  • time_range is a TAMS timerange: bracketed seconds:nanoseconds bounds, with an open start or end permitted — a feed still recording is [0:0_).
  • Against a locator, a range is offset from the stored file’s own zero, never from embedded source timecode.
  • Binding media to a story happens at the editorial act, never at ingest. A clip that matches no story is held in an ORPHAN shell story until a proposed match is confirmed.

Distribution

Links and tellings

som.link.*

Asset ↔ destination

link.committed, link.gate_changed, link.withdrawn. Every event carries link_id, asset_id, destination_id and a compliance_gate_status of PENDING, CLEARED or BLOCKED. A commit names who and when; a withdrawal names who, when and why.

The gate is per destination, so one asset can be cleared for digital and blocked for broadcast at once. The story’s assets[].usage[] is maintained from these events alone — committed-only and fail-closed.

som.telling.*

Exposure

telling.started (needs exposure_start, forbids exposure_end), telling.ended, and telling.exposed for an instantaneous exposure carrying both. Exposure times are immutable; scheduled_start is mutable and never used to derive on-air state.

transforms[] records reshapes at the edge — CROP, TRIM, CAPTION_BURN or an x- vendor value — in application order, append-only. A transform never lifts a compliance[].media_range hold.

Governance

som.system.audit — the trail

{
  "audit_id":    "0190e000-0008-7000-8000-0000000000f3",
  "action":      "SUPPRESSED",
  "target":      { "kind": "ASSET", "id": "pkg-acquit" },
  "actor":       { "actor_id": "automation-01", "actor_type": "system" },
  "reason":      "Acquit package suppressed; never linked to air",
  "recorded_at": "2026-06-23T14:30:02.000000Z"
}
  • action is one of CLEARED, SUPPRESSED, WITHHELD, OVERRIDDEN; target.kind is LINK, ASSET or TELLING.
  • The actor is an object, never a flat string. A suppression targets the held asset — the branch that never airs never gets a link.
  • Distinct from skill execution records. It records governance: clearances, suppressions, withholdings and overrides, with who and why.

Integrating

What you implement, by role

You are…PublishReadWatch out for
Story owner
NCS, planning
story.contextskill.warning.raised, assertions from othersEvery field on every snapshot. Immutable story_id, rising sequence_number, re-stamped originating_system.
Tool with an executor
rundown, MAM, CMS, playout, graphics
skill.warning.raisedstory.contextYour tool decides what it withholds. Never edit content to release a hold, and never render a hold as a banner with a “publish anyway” button.
Media store
TAMS, MAM
delivery.media_available—Source URIs, not Flow ids — or a locator. Re-announce with a growing time_range while capture continues.
Committing / exposing system
CMS, playout, social
som.link.*, som.telling.*story.contextGate status per destination. Exposure times are event-stamped and immutable.
Governance actor
standards, legal, automation
som.system.auditeverything relevantActors are objects. Record the reason.

Next: how a skill is recalled →