Editorial Workflows

Guards and enforcement

Define Content Lake enforcement contracts that protect referenced content while an instance occupies a stage.

Prerelease

Guards are workflow rules designed for enforcement where content writes land: the Content Lake. A guard declares which mutations are allowed while an instance occupies a stage.

Guards

Engine conditions and permission gates control engine-mediated operations. Guards serve a different role: they persist the enforcement contract beside the content it protects.

Use a guard when a document must not be published or otherwise changed during a stage. The guard identifies the protected document and the mutations it allows or denies.

A stage declares its guards inline. This one denies publishing the subject while the instance sits in review.

defineStage({
  name: 'review',
  guards: [
    {
      name: 'lock-subject',
      match: {idRefs: [{type: 'fieldRead', field: 'subject'}], actions: ['publish']},
      // no predicate ⇒ an unconditional deny while this stage holds
    },
  ],
  // activities, transitions ...
})

A guard is stored as its own document beside the content it protects. The engine creates it when the instance enters the guarded stage and deletes it on exit. Its lifetime therefore follows the stage visit.

During the prerelease, the Studio plugin and engine honor the active guard. Content Lake enforcement will extend the same contract to raw clients.

Loading...

A retracted guard is physically deleted; there is no retained inactive guard document and consumers do not need to filter lifted records. Retraction uses revision fencing so a stale deletion cannot remove a guard that a concurrent stage change has reactivated.

Freezing content in a stage

A common guard pattern is a field freeze. While the instance occupies a review stage, the guard allows writes only when the named fields remain unchanged.

defineStage({
  name: 'review',
  guards: [
    {
      name: 'freeze-review-fields',
      match: {idRefs: [{type: 'fieldRead', field: 'subject'}], actions: ['update']},
      // allow a write only if it leaves body and title untouched
      predicate: '!delta::changedAny((body, title))',
    },
  ],
  // activities, transitions ...
})

The guard protects content. Conditions on the instance still decide who approves and when the workflow moves.

“Publish only while in stage X” is the same shape with actions: ['publish'], as in the first example on this page. Use a publish guard when publication must be held until the instance leaves the stage.

During the prerelease, the Studio plugin reads deployed guards and disables native write actions they deny (publish, unpublish, and delete), naming the workflow that holds the document.

Author one guard per concern. Do not make a rule depend on the evaluation order of overlapping guards.

Identity and grants

Every move in a workflow is made by whoever holds the token the call is made with. The engine reads who the token belongs to and treats them as the actor, the person or agent the action is attributed to, stamped into history and matched against role rules. That same token also performs the engine’s own writes. The engine has no separate identity of its own. History also records an advisory second half alongside the actor: an execution context, the runtime plus an optional {kind, id} the host declares once at createEngine. A server proxy acting with a user’s token then shows up as that user, via that proxy, instead of forging an identity.

The same one-token rule covers the work the engine fires itself. A cascade-fired action (one with a when, fired by the engine the moment its condition holds) executes on the cascading token, whichever legitimate call drove the cascade; there is no synthetic engine actor. An action can pin who may execute it with roles; a trigger whose pin the current token doesn’t fulfill stays armed and fires on the next capable cascade (in practice, a scheduled robot). History records the executing token together with a triggered marker, so an automated firing is never mistaken for a caller invoking the action. Locks deployed or deleted on a hop a trigger fires ride that cascading token as well, so the authoring caution above covers automated moves too.

Caller-bound conditions can read the caller through $actor and their permissions through an advisory $can, so a rule can say “only the assigned reviewer may approve.” A caller is bound only where one is actually acting: the filter on an action a caller fires, an activity’s requirements, and editable predicates. Routing is deliberately caller-blind: a transition’s when, an activity’s filter, and a cascade-fired action’s when and filter are re-evaluated by whatever legitimate call comes along and must resolve the same way regardless of whose token that is, so deploy rejects a caller read there. (The Content Lake’s identity() function exists only in a guard’s predicate context, not in workflow conditions.) As with every check the engine makes, that gate is advisory. The Content Lake’s access control is what holds.

Reference

Guard declaration

This is the object passed to defineGuard(...) in a stage’s guards array. The guard is active for that stage visit and targets referenced content, not the instance document.

  • name

    string

    Required guard name; unique within the definition.

  • title

    string

    Optional display label.

  • Optional explanation.

  • match

    GuardMatch

    Required target selector: document types, ids or patterns, and mutation actions.

  • predicate

    string

    Optional Content Lake delta-mode GROQ predicate. Strictly true allows; any other result denies.

  • metadata

    Record<string, GuardRead>

    Optional workflow values projected for the predicate as guard.metadata.

Visiting agent?

Was this page helpful?