Reference

Skills

A skill is data, not code: one JSON file the executor interprets. It declares what it operates on, states its rules, and proposes — it never acts.

Anatomy

One file, four parts

{
  "id": "acme/my-skill",
  "version": "0.1.0",
  "name": "Acme Example Skill",
  "description": "What it checks and why.",
  "skill_type": "VENDOR",
  "disclosure_level": "L2",
  "migration_policy": "GATED",

  "reads":    ["headline", "assets"],
  "produces": ["skill.warning.raised",
               "skill.run.completed"],

  "advert": {
    "role": "compliance check",
    "operates_on": ["story.context"],
    "produces": ["skill.warning.raised"],
    "fires_on": ["headline",
                 "assets[].acquisition_state"]
  },

  "rules": [
    {
      "rule_id": "acme-style-001",
      "type": "term_match",
      "config": {
        "field": "headline",
        "terms": ["cops", "slammed"]
      },
      "default_severity": "flag",
      "affected_fields": ["headline"],
      "detail_template":
        "Informal term '{term}' in {field}."
    }
  ]
}
1 · Identity

id in vendor/skill-name form, a semver version, and the governance triple: skill_type, disclosure_level, migration_policy.

2 · Advert

The machine-readable claim. If operates_on doesn’t include story.context, the executor skips your skill entirely — and logs exactly that, once, at information level. Check the app log before debugging anything else.

3 · Rules

Each has a rule_id, a type, a config, a default_severity, its affected_fields, and a detail_template with {placeholder} substitutions.

4 · Severity

hold / flag / inform, lower-case. One of only two deliberate exceptions to SOM’s UPPER_SNAKE enum convention — the other being x- and com.{vendor} extension values.

Most vendors never write code. A skill file plus, optionally, a test scenario is the whole touchpoint — the bus topology, approval gate, audit trail and validation pipeline stay untouched. A custom rule type in the executor is the escape hatch when the seven built-ins genuinely can’t express the logic.

Rule types

Seven built-ins

field is always a dotted path relative to the payload root. Static validation checks that the required config keys are present and that any regex compiles.

TypeConfigFires whenSubstitutions
term_matchfield, terms[], case_sensitive?the field’s text contains one of the terms (once per term){term} {field} {value}
phase_with_missing_fieldphase, field, phase_field?the story is in phase AND field is empty{phase} {field}
field_value_infield, values[]the field equals one of values{value} {field}
field_presentfieldthe field exists and is non-empty{field}
field_absentfieldthe field is missing or empty{field}
field_regexfield, pattern, case_sensitive?the field matches the regex{match} {field} {value}
field_changedfield, to?, from?the value differs from the previous story version — optionally only for a given transition{field} {item} {from} {to}
field_changed is special

It is the only type that looks across versions, and the only one that supports an array wildcard — exactly one [], as in assets[].acquisition_state. Elements are matched between versions by asset_id / source_id / flag_id / id.

It stays quiet on the first sighting of a story, because there is nothing to compare against — and “first sighting” includes the first story after a restart.

Arrays otherwise

Every other rule type treats an array field — compliance, assets — as a whole, checking presence or absence. Per-element logic such as “any compliance flag of type X” means writing a custom rule type in the executor.

Validation

Three layers before you go live

1

Static

Schema and config-key checks, run automatically on every register or update. Unknown rule types, missing config keys and bad regexes come back as structured errors before anything reaches the bus.

2

Dry-run

Evaluates your rules against every seed story without publishing. You see precisely which stories fire which rules.

Dry-run has no “previous version”, so field_changed rules can never fire here. Test those live.

3

AI review

Optional. Ships the skill, the seeds and the dry-run result to an LLM for structured editorial feedback — a second opinion on the rule’s wording and intent, not a gate.

Iteration

The loop you’ll actually run

  • Register the skill — static validation runs on the spot.
  • Dry-run it against the seed stories and read which rules fired where.
  • Review (optional) with the AI pass for a sanity check on wording and intent.
  • Go live: publish seeds or run a scenario, and watch your run records and staged outputs appear.
  • For a field_changed rule, mutate a story so there is an actual transition to detect — advance a phase, or drive a media arrival to completion.

Your outputs then ride the same gate as everything else: staged → human decision → som.skills.events or som.skills.rejected, republished in a fresh gate-attributed envelope with the reviewer stamped in payload.extensions and the decision recorded on som.system.audit.

Pitfalls

Why it isn’t working

SymptomCause
My skill never runs, but dry-run firesThe advert’s operates_on doesn’t cover story.context. The executor logs this once per skill when the next story arrives.
A field_changed rule never firesNo previous version this session — first sighting, or a restart. Republish once to establish a baseline, then trigger the transition. Dry-run can never fire change rules.
Rule paths resolve to nothingThe config predates the v0.3.x renames: sources[] is now editorial_source[], skills_config.broadcaster is now skills_config.newsroom. See the rename table.
Rule fires but the detail reads wrongThe detail_template uses a substitution the rule type doesn’t provide. Each type offers a fixed set — check the table above.
Nothing appears for a warning I emitted externallyMessage-type names are suffixed on the wire. Consumers route on skill.warning.raised, not skill.warning.

Next: run the whole thing locally →