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.