Summary#
[Authorize] on a workflow event declares WHO may raise it. It is a single-parameter principal => <predicate>
lambda — the parameter is the acting principal, and this.Item (the workflow's tracked entity) is in scope — evaluated
when the event is raised. A principal who does not satisfy the predicate is refused; the raise never runs a route or a
transition. This is produce-side authorization: a different question from a slot's Candidates (who may HOLD a work
item), and the answer for an event that has no slot to hang authorization on — the canonical case being a Cancel that
any state should honour but only certain principals may trigger.
Signature#
[Authorize(<principal> => <predicate over the principal and this.Item>)]
event <Name>(<typed params>);Description#
An event is a thing the outside world can raise on a running workflow. Left undecorated, anyone with access to the
workflow's surface may raise it. [Authorize] narrows that: it is a boolean predicate the engine evaluates against the
acting principal the moment the event is raised, before any routing or transition happens.
- The lambda takes exactly one parameter — the acting principal — typed as the app's
[Principal]entity. this.Item(the tracked entity) is in scope, so the predicate can compare the principal to the item — the common shape is ownership (u => u == this.Item.Requester) or a role/relationship test (u => u == this.Item.Requester || u.Role == Role.Support).- The predicate is fail-closed: if there is no acting principal, no
[Principal]entity, or the principal cannot be resolved, the raise is refused.
A refused raise throws — it does not route to an on <Event>.Denied arm (that would invite an author to write an
empty one and silently swallow a security failure). The refusal is recorded on the workflow's audit timeline, so "who
tried to raise this and was refused" is a query, and the entity does not move.
Relationship to Candidates#
[Authorize] and a slot's Candidates answer different questions and do not substitute for each other:
| Question | Scope | |
|---|---|---|
[Authorize] on an event | who may raise this event | the event (workflow-wide) |
Candidates on a subscribe | who may hold / satisfy this slot | one slot in one state |
An event with no slot (Cancel) can only be gated with [Authorize]. A slot in a specific state that different
principals may act on is gated with Candidates. A workflow may use both.
Raising a gated event from anywhere#
A workflow-level route (on Cancel { goto Cancelled; }, declared once beside the states) is live in every non-terminal
state, so a gated Cancel can be raised at any point in the run and the same route decides where it goes. A state may
override the workflow-level route for that event by declaring its own on <Event> (nearest-wins).
Examples#
enum Decision { Approve, Reject }
enum OrderState { Placed, Done }
[Principal]
entity Person {
[Required, MaxLength(200)] string Email;
security { allow read, create when IsAuthenticated; }
}
entity Order {
[Required, MaxLength(60)] string Reference;
[Required] Person Requester;
OrderState Status; // no default: the workflow owns this field
security { allow read, create, update when IsAuthenticated; }
}
workflow OrderFlow {
Tracks = Order.Status;
Autostart = true;
Initial = Placed;
// The rule travels with the EVENT, so every path that could raise it is covered by one line.
[Authorize(u => u == this.Item.Requester)]
event Cancel();
state Placed {
subscribe Cancel();
on Cancel { goto Done; }
}
terminal success Done { }
}Only the requester or a support agent may cancel an order, and a cancellation is honoured from any state:
[Authorize(u => u == this.Item.Requester || u.Role == Role.Support)]
event Cancel();
on Cancel { goto Cancelled; }The requester alone may cancel:
[Authorize(u => u == this.Item.Requester)]
event Cancel();See also#
- subscribe — a slot's
Candidates, the who-may-HOLD gate (contrast with who-may-RAISE here)