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
- content — Article lifecycle, content storage, and state transitions
- authors — Author profiles, roles (writer, editor, reviewer), and permissions
- categories — Content categorization and taxonomy
- engagement — Comments, reactions, and reader interaction
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
create_article— Create a new article in draft stateupdate_article— Modify article content (only in draft or in_review state)delete_article— Delete an article (only in draft state)get_article— Retrieve article by IDlist_articles— List articles with filtering by state, author, category
Lifecycle transitions
submit_article— Move from draft to in_reviewapprove_article— Move from in_review to approvedreject_article— Move from in_review back to draft (with reviewer notes)publish_article— Move from approved to publishedarchive_article— Move from published to archived
Supporting capabilities
upload_media— Attach a media asset to an articledelete_media— Remove a media assetcreate_comment— Add a comment to a published articledelete_comment— Remove a comment (moderation)assign_category— Tag an article with a category
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:
- The
engagementmodule referencescontent.articlebut does not referenceauthors.authordirectly. Comment display needs author names, but this is resolved through the article's author reference, not a direct cross-module dependency. - The
categoriesmodule is self-contained. It defines taxonomy but does not depend on content, authors, or engagement. - The
contentmodule referencesauthors.authorfor article authorship and reviewer assignment.
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
- Declarative state machines with compiler-enforced transitions
- Deny policies for preventing specific role-action combinations
- Two-layer defense: policy-level deny plus invariant-level constraint
- Media asset invariants for content integrity
- Module boundaries that isolate engagement from content lifecycle