Join patterns¶
Each pattern below shows a small workflow and what the engine does with it. The rules behind them are explained in Joins and splits; this page is the picture book.
Two rules hold in every pattern:
- A join counts every branch drawn into it, at every level: the branches of its split, and the branches of any split inside them on the way.
- A join never fires while a token of its pass is still on its way to it.
The three pairings¶
Parallel: every branch must arrive¶
flowchart LR
s{{"split: all"}} --> a["A"]
s --> b["B"]
s --> c["C"]
a --> j{{"join: wait_all"}}
b --> j
c --> j
j --> next["Next"]
The split sends work to A, B and C. The join fires once all three have
arrived. If one of them is canceled, or goes where the join cannot be
reached, the join stops on an incident: an operator fires it with what
arrived, or cancels the instance. The parallel gateway sets up this pair.
Inclusive: only the branches taken must arrive¶
flowchart LR
s{{"split: all"}} -->|"needs review"| a["Review"]
s -->|"needs payment"| b["Payment"]
s -->|"needs signature"| c["Signature"]
a --> j{{"join: matching"}}
b --> j
c --> j
j --> next["Next"]
The conditions sit on the split's flows. Say only the review and the payment
are needed: the split sends work to those two, and the join waits for those
two. A branch that is canceled or ends elsewhere is not waited for either.
The inclusive gateway sets up this pair.
Exclusive: one way only¶
flowchart LR
s{{"split: first"}} -->|"approved"| a["Approve"]
s -->|"otherwise"| b["Reject"]
a --> j{{"join: immediate"}}
b --> j
j --> next["Notify"]
The split takes the first flow whose condition holds, so only one token ever
reaches the join, and it goes straight on. The exclusive gateway sets up
this pair.
Firing early¶
Threshold: N of M¶
flowchart LR
s{{"split: all"}} --> a["Reviewer 1"]
s --> b["Reviewer 2"]
s --> c["Reviewer 3"]
a --> j{{"join: threshold, count 2"}}
b --> j
c --> j
j --> next["Next"]
The join fires as soon as two reviewers have answered, whatever they answered. The third reviewer's branch is then canceled, with its task (see When it fires). If every branch has arrived and there are still fewer than two, the join stops on an incident.
Quorum: decided either way¶
flowchart LR
s{{"split: all"}} --> a["Voter 1"]
s --> b["Voter 2"]
s --> c["Voter 3"]
a --> j{{"join: quorum, 2 'yes'"}}
b --> j
c --> j
j -->|"reached"| ok["Approve"]
j -->|"not reached"| ko["Reject"]
The join tests a condition on each arriving branch, here vote == yes. It
fires as soon as two branches vote yes (reached), or as soon as that can
no longer happen (not reached): after two "no", one vote left cannot make
two. It writes the result, and the outgoing flows route on it.
Timeout: give up on a slow branch¶
flowchart LR
s{{"split: all"}} --> a["Legal"]
s --> b["Finance"]
a --> j{{"join: matching, join timeout P7D"}}
b --> j
j --> next["Next"]
The join waits for both, but its node's join timeout fires it with what has arrived once seven days have passed since the first arrival. Any join takes one: a quorum that has not decided in a week settles as not reached. The timeout can remind first, or stop on an incident instead (see Timeouts at a join).
When it fires: the branches left behind¶
flowchart LR
s{{"split: all"}} --> a["A"]
s --> b["B"]
s --> r["Reminder loop"]
a --> j{{"join: threshold, count 1"}}
b --> j
r --> done["End"]
j --> next["Next"]
A fires the join. What becomes of B, still running, and of the reminder loop, which never goes to the join, depends on the join's When it fires setting:
| Setting | B (heading to the join) | The reminder loop (going elsewhere) |
|---|---|---|
| Cancel the branches heading here (default) | canceled | runs on |
| Cancel the split | canceled | canceled |
| Ignore | runs on, ignored if it reaches the join | runs on |
A branch that splits again¶
Two ways into the join¶
flowchart LR
s{{"split: all"}} --> a["A"]
s --> b["B"]
s --> c["C, split: all"]
a --> j{{"join"}}
b --> j
c --> j
c --> x["X"]
x --> j
j --> next["Next"]
C sends work both straight to the join and through X. The join counts four branches: A, B, C's branch straight in, and C's branch through X. It waits for X's token too, then fires once, with all of them.
wait_all: if C does not take its flow to X (a false condition), a path drawn into the join will not deliver. The join stops on an incident: "the flow from n_c to n_x was not taken".matching: it waits only for what C actually sent.- A threshold or a quorum counts C's two branches as two. A quorum waits while C has not split yet, since C could still bring two votes.
- A collect line takes a value from each of the four.
- A quorum by percentage or majority is refused when the workflow is saved: how many branches there are in all is only known once C has split. Bring C's branches together first (below), or count a fixed number.
Side work that leaves¶
flowchart LR
s{{"split: all"}} --> a["A"]
s --> c["C, split: all"]
a --> j{{"join: wait_all"}}
c --> j
c --> x["Archive"]
x --> e["End"]
j --> next["Next"]
The flow from C to the archive never leads to the join, so it is no path into it. The join fires once A and C's branch straight in have arrived. The archive runs on by itself.
Bringing a split's branches together first¶
flowchart LR
s{{"split: all"}} --> a["A"]
s --> c["C, split: all"]
c --> c1["C1"]
c --> c2["C2"]
c1 --> ij{{"inner join: wait_all"}}
c2 --> ij
a --> j{{"join: quorum, majority"}}
ij --> j
j --> next["Next"]
The inner join brings C1 and C2 together and sends one token on. The outer join counts it as one branch, C's. So a quorum by majority counts two branches here, A and C, and is accepted on save.
Loops¶
Back through the split: a new pass¶
flowchart LR
s{{"split: all"}} --> a["A"]
s --> b["B"]
a --> j{{"join: wait_all"}}
b --> j
j --> check{"Again?"}
check -->|"yes"| s
check -->|"no"| done["End"]
Each time a token goes through the split, it starts a new pass, with branches of its own. The join fires once per pass, with that pass's branches only. Branches of two passes never count together.
A loop inside a branch¶
flowchart LR
s{{"split: all"}} --> a["A"]
s --> c["Draft"]
c --> rv{"Approved?"}
rv -->|"no"| c
rv -->|"yes"| j{{"join: wait_all"}}
a --> j
j --> next["Next"]
The draft can go round many times before it is approved. It is still one branch: the join waits for it, and counts it once.
A step with a timer¶
flowchart LR
r["Review<br/>timer: alert after 2 days"] -->|"alert"| m["Alert manager"]
r -->|"answered"| d["Done"]
m --> j{{"join: wait_all"}}
d --> j
j --> next["Next"]
A step whose timer sends a branch out acts as the split. While the review waits, the join waits for both flows: the timer can still send the alert. Once the review is answered:
- if the alert went out, the join waits for it too;
- if the review was answered before the timer fired, no alert was sent, and the join does not wait for one;
- if the timer fired twice, both alerts are the same branch sent twice: the join waits for both tokens, and takes them together in one fire.
When wait_all stops on an incident¶
A wait_all join requires every path drawn into it. It stops on an
incident, with what happened, as soon as one will not deliver:
| What happened | Example |
|---|---|
| A split did not take its flow towards the join | the flow from n_s to n_c was not taken |
| A token went where the join cannot be reached | token 42 went from n_c to n_end, from where the join cannot be reached |
| A token was canceled on the way | token 42 was canceled at n_x |
An exclusive choice that takes one of its flows is fine as long as that flow
still leads to the join, and a step's timer that never fired is not a missing
path. When saving in the form, a wait_all join reached through a flow with a
condition, or through a choice that can lead away, gets a warning.
Refused on save¶
| Shape | Why | Way out |
|---|---|---|
| A split reaches the join, but some path to the join avoids it | the join cannot tell which branches to wait for | close the split's branches with a join of their own, then bring the paths together with an immediate join |
| A branch reaches the join both through an inner join and around it | the join cannot tell what will arrive | draw each path so it reaches the join one way |
| A threshold or quorum count above the branches that can reach the join | it could never fire | lower the count, or add branches |
| A quorum by percentage or majority after a branch that splits again | how many branches there are is only known once it has split | bring that branch's branches together first, or count a fixed number |