Skip to main content

Patterns

In the portal: Journeys.

What this is​

Twelve named shapes. Between them they cover almost everything anybody builds, and every one of them is made of the same three things: a node, a rule on the way out, and something the node hands over.

Each pattern gives you the structure, when to reach for it, the settings that matter, and the one thing that goes wrong with it.

They combine. Most real programmes are four or five of these joined end to end.


1. Deliver and wait​

The spine of every journey. Everything else is a variation on it.

Structure. One node, one action that delivers a sequence, one arrow out with the condition "that sequence has been submitted".

When to use it. Any step where the participant has to do something before the next thing makes sense.

Configuration.

{ "id": "welcome", "type": "start",
"actions": [ { "type": "createUserSequence", "sequenceId": "@sequences/WELCOME",
"autoOpen": true, "persistent": false } ],
"transitions": [ { "toNodeId": "sleep-check",
"condition": { "field": "newResponse.WELCOME.at", "op": "exists" } } ] }

.at exists is the standard way to say "they finished it". It works whether or not the sequence is scored.

The trap. persistent: false makes the item leave the home screen once it is submitted, which is right for a step in a flow. Leave it at true and the participant collects a growing pile of ticked cards they have finished with.


Structure. The first node after start delivers a sequence that opens immediately and cannot be closed.

When to use it. Consent, a mandatory safety notice, or the handful of details the rest of the programme needs before it can do anything sensible.

Configuration.

{ "type": "createUserSequence", "sequenceId": "@sequences/CONSENT",
"autoOpen": true, "closable": false, "persistent": true, "revisable": false }

closable: false is the blocking screen. There is no back gesture and no way past it except answering.

The trap. A blocking screen has to be answerable offline, and it must not ask for anything somebody could reasonably refuse to give. If a person can legitimately say no, give the screen a "no" answer that leads somewhere.


3. Score and branch​

Structure. One node delivers a scored questionnaire. Two or more arrows out, ordered, testing the score or its band, with a catch-all last.

When to use it. Deciding who needs more help. Sorting people into a supported track and a standard track is the most common branching decision in health content.

Configuration.

"transitions": [
{ "toNodeId": "supported", "priority": 0,
"condition": { "field": "newResponse.SLEEP_CHECK.score", "op": "at_least", "value": 10 } },
{ "toNodeId": "standard", "priority": 1,
"condition": { "field": "newResponse.SLEEP_CHECK.at", "op": "exists" } }
]

Bands work the same way, with "field": "...band", "op": "is", "value": "moderate", and read better when the instrument already has named severity levels.

The trap. Write the specific arrow above the general one. An arrow with no condition, or an .at exists catch-all, placed first swallows every branch below it.


4. Choice branch​

Structure. A card with a single choice question. Arrows testing which option was chosen.

When to use it. Anywhere the participant should decide the pace or the direction: "I am ready to start" against "tell me more first".

Configuration.

{ "field": "newResponse.START_CHOICE.values.READY_NOW", "op": "is", "value": "yes" }

The path is the sequence, then values, then the atom.

The trap. Always offer the gentler option, and make sure it leads somewhere real. A choice where one branch is the programme and the other is a dead end is not a choice, and participants notice.


5. Timed step​

Structure. A node the participant sits on for a set period, with an arrow that opens when enough time has passed.

When to use it. Weekly pacing. Letting something settle before asking about it. A gap between an intervention and the check that measures it.

Configuration.

"transitions": [
{ "toNodeId": "week-two", "priority": 0,
"condition": { "field": "secondsInNode", "op": "at_least", "value": 604800 } }
]

Seconds, always. A week is 604800. The minimum is two seconds.

The trap. Waits are written in seconds, so read the number back before you publish: 604800 is a week, and dropping one digit makes it under a day. Where a node's only way out is a time gate, give it a second arrow as well, or put an unconditional one downstream.


6. Drip content over days​

Structure. One node that hands out several deliveries at once, each with its own delayed start.

When to use it. A course where reading arrives on days one, three, seven and fourteen and nothing branches in between. It is far less machinery than a node per piece.

Configuration. Several createUserSequence actions on one node, each with its own bound:

{ "type": "createUserSequence", "sequenceId": "@sequences/DAY_THREE",
"surfaceId": "@surfaces/CARD",
"schedule": { "type": "once",
"start": { "type": "after", "after": 3, "period": "days" },
"end": { "type": "never" } } }

The trap. A delayed start is measured from the moment the delivery was created, not from the start of the journey. If the node handing them out sits four days into the programme, day three of the schedule is day seven of the participant's experience. Decide which clock you mean and put the deliveries on a node where those two agree.


7. Standing check-in on a schedule​

Structure. One node, one delivery, a recurring schedule. The journey does not loop at all.

When to use it. A daily habit, a weekly mood check, a morning symptom diary. Anything that recurs on a calendar rather than in response to something.

Configuration.

{ "type": "createUserSequence", "sequenceId": "@sequences/SLEEP_DIARY",
"surfaceId": "@surfaces/TODO", "persistent": true, "unscheduledSubmission": false,
"schedule": { "type": "weekly", "days": ["MO","TU","WE","TH","FR","SA","SU"],
"start": { "type": "instant" },
"end": { "type": "after", "after": 6, "period": "weeks" } } }

persistent: true leaves it on the screen with a tick for the rest of the day, so the participant can see they have done it. It returns tomorrow.

The trap. Do not build this as a loop in the graph. A node per day is forty-two nodes to edit, and it puts a participant who skips a day behind an arrow that needs that day's answer. One delivery with a schedule covers the whole six weeks.


8. Self-loop monitor​

Structure. A node that delivers something and has an arrow pointing back at itself, plus a higher priority arrow that leaves once a condition is met.

When to use it. When each round has to be handed out fresh in response to the last one, rather than on a calendar. A weekly review where the next review is only created once the previous one is done.

Configuration.

"transitions": [
{ "toNodeId": "finished", "priority": 0,
"condition": { "field": "tags", "op": "contains", "value": "COURSE_COMPLETE" } },
{ "toNodeId": "review", "priority": 1,
"condition": { "field": "newResponse.WEEKLY_REVIEW.at", "op": "exists" } }
]

Coming back to the same node re-runs its actions, so a new delivery is created each round. The submission is spent by the arrow that used it, so one answer produces exactly one lap.

The trap. Write the exit arrow first and give it a real condition. A loop with no way out is a journey that never completes, and completion is what a programme owner reports on.


9. Branch on something that did not happen​

Structure. A waiting node with two arrows: one for "they did it", one for "enough time has gone by and they still have not".

When to use it. Re-engagement. A nudge after three quiet days. Escalating when a measurement has not been taken.

Configuration.

"transitions": [
{ "toNodeId": "carry-on", "priority": 0,
"condition": { "field": "latestAtomResponse.WALK_LOGGED", "op": "exists" } },
{ "toNodeId": "nudge", "priority": 1,
"condition": { "all": [
{ "field": "latestAtomResponse.WALK_LOGGED", "op": "not_exists" },
{ "field": "secondsInNode", "op": "at_least", "value": 259200 } ] } }
]

Absence is a fact you can read at any moment: the answer is either on file or it is not, and the clock resets when the participant arrives at the node. Those two together are what "they have not done it in three days" is made of. The full explanation is on Conditions.

The trap. not_exists means never, not "not lately". For "they used to and have stopped", compare latestAtomResponse.<atom>.at against a time instead, because that field is a number.


10. Tag as memory​

Structure. A node adds a tag on the way through. Later nodes, later journeys and individual cards read it back.

When to use it. Any fact the programme needs to remember. The track somebody is on. Their consent. A coach already alerted. The pace they chose.

Configuration.

{ "type": "createUserTag", "tagId": "@tags/PACE_GENTLE" }
{ "field": "tags", "op": "contains", "value": "PACE_GENTLE" }

The same tag can gate an arrow, gate a single action, or decide whether one card on one page is shown. That last one is what lets a single piece of content say different things to different people, instead of maintaining three near-identical copies.

The trap. Do not set a tag and read it in the same breath. The write is optimistic on the phone and confirmed on the server, so a card handed out by the same node that added the tag can render before the tag is there. Put a step or a wait in between.


11. Escalate with nothing on their screen​

Structure. A node with no delivery at all. Two or three actions that raise a task, tag the participant, and tell somebody.

When to use it. When the right response is a human being, not more content. A high screening result, a run of poor sleep, a safety answer.

Configuration.

{ "id": "escalate", "type": "standard",
"actions": [
{ "type": "createUserTag", "tagId": "@tags/SUPPORTED" },
{ "type": "createTask", "taskTypeId": "@taskTypes/FOLLOW_UP",
"name": "Screening result in the supported range", "priority": 2 },
{ "type": "sendChatMessage",
"message": "I have asked one of our coaches to get in touch this week." } ],
"transitions": [ { "toNodeId": "carry-on" } ] }

The arrow out has no condition, so the participant passes straight through and carries on with the programme.

The trap. A coach should be able to tell from the task why it was raised, without opening the participant's record. In the portal builder you can only choose a task type, so write that reason into the type's default name and description, and make one task type for each reason. In an import file you can also set name and description on the action itself.


12. Ask in the chat and branch on the reply​

Structure. A node sends a chat message with a question attached. The arrow out reads the reply.

When to use it. A single question that does not deserve a card. Checking in. Asking how something went. It is by far the lightest thing you can ask somebody to do.

Configuration.

{ "type": "sendChatMessage", "atomId": "@atoms/HOW_WAS_THIS_WEEK",
"message": "How did this week go?" }
{ "field": "newAtomResponse.HOW_WAS_THIS_WEEK.at", "op": "exists" }

Attaching an atom makes the message answerable in place. The answer is a real, recorded response, so it shows on Progress and can be branched on like any other.

The trap. Write both halves in an import file: the chat message with its atom, and the arrow that reads the reply. The atom answer fields are covered on Conditions.


Putting them together​

Four combinations that come up over and over.

Screen, then sort, then run. Pattern 2 to take consent, pattern 3 to score, and two copies of pattern 7 delivering a different standing check-in on each branch.

Course with a weekly rhythm. Pattern 1 for each week's reading, pattern 5 between them, and one pattern 7 check-in that runs across the whole thing on its own schedule.

Quiet monitoring. Pattern 7 to gather, pattern 11 to escalate, and nothing else. The participant sees a daily check-in and never sees the machinery that watches it.

Chained programmes. End a journey with a createUserJourney action pointing at the next one. A six month programme is easier to build, review and change as four journeys of six weeks than as one journey of ninety nodes. Somebody already running the target journey is left exactly where they are, so a chain never resets anybody's progress.

The rules that apply to all twelve​

Every branching node needs a way out that can always fire. An unconditional arrow or an .at exists catch-all, written last, is what keeps everybody moving.

Arrows read the answer in hand, actions read the answer on file. An action gated on newResponse at the far end of an arrow that tested newResponse does not run, because the submission is spent by then. Use latestResponse in an action.

Recurrence belongs to the schedule, not to the graph. If you find yourself drawing one node per day, stop and put a schedule on one delivery instead.

The journey moves when the app runs. Every wait, every branch and every delivery matures the next time the participant opens their phone.

What you can now do​

Every one of the twelve is built from the same four parts: a node, an action, a condition and a schedule. Nothing here needed a feature of its own, and nothing here needed anybody to write code.

So when somebody describes a programme they want, the useful question is which of these twelve it is a version of. It is almost always one of them, or two of them in a row.