CodamAIDocs
Topicdone

Life cycle of a revision

From the draft through review to the published revision, plus reject, roll back and retire. Nothing is changed; every step is a new entry in a hash chain.

Variants
IN_REVIEWPUBLISHEDSUPERSEDEDREJECTEDRETIREDRoll back

What this is about

A template is a controlled document: it changes only through a new, reviewed state, and every report can be traced back to exactly that state. For this, every language variant has a chain. That is a list of entries that only grows. Every entry records one step: submitted, approved, rejected, published, superseded, retired.

The states

flowchart LR
    D["Draft"] -->|submit| R["IN_REVIEW<br/>in review"]
    D -->|"submit,<br/>no approval stages"| P
    R -->|"last stage<br/>approved"| P["PUBLISHED<br/>published"]
    R -->|reject| X["REJECTED<br/>rejected"]
    P -->|"next revision<br/>published"| S["SUPERSEDED<br/>superseded"]
    P -->|retire| T["RETIRED<br/>retired"]
    S -.->|"roll back:<br/>new revision"| P
    T -.->|"roll back:<br/>new revision"| P
StateMeaning
IN_REVIEWsubmitted, running through the approval stages
PUBLISHEDthe revision of the variant that renders. There is at most one
SUPERSEDEDwas published, was replaced by a later one
REJECTEDrejected in review. A new attempt is a new revision
RETIREDwas published, was withdrawn without a successor

The draft is not a state of a revision. It sits next to the chain and only becomes a revision when you submit it.

The actions

What you can do with a variant

When: the draft is ready

  1. 1
    Client→CRMS
    POST /crms/variants/{variant}/submit with {"reason": "…"}
  2. 2
    CRMS
    checks the write role of the scope and the reason
  3. 3
    CRMS
    is there no draft? → 409 template-draft-missing
  4. 4
    CRMS
    lets the renderer parse the source. Syntax error → 422 template-source-rejected with violations
  5. 5
    CRMS
    is another revision already in review? → 409 template-review-pending
  6. 6
    CRMS→Database
    appends an entry REVISION. If the scope has no approval stages, PUBLISHED follows right away
    Result: new revision, IN_REVIEW or PUBLISHED

When: a revision is in review

POST /crms/revisions/{revision}/approve. Whoever has the role of the current stage approves. After the last stage, the revision is published. All rules are in Approval chains.

Result: entry APPROVED, after the last stage also PUBLISHED

When: a revision in review is not right

POST /crms/revisions/{revision}/reject with a reason. Here the reason is always required. Whoever rejects needs the role of the current stage. The draft stays; you keep working in it and submit again.

Result: entry REJECTED, the revision stays rejected

When: an earlier version should apply again

POST /crms/variants/{variant}/rollback with {"target": "<revision>", "reason": "…"}. The target must be SUPERSEDED or RETIRED. CRMS creates a new revision with the old content and publishes it at once, without approval stages: the content was approved before.

Result: new revision with rollbackOf, PUBLISHED. If there was a published one so far, it becomes SUPERSEDED

When: the variant should not render at all anymore

POST /crms/variants/{variant}/retire with a reason. The published revision becomes RETIRED, and the variant has no published revision anymore. Without a published revision, CRMS answers 409 template-not-published.

Result: entry RETIRED, answer 204

Submit, roll back and retire need the write role of the scope, see How a template is built. Who may approve and reject is set by the approval chain, see Approval chains.

Continuing from an old state

POST /crms/revisions/{revision}/draft with {"expectedVersion": 3} copies the content of any revision into the draft of its variant. This way you continue after a rejection or from an older state without typing everything again.

The reason

A reason is a short text saying why something happens. It goes into the chain.

ActionReason required?What happens without a reason
Rejectalways422 template-reason-required
Submit, roll back, retireyes (default)422 template-reason-required
Submit, roll back, retireswitched offThe action runs, the entry has no reason
Approve–An approval has no reason, it has a meaning (see Approval chains)

You switch the requirement off with CODAMAI_CRMS_TEMPLATE_REASON_REQUIRED=false, in the hub with the field templateReasonRequired. A reason has at most 2000 characters, otherwise template-reason-too-long|2000.

The chain and its hashes

A hash is a fingerprint: a short string that changes completely as soon as one character of the content changes. CRMS uses SHA-256 and writes a hash as sha-256:<hex>.

  • The content hash of a revision is the hash of its source. Fields and assets are in the REVISION entry with their own hashes.
  • The hash of an entry covers everything the entry says, plus the content hash of the revision and the hash of the entry before it.

Because every entry includes the previous one, a changed, deleted or reordered row stands out. And because an approval also carries the content hash, it binds exactly the content that was approved.

Read the trail
GET /crms/variants/{variant}/trail
The entries of the chain
[
  {
    "sequence": 1,                  ← position, without gaps
    "type": "REVISION",
    "revision": "0b7e…",
    "actor": "anna",                ← from the token
    "at": "2026-10-17T08:30:00.123456Z",
    "reason": "New logo",
    "attributes": { "number": 1, "scope": "TENANT",
                    "locale": "de", "fields": [ … ],
                    "assets": [ … ], "approval": { … }, … },
    "contentHash": "sha-256:9f86…",
    "hash": "sha-256:41c2…"
  },
  {
    "sequence": 2,
    "type": "APPROVED",
    …
    "previousHash": "sha-256:41c2…", ← hash of entry 1
    "hash": "sha-256:a07d…"
  }
]
CallEffect
GET /crms/variants/{variant}/revisionsall revisions with state, number, author, reason, hash, stages and approvers, oldest first
GET /crms/variants/{variant}/trailall entries of the chain, with every hash, to recompute it
POST /crms/variants/{variant}/verifyCRMS recomputes the chain. Answer {"intact": true} or {"intact": false, "brokenAt": 4} with the first entry that does not match
GET /crms/revisions/{revision}/contentsource, fields and described assets of a revision

Every entry also goes into the journal of the tenant, for system templates into the platform’s. So the proof also lives outside the template tables. More in The hash chains.

Pitfalls

Next

Search