Everything on a board — a daily todo, a boss fight, a revenue tick, a sabotage — is an instance of one model. Learn it once and every space type makes sense.
When → Do → Then
The inspector shows a space as a single progressive-disclosure pane:
When this happens: [ on_land · always ] ← trigger + conditions
Then do: [ + add action ] ← actions
Outcome: [ + reward ] [ + penalty ] ← rewards / penalties / branchesUnder the hood that's five orthogonal parts. Most spaces only use two or three.
1. Triggers — when
When the space fires. on_land (the piece lands here), on_pass (passes
through), on_tick (a daily scheduler tick — for passive income / habits),
on_condition, on_enter_neighbor, on_schedule.
2. Conditions — gate
Boolean gates checked before the space runs. Time window, streak length, a badge
owned, a geofence, visit history, a
workflow result, or a fully general
inline expr. All must hold (an empty list is
always true).
3. Actions — do
The ordered things that happen: award XP / currency / a badge, add a todo, run a Kaizen workflow, notify, move the piece, branch, or a cross-player effect.
4. Rewards — then
Payouts — but richer than a flat number. A reward has a base, an optional variable bonus, a probability, and a reveal delay:
base: 10
variable: {{ random(1, 5) + session.streak }}
probability: 0.7 # 70% chance — variable rewards beat fixed ones
delayMs: 400 # the anticipation is part of the dopamineReward kinds: xp, currency, badge, unlock, free_move, extra_turn,
streak_freeze.
5. Penalties — or else
Setbacks: lose_turn, move_back, send_to_start, deduct_xp /
deduct_currency, an add_punishment_todo (a catch-up task), or
deteriorate_property.
Data is the sixth field — the type-specific inputs a space needs
(todoId, googlePlaceId, amount, thresholds…). Each space type documents
its own data.* keys in the Space Type Reference.
Why it's split this way
The five-way split is the schema, not the layout you see. It means a "Boss Room" and a "Daily Todo" are the same machine with different settings — and that the AI generator, import/export, and every preset compose from the same parts with zero bespoke code. When the structured fields can't express something, drop to an inline expression or a workflow.
One worked example
A challenge space — a demanding todo with a streak-scaled, slightly-delayed reward, available only in the evening:
Trigger: on_land
Condition: {{ time.hour >= 18 && time.hour < 22 }}
Action: (the todo links via data.todoId; completing it resolves the space)
Reward: base 30, variable {{ session.streak * 2 }}, delay 400ms