Content Publishing Example

An article publishing platform with a formal state machine, role-based deny policies, and media asset invariants.

The Problem

Content publishing platforms have a deceptively simple core — articles go from draft to published — but the lifecycle rules, role permissions, and content integrity constraints are where the real complexity lives. Can a writer publish their own article? Can a published article go back to draft? What happens to comments when an article is archived? Can media assets exceed a size limit?

In traditional codebases, these rules are spread across controllers, middleware, database validations, and business logic layers. An AI agent asked to "add a scheduled publishing feature" would need to discover the state machine by reading code, then hope it correctly identifies all the transition constraints. SysMARA makes the state machine, the deny rules, and the invariants explicit and machine-readable.

System Structure

Modules

Entities

Entity Module Description
article content A piece of content with title, body, state, author reference, and reviewer reference
author authors A content creator with role (writer, editor, admin) and profile
category categories A taxonomy node for organizing content
comment engagement A reader comment attached to a published article
media_asset content An image, video, or file attached to an article

Capabilities

The system defines 15 capabilities covering CRUD operations and the full article lifecycle:

Content CRUD

Lifecycle transitions

Supporting capabilities

Article State Machine

The article lifecycle follows a strictly forward state machine:

draft → in_review → approved → published → archived
  ↑______________|
  (reject returns to draft)

The only backward transition is reject_article, which returns an article from in_review to draft. All other transitions move forward. This is enforced by the article_state_transitions_only_forward invariant.

The state machine is defined declaratively in the spec:

# specs/content/flows/article-lifecycle.yaml
name: article_lifecycle
type: state_machine
entity: article
field: state
states:
  - draft
  - in_review
  - approved
  - published
  - archived
transitions:
  - from: draft
    to: in_review
    capability: submit_article
  - from: in_review
    to: approved
    capability: approve_article
  - from: in_review
    to: draft
    capability: reject_article
  - from: approved
    to: published
    capability: publish_article
  - from: published
    to: archived
    capability: archive_article

Key Invariants

The system enforces 10 invariants:

Invariant Severity Description
article_state_transitions_only_forward critical State transitions must follow the declared state machine. No skipping states (e.g., draft directly to published).
published_article_must_have_reviewer critical An article in published state must have a non-null reviewer_id. Articles cannot reach published without going through review.
slug_must_be_unique critical Article slugs must be globally unique across all states.
media_size_limit error Individual media assets cannot exceed 50MB. Total media per article cannot exceed 200MB.
draft_article_deletable_only critical Only articles in draft state can be deleted. Published or archived articles must be archived, not deleted.
comments_only_on_published error Comments can only be added to articles in published state.
archived_article_immutable critical Once archived, article content cannot be modified.
author_must_exist critical Every article must reference a valid author.
category_not_empty_for_publish error An article must have at least one category assigned before it can be published.
reviewer_is_not_author critical The reviewer of an article cannot be the same person as the author.

The Deny Policy: writer_cannot_publish_own

One of the most important access control rules is a deny policy:

# specs/content/policies/writer-cannot-publish-own.yaml
name: writer_cannot_publish_own
type: deny
priority: 150
description: "A writer cannot publish an article they authored"
condition:
  role: writer
  capability: publish_article
  match: "request.user_id == article.author_id"

This deny policy has a high priority (150), ensuring it overrides any allow policies that might grant writers general publishing access. Even if a future policy change grants writers broader permissions, this deny rule prevents the specific case of self-publishing.

Combined with the published_article_must_have_reviewer invariant, this creates a two-layer defense: the policy prevents the action at the access control layer, and the invariant prevents the state transition at the data layer.

Module Boundaries

The module structure creates clear responsibility boundaries:

This means adding a new engagement feature (like reactions) only impacts the engagement module and its reference to content.article. It does not affect authors, categories, or the article state machine.

CLI Commands

Validate the full system

npx sysmara validate

Explain the state machine

npx sysmara explain flow content.article_lifecycle

Outputs all states, valid transitions, and which capabilities trigger each transition.

Check impact of adding scheduled publishing

npx sysmara impact change-plans/add-scheduled-publishing.yaml

Would flag that the state machine needs a new transition (approved → scheduled → published), that the article_state_transitions_only_forward invariant needs updating, and that a new scheduled_at field is needed on the article entity.

Explain why a writer cannot publish

npx sysmara explain policy content.writer_cannot_publish_own

Shows the deny rule, its priority, the condition, and which invariants provide additional protection.

What This Example Demonstrates