Guards¶
In one sentence¶
A guard is a small condition in when= that chooses which transition an event takes, from the data of the token and the event.
Why it exists¶
One event can lead to different places. For example, a failed test run goes back to coding two times, but the third failure ends the token. The routine must not know this rule, because it does not see the graph.
So the graph holds the rule, as a guard on the transition. Stateous checks the guards when you compile, so a token never has two ways to go.
Routines decide, and guards only route. A decision about the work, such as "the body of the ticket has the word 'down'", is the work of a routine. The routine returns the event that names the decision, for example urgent or normal. A guard only chooses a transition from data that is already there: a count, a field of the payload, a field of the context. So the guard language has no text search and no arithmetic. See Where data goes.
How it works¶
A guard is an expression in a small language. It is not Python, and nothing runs it as Python. It can use these items:
| Item | What it reads |
|---|---|
ctx['key'] |
A field of the token's context, with the changes of the routine. |
payload.field |
A field of the payload of the event. |
count('event') |
How many times the token accepted this event, this one included. |
<, <=, >, >=, ==, !=, in, not in |
Comparisons. |
and, or, not |
Logic. |
1, -2.5, 'text', true, false, null, [1, 2] |
Numbers, strings, the three names, and lists of these. |
Some rules of the language:
- A guard reads the context as
ctx['key'], never asctx.key. It cannot readctx.memory. - On the right of
inandnot in, use a list, or a value fromctx,payloadorcount. A string is not permitted there. - A value that is not set reads as
null. A comparison ofnullwith a number is false, not an error. count('event')counts the event over the whole life of the token, not for one visit.- A guard on a transition from
STARTreads only the context. A new token has no event yet, so it has no payload and no count. - A guard on
joinedcan also usebranches.all(...)andbranches.any(...). See Parallel and nested work.
When an event comes, the control plane looks at the transitions from the token's state on that event:
- If one guard matches, the token takes that transition.
- If no guard matches, the token takes the transition with
otherwise=True. - If no guard matches and there is no
otherwise=True, the control plane refuses the event. The token stays where it is.
The rule of otherwise=True. This is the one place where the docs give this rule:
- A transition that is the only one from its state on its event needs no guard and no
otherwise=True. - When a state has two or more transitions on one event, each has a guard, except at most one. That one has no guard, and it must have
otherwise=True: it is the fallback. - The fallback is optional. With no fallback, an event that no guard matches is refused.
Compile stops if a transition breaks this rule.
The overlap check¶
stateous compile makes sure that the guards on one state and one event never overlap. Two guards overlap when some data can make both of them true.
If two guards can overlap, compile stops with an error, and it gives an example of such data. Stateous never chooses between two matching guards at run time.
Compile also stops when it cannot prove that two guards never overlap. This occurs when a guard compares two values with each other, orders a list, or uses branches. Then make one transition the fallback with otherwise=True.
Example¶
In sdlc, the event failed has two transitions from testing:
flow.transition(testing, coding, on=failed, when="count('failed') < 3")
flow.transition(testing, END, on=failed, otherwise=True)
The first and second failed have a count of 1 and 2, so they go back to coding. The third has a count of 3, so the token ends.
A guard can also read the payload and the context. The orders starter ends a token when the review approves it:
flow.transition(review, END, on=reviewed, when="payload.approved == true")
flow.transition(review, draft, on=reviewed, otherwise=True)
Now give the context of orders an int field amount. These two guards overlap:
flow.transition(review, END, on=reviewed, when="ctx['amount'] > 100")
flow.transition(review, draft, on=reviewed, when="ctx['amount'] > 50")
stateous compile refuses them, and it gives an amount that makes both of them true:
error: workflow 'orders' is not valid:
- 'reviewed' from 'review': the guards "ctx['amount'] > 100" and "ctx['amount'] > 50" can both be true, e.g. when ctx['amount'] = 101; make them exclusive, or make one the fallback (otherwise=True)
Change the second guard to ctx['amount'] <= 100, or make it the fallback.
Guarantees¶
- An event takes at most one transition. Compile proves that the guards on one state and event never overlap.
- A guard has no side effects. It reads data, and it changes nothing.
- A guard reads only fields that its models have. Compile checks each
ctx['key']against the context model, and eachpayload.fieldagainst the payload model. count('event')must name an event of the workflow, ortimeout,errororjoined.- Not guaranteed: that an event finds a transition. With no
otherwise=True, an event that no guard matches is refused.
See also¶
- The model: the words transition and guard.
- Timers and timeouts: guards on the events
timeoutanderror. - Write a workflow: compile errors for guards that overlap.