Build

Get started

Read the short documents, run the conformance corpus, validate your own message with format assertion on, and publish it. Then pick a tier and talk to the working group.

Step 2

Run the conformance corpus

The specification repository ships worked examples that must validate, twenty negative cases that must be rejected, and pure-Python tools that check both.

git clone https://github.com/storyobjectmodel/som.git
cd som
pip install jsonschema rfc3339-validator

python3 tools/validate.py            # every example is legal
python3 tools/validate_negative.py   # every negative case is rejected
python3 tools/som_lint.py schema     # schemas agree with their own claims
python3 tools/validate_sequence.py examples/hurricane-run
                                     # a story over time holds together
Four different questions
  • Is this message legal? — validate.py
  • Do the schemas actually constrain? — validate_negative.py. Nineteen cases fail on the schemas; the twentieth, som_version: "0.3.2", fails on the wire-version rule, which your implementation must apply itself.
  • Does the schema match what it says about itself? — som_lint.py
  • Does a run of snapshots hold together? — validate_sequence.py, where the expensive bugs live.

Install rfc3339-validator. Without it, Python’s jsonschema quietly accepts a malformed timestamp. The negative corpus includes that case precisely so the check fails loudly when the package is missing.

Step 3

Validate in your own stack — with formats asserted

Two implementations that disagree about whether message_id must be a UUID are not interoperable. In Node that means Ajv’s 2020-12 build plus ajv-formats.

// npm i ajv ajv-formats
import Ajv2020 from 'ajv/dist/2020';
import addFormats from 'ajv-formats';

const BASE = 'https://storyobjectmodel.com/schema/1.0';
const FAMILY_SCHEMA: Record<string, string> = {
  'story.context': 'story-context',
  'skill.warning.raised': 'skill-warning',
  'delivery.media_available': 'delivery-media-available',
  'link.committed': 'link-event', 'link.gate_changed': 'link-event', 'link.withdrawn': 'link-event',
  'telling.started': 'telling-event', 'telling.ended': 'telling-event', 'telling.exposed': 'telling-event',
  'system.audit': 'system-audit',
};

const ajv = new Ajv2020({ strict: false, allErrors: true });
addFormats(ajv); // conformance §4: uuid and date-time MUST be asserted

const load = async (name: string) => (await fetch(`${BASE}/${name}.schema.json`)).json();

export async function validateMessage(msg: { som_version?: string; message_type?: string; payload?: unknown }) {
  const envelope = ajv.compile(await load('envelope'));
  if (!envelope(msg)) return { ok: false, errors: envelope.errors };

  // §3: 0.3.2 is not SOM 1.0 — and never branch on the version otherwise.
  if (msg.som_version?.startsWith('0.')) return { ok: false, errors: ['pre-1.0 som_version'] };

  // §2 and §5: dispatch on message_type only; ignore families you don't handle.
  const family = FAMILY_SCHEMA[msg.message_type ?? ''];
  if (!family) return { ok: true, ignored: true };

  const payload = ajv.compile(await load(family));
  return payload(msg.payload) ? { ok: true } : { ok: false, errors: payload.errors };
}

Illustrative, not a reference implementation — cache compiled validators rather than fetching per message. The schemas are served at the URL in their own $id, so resolving by identifier works too.

Step 4

Publish a conformant story snapshot

{
  "som_version": "1.0.0",
  "message_id": "0199a1c4-7a2e-7b31-8c55-4d2f9e6a1b07",
  "correlation_id": "0199a1c4-0000-7000-8000-000000000911",
  "message_type": "story.context",
  "timestamp": "2026-09-11T16:41:00.000000Z",
  "originating_system": {
    "system_id": "ncs-nyc-01", "system_type": "ncs"
  },
  "topic": "som.story.context.hurricane-2026-0911",
  "payload": {
    "story_id": "hurricane-2026-0911",
    "slug": "HURRICANE-LANDFALL",
    "headline": "Hurricane makes Cat 4 landfall",
    "story_type": "ACTIVE",
    "lifecycle": { "phase": "BREAKING",
                   "phase_entered_at": "2026-09-11T14:00:00Z" },
    "sequence_number": 7,
    "updated_at": "2026-09-11T16:41:00Z"
  }
}
Checklist for every snapshot
  • A fresh UUID message_id (v7 recommended); the same correlation_id for the story’s lifecycle.
  • The same story_id, a higher sequence_number, a later updated_at.
  • Every field, including the ones other systems contributed. Omission means absent.
  • lifecycle present if and only if story_type is ACTIVE.
  • Your own originating_system, re-stamped on every publish.
  • Anything the standard doesn’t carry goes under extensions["com.{vendor}.…"].

See it on a live bus. The project’s emulator ↗ is a dashboard on a running bus you can use yourself, and the IBC demos ↗ show two full scenarios end to end. A .NET reference implementation is to follow in a separate repository; the pre-1.0 hackathon starter emits 0.3.2 and is not 1.0 traffic.

Joining

Nothing about your product has to come out

One system is enough to start. Integrations are described in three tiers.

1

Subscribe and publish

Read the story and put your own family on the bus. The project estimates this in days, not months.

2

Bidirectional context

Your tool both consumes and contributes structured context — assertions, provenance, links, tellings.

3

Decide against the story

Your tool acts autonomously on what it reads, through an executor applying the house’s skills.