States, transitions, guards, and the effects a transition runs.

Workflows

Transitions are event: From -> To. The same event may leave several states, and a transition whose source and target match is a self-loop — useful for recording that something happened without moving the record.

A state line decorates a name the states list already declares. color is the lane colour, and sla is how long a record may sit there before the sweep runs the actions in the block. The block runs engine actions the sweep can enqueue. do notify and do schedule need an event to trigger on, so they are refused there. A transition ending in signature requires an electronic signature before it fires.

app reviews "Reviews" {}

entity Document {
  approver: text

  workflow {
    initial Draft
    states Draft, Review, Approved
    terminal Approved

    state Review color #f59e0b sla 48h {
      do webhook "review-overdue"
    }

    approve: Review -> Approved requires approver signature
  }
}

Guards have three spellings, in increasing order of power:

app approvals "Approvals" {}

entity Article {
  workflow {
    initial Draft
    states Draft, Review, Approved, Rejected
    terminal Approved, Rejected

    submit:  Draft -> Review requires title, body
    approve: Review -> Approved when approvals >= 2 and author != requester
    reject:  Review -> Rejected guard jsonlogic { "and": [{ "!!": [{ "var": "rejection_reason" }] }, { "==": [{ "var": "locked" }, false] }] }
  }
}

requires a, b means those fields must be present. when <expr> is a record-local expression. guard jsonlogic { … } is the literal form, for anything the expression grammar cannot say — the escape hatch that keeps the language small without making it limiting.

A guard may also count related records, which the engine evaluates against the database rather than the record in hand:

app tickets "Tickets" {}

entity Ticket {
  workflow {
    initial Open
    states Open, Closed
    terminal Closed

    close: Open -> Closed when count(Note where ticket == this) >= 1
  }
}

Making a transition do something

A transition can carry effects, which run inside the transition's own transaction. If one fails, the transition rolls back with it:

app leads "Leads" {}

entity Lead {
  owner: text

  workflow {
    initial Qualified
    states Qualified, Converted
    terminal Converted

    convert: Qualified -> Converted when owner != null {
      do spawn Opportunity from this
      do update Lead set converted_at = now where id == this.id
      do append AuditEntry { "reason": "converted" }
      do notify owner "Lead converted"
    }
  }
}

The verbs are append, update, delete, transition, spawn, webhook, run, algorithm, recompute, notify and schedule. Inside an effect, this is the record the transition fired on and me is the person who fired it.

Because every firing of a self-loop appends to the event ledger, and that ledger is append-only, bound a self-loop that fires automatically — with its own guard (when retry_count < 3) or with scheduling.