Build perfectly balanced playlists by combining multiple goals
The Multi-Objective Sequencer (MOS) is an ordering component that builds a playlist track by track, choosing the best next track at each step based on multiple goals you define.
Most ordering components optimize for a single thing — sort by popularity, sort by tempo, shuffle randomly. The MOS lets you combine many goals at once. For example: "I want high-energy tracks, spread out the artists, keep the tempo between 110–140 BPM, and gradually increase valence." No single sorter can do all of that. The MOS can.
You feed it a pool of candidate tracks (from any source components), configure one or more objectives, and set a limit for how many tracks to output. The MOS then assembles the best playlist it can from the available pool.
On the graph canvas, the MOS node displays a human-readable summary of all active objectives directly in the node body, with attribute names in bold. Non-default weights and slot restrictions are shown alongside each objective. This lets you see at a glance what the MOS is doing without opening the editor. If more than six objectives are configured, the display is truncated — double-click the node to see and edit the full list.
Objectives are the building blocks of the MOS. Each objective defines a single goal — like "prefer high-energy tracks" or "space out artists." You add one or more objectives to the MOS and assign each a weight that controls how important it is. The MOS then balances all your objectives together when choosing each track.
Every objective operates on a track attribute — a property of a track like popularity, tempo, artist, or energy. You pick which attribute the objective should care about, and the objective uses it to score candidates.
There are 14 objectives, and they fall into a few natural categories:
These score tracks based on a single attribute value, without considering what's already in the playlist:
These are context-aware — they look at what's already been placed in the playlist and score candidates based on how well they fit next. This is what makes the MOS fundamentally different from a simple sort:
A typical MOS configuration combines objectives from both categories. For example, a party playlist might use In Range to keep energy high, Maximize to prefer danceable tracks, and Match Max Run to allow short runs of the same artist but force a cooldown after. See the full objectives reference below for details on each one.
Every objective has a weight that controls how much influence it has on track selection. Weights range from "off" (ignored) to "unbreakable" (hard constraint).
| Weight | Value | Behavior |
|---|---|---|
off | 0 | Objective is completely ignored |
lowest | 0.01 | Barely considered — only breaks ties |
low | 0.25 | Minor preference |
medium | 0.5 | Default — balanced importance |
high | 0.75 | Strong preference |
highest | 1.0 | Dominant — will usually win over lower-weighted objectives |
unbreakable | ∞ | Hard constraint — tracks that violate this are excluded entirely |
For each candidate track, the MOS calculates:
total score = (score1 × weight1) + (score2 × weight2) + …
The track with the lowest total score wins. Since each objective returns a score between 0 (best) and 1 (worst), a perfectly fitting track scores near 0.
Unbreakable objectives work differently from weighted ones. Instead of contributing to the score, they act as hard filters: any track with a non-zero score on an unbreakable objective is disqualified from that position entirely. If no candidates pass all unbreakable constraints, the MOS stops building the playlist — it won't compromise on unbreakable rules.
highest instead for a very strong preference.
mo_score attributeEvery track output by the MOS gets an mo_score attribute stamped on it (0 = perfect fit, higher = more compromise). You can use this in downstream components — for example, to filter out tracks above a score threshold, or to inspect how well the sequencer achieved its goals.
By default, every objective applies to every position in the output playlist. The slots parameter lets you restrict an objective to specific positions. This opens up powerful sequencing patterns — like shaping the energy arc of a playlist, anchoring the opening track, or applying different rules to the beginning and end.
The slots field accepts a flexible format. Positions are 1-indexed (position 1 is the first track). You can combine any of these in a single field:
| Format | Example | Meaning |
|---|---|---|
| Single number | 1 | Only position 1 |
| Range | 1-5 | Positions 1 through 5 |
| Comma-separated | 1,3,5 | Positions 1, 3, and 5 |
| Space-separated | 1 3 5 | Same as above |
| Mixed | 1-3, 7, 10-12 | Positions 1, 2, 3, 7, 10, 11, 12 |
| Section | 1/4 | First quarter of the playlist |
| Section (mixed) | 1/2, 45-50 | First half plus positions 45–50 |
The section format n/N divides the playlist into N equal sections and selects section n. Sections are resolved against the playlist limit parameter. For a 32-track playlist:
1/8 → positions 1–43/8 → positions 9–121/2 → positions 1–16Sections distribute positions as evenly as possible. When the limit doesn't divide evenly, the last section absorbs the remainder. For example, with a limit of 10 and 3/3, section 3 gets positions 7–10 (four tracks instead of three).
You can freely mix section syntax with ranges and single positions: 1/4, 30, 40-45.
Leave the slots field empty (the default) to apply the objective to all positions.
Context-aware objectives (order-by, variety, min-separation, max-match, match-max-run, range-max-run) track what's already been placed in the playlist. When an objective has a slot restriction, it only sees tracks from positions within its own slot range as context. This means two objectives of the same type with different slot ranges operate independently.
For example, if you have "order-by energy ascending" for slots 1–25 and "order-by energy descending" for slots 26–50, the descending objective starts fresh at position 26 — it doesn't see the ascending run as context. This is what allows you to create energy arcs, V-shapes, and other non-monotonic patterns.
The MOS uses a greedy selection algorithm. It builds the playlist one track at a time:
Because scoring happens at each step, objectives that depend on what's already in the playlist (like variety, min-separation, and order-by) can make context-aware decisions. The MOS doesn't just sort — it sequences.
Detailed parameters and scoring behavior for each of the 14 objectives. The colored tags indicate what attribute types the objective works with.
Prefer tracks with higher values for the chosen attribute.
| Parameter | Default | Description |
|---|---|---|
field | popularity | The numeric attribute to maximize |
weight | medium | Importance (cannot be unbreakable) |
Scoring is relative to the actual pool: the highest-value track in the pool scores 0, the lowest scores 1. This means it adapts to whatever tracks you feed in.
Use when: you want the most popular, most energetic, or loudest tracks to be preferred.
Prefer tracks with lower values for the chosen attribute.
| Parameter | Default | Description |
|---|---|---|
field | popularity | The numeric attribute to minimize |
weight | medium | Importance (cannot be unbreakable) |
The inverse of maximize: the lowest-value track scores 0, the highest scores 1.
Use when: you want deep cuts (low popularity), quiet tracks (low energy), or short songs (low duration).
Prefer tracks with attribute values inside a given range.
| Parameter | Default | Description |
|---|---|---|
field | popularity | The numeric attribute to check |
min_val | 0 | Minimum value (inclusive) |
max_val | 100 | Maximum value (inclusive) |
weight | medium | Importance (can be unbreakable) |
Tracks inside the range score 0 (perfect). Tracks outside score proportionally to how far away they are. This makes it a "soft" preference when weighted and a "hard" filter when unbreakable.
Use when: you need tracks within a BPM window (tempo 120–140), a popularity bracket, or a release year decade. Set to unbreakable to enforce a hard cutoff.
Prefer tracks closest to a specific target value.
| Parameter | Default | Description |
|---|---|---|
field | popularity | The numeric attribute to target |
target | 50 | The desired value |
weight | medium | Importance (cannot be unbreakable) |
Scores based on distance from the target: a track at exactly the target value scores 0, and the track furthest from the target scores 1.
Use when: you want tracks near a specific tempo (target 128 BPM), a sweet spot of energy (target 0.7), or medium popularity (target 50).
Prefer tracks that exactly match a given value.
| Parameter | Default | Description |
|---|---|---|
field | artist | The attribute to match on |
value | (empty) | The exact value to match |
weight | medium | Importance (can be unbreakable) |
Binary scoring: tracks that match score 0, everything else scores 1. Comparison is exact string equality.
Use when: you want to prefer (or require) tracks from a specific artist, on a specific album, from a specific country, or in a specific key.
Prefer tracks where the attribute contains a given value (substring or list membership).
| Parameter | Default | Description |
|---|---|---|
field | artist | The attribute to check |
value | (empty) | The value to look for |
weight | medium | Importance (can be unbreakable) |
For text attributes, checks if the value appears as a substring. For list attributes (like mb_genres), checks if the value is a member of the list. Matching tracks score 0, non-matching score 1.
Use when: you want to favor tracks that belong to a genre ("rock" in mb_genres), or whose artist name contains a keyword.
Limit how many tracks can share the same value for an attribute.
| Parameter | Default | Description |
|---|---|---|
field | artist | The attribute to limit |
max_count | 1 | Maximum tracks allowed with the same value |
weight | medium | Importance (can be unbreakable) |
Tracks score 0 if fewer than max_count tracks with the same value are already in the playlist; otherwise they score 1. This is context-aware — it checks what's already been selected.
Use when: you want at most 2 tracks per artist, no more than 3 tracks from the same album, or exactly 1 track per label. Set to unbreakable to enforce a hard cap.
Prefer tracks that continue in ascending (or descending) order.
| Parameter | Default | Description |
|---|---|---|
field | popularity | The attribute to order by |
inverse | false | If true, prefer descending instead of ascending |
spread | full | off = sequential, full = span the full range, segment = each slot segment spans the full range |
weight | medium | Importance (cannot be unbreakable) |
This is context-aware: it looks at the last track placed and prefers tracks that continue in the same direction. Tracks that continue the order score low; tracks that reverse it score high.
When spread is enabled, the objective uses rank-based selection to pick tracks that represent the full distribution of values. Instead of clustering at one end of the range (which happens when the pool is much larger than the playlist), it picks tracks at evenly-spaced rank positions through the sorted pool. This works well with any distribution shape — uniform, bimodal, or skewed.
When spread is set to segment, each contiguous slot segment independently spans the full value range. With full spread, a single order-by objective across multiple segments distributes its picks sequentially — the first segment gets low-ranked values and later segments get higher ones. With segment, every segment covers the full range on its own. This is useful when you want the same energy arc repeated in multiple sections of the playlist.
Use when: you want energy to build over the playlist, tempo to gradually increase, or popularity to wind down from hits to deep cuts. Enable spread when feeding in a large pool and you want the output to represent the full range, not just a narrow slice.
Ensure tracks sharing the same value are spaced apart in the playlist.
| Parameter | Default | Description |
|---|---|---|
field | artist | The attribute to enforce separation on |
separation | 5 | Minimum number of tracks between same values |
weight | medium | Importance (can be unbreakable) |
Checks the last N positions in the playlist (where N = separation). If the same value appeared recently, the score increases as the match gets closer. A match right next to the current position scores 1 (worst); a match exactly at the separation distance scores near 0.
Use when: you don't want the same artist back-to-back, or you want to space out tracks from the same genre, album, or era.
Maximize diversity of an attribute across the playlist.
| Parameter | Default | Description |
|---|---|---|
field | artist | The attribute to diversify |
inverse | false | If true, prefer similarity instead of variety |
weight | medium | Importance (can be unbreakable) |
Scoring adapts based on the attribute type:
Set inverse to true to flip the behavior — prefer similarity and clustering instead of variety.
Use when: you want maximum artist diversity, a wide spread of tempos, or genre variety across the playlist. Use inverse when you want genre clustering or to keep similar tempos together.
Limit consecutive runs of tracks that share a value, with a mandatory separation gap before that value can appear again.
| Parameter | Default | Description |
|---|---|---|
field | artist | The attribute to check |
value | (empty) | Specific value to match. Leave empty for "general match" — the rule applies to any repeated value. |
max_run | 3 | Maximum consecutive matching tracks allowed |
separation | 5 | Required non-matching tracks after a maxed-out run before matching tracks are allowed again |
weight | medium | Importance (can be unbreakable) |
This objective combines run-length limiting with a cooldown period. Once a run of matching tracks hits max_run, the objective penalizes further matches until separation non-matching tracks have been placed. During the cooldown, scoring is gradient — tracks closer to completing the separation period get a lower (better) penalty.
Use when: you want to prevent artist fatigue (general match on artist), limit consecutive tracks from the same decade, or control how often a specific genre or attribute value appears in runs. Compared to Min Separation, this objective allows short runs before enforcing the gap — Min Separation prevents any repeat within the window.
Limit consecutive runs of tracks whose attribute falls within (or outside) a numeric range, with a mandatory separation gap.
| Parameter | Default | Description |
|---|---|---|
field | popularity | The numeric attribute to check |
low | 0 | Lower bound of range (inclusive) |
high | 100 | Upper bound of range (inclusive) |
invert | false | If true, match values outside the range instead |
max_run | 3 | Maximum consecutive matching tracks allowed |
separation | 5 | Required non-matching tracks after a maxed-out run |
weight | medium | Importance (can be unbreakable) |
Works like Match Max Run but matches based on a numeric range instead of exact value. A track "matches" if its attribute value falls within [low, high] (or outside if invert is true). Once max_run consecutive matches occur, further matches are penalized until separation non-matching tracks have been placed.
Use when: you want to prevent too many high-energy tracks in a row ("max 3 tracks with energy > 0.8, then 2 calmer ones"), limit runs of obscure tracks, or alternate between tempo zones. Use invert to limit runs of tracks outside a range.
Score candidates by distance from the last selected track's attribute value.
| Parameter | Default | Description |
|---|---|---|
field | energy | The attribute to measure continuity on |
invert | false | If true, prefer tracks furthest from the last track |
weight | medium | Importance (can be unbreakable) |
Scoring adapts based on the attribute type:
Set invert to true to prefer tracks that are distant from the last track instead.
Use when: you want smooth transitions in energy, tempo, or mood between consecutive tracks. Use invert when you want maximum contrast between adjacent tracks.
Target a desired percentage distribution of attribute values in the output playlist.
| Parameter | Default | Description |
|---|---|---|
field | artist_gender | The attribute to control distribution for |
values | (empty) | Space-separated value(N%) pairs (see below) |
weight | medium | Importance (cannot be unbreakable) |
The values field uses a compact format: each entry is a value followed by a target percentage in parentheses, separated by spaces. Three match modes are auto-detected from the token shape:
female(30%) — matches tracks where the field equals "female" exactly1970-1979(20%) — matches tracks where the numeric field value is between 1970 and 1979 (inclusive)rock(20%) on a list field like mb_genres — matches tracks where "rock" is in the genre listPercentages don't need to sum to 100%. Unspecified values act as filler, giving other objectives more room. Percentages can exceed 100% total when buckets overlap or when using list fields where a single track can match multiple buckets.
At each step, the objective simulates adding each candidate to the playlist and measures how close the resulting percentages would be to the targets. The candidate that brings the actual distribution closest to the desired distribution scores 0 (best); the one that moves it furthest away scores 1 (worst).
female(30%) male(50%) — target 30% female and 50% male artists (20% can be anything)1970-1979(20%) 1980-1989(30%) — target decade distribution for release yearsrock(20%) jazz(20%) classical(20%) — balance genres in a multi-genre playlistUse when: you want to control the demographic or categorical makeup of a playlist — gender balance, genre proportions, era distribution, or country representation.
Here are some real-world scenarios showing how to combine objectives. Each example starts with a goal and lists the objectives you'd configure.
Goal: upbeat, danceable tracks with no artist repeats back-to-back.
highhighunbreakablemediumGoal: tracks that flow together with gradually increasing tempo and compatible keys.
highunbreakablemediumlowThe inverse variety on camelot_num clusters tracks with similar keys together, making transitions smoother.
Goal: lesser-known tracks from many different genres and eras.
highhighmediumunbreakableGoal: calm, instrumental tracks at a steady tempo for concentration.
unbreakablehighhighmediummediumGoal: no more than 2 tracks per artist, balanced across different labels.
unbreakablemediumlowlowestPopularity at lowest acts as a tiebreaker — when all else is equal, prefer the more popular track.
Goal: a playlist that's roughly 40% female artists, 40% male, with genre diversity and decent popularity.
female(40%) male(40%), weight: highrock(25%) pop(25%) electronic(25%), weight: mediumhighlowestThe 20% gender slack and 25% genre slack give the algorithm room to satisfy both distribution objectives. Popularity at lowest breaks ties.
Goal: a 50-track playlist that rises in energy for the first half and fades out in the second half.
medium, slots: 1/2medium, slots: 2/2unbreakableUsing section syntax (1/2 and 2/2) instead of hard-coded ranges means this works regardless of the playlist limit — change the limit from 50 to 100 and the split point adjusts automatically. The two order-by objectives operate independently — the descending half starts fresh from the highest available energy. The min-separation objective has no slots, so it applies globally.
Goal: start with a specific artist, end with a mellow track, fill the middle with variety.
unbreakable, slots: 1unbreakable, slots: 50highlowThe match and range objectives only fire at positions 1 and 50 respectively, so they don't constrain the middle of the playlist at all.
Goal: a playlist that starts with rock tracks and transitions to electronic in the second half.
highest, slots: 1/2highest, slots: 2/2highmediumThe contains objectives steer genre selection within their slot ranges. Using 1/2 and 2/2 keeps the split proportional to the playlist size. The global variety and order-by objectives apply throughout.
Goal: gradually increase tempo for the first 20 tracks, hold steady in the middle, then slow back down.
highest, slots: 1-20high, slots: 21-35highest, slots: 36-50mediumThree distinct phases: a ramp up, a plateau held within a 10 BPM window, and a ramp down. You could also use 1/3, 2/3, 3/3 for equal thirds that adapt to any limit. Each phase's context is independent, so the descending ramp starts from the highest available tempo — not from where the plateau left off.
Begin with one or two objectives and preview the result. Add more objectives only when you see specific problems to fix. Too many competing objectives can dilute each other, leaving you with a mediocre compromise.
Unbreakable constraints are powerful but rigid. Each unbreakable objective shrinks the candidate pool. Stack too many and the MOS will run out of eligible tracks and stop early, giving you a shorter playlist than your limit. Reserve unbreakable for things you truly won't compromise on (like "no duplicate artists").
If two objectives matter but one matters more, don't set both to high. Set the primary one to high and the secondary to medium or low. The weight ratio is what determines influence. Two objectives at the same weight will fight each other equally.
lowest for tiebreakersAn objective at lowest weight (0.01) barely affects scoring — but when multiple tracks tie on your main objectives, it decides the winner. Great for a default like "prefer popular tracks when all else is equal."
The MOS can only choose from what you give it. A pool of 500 tracks with a limit of 50 gives the algorithm room to be selective. A pool of 60 tracks with a limit of 50 leaves almost no room to optimize. For best results, give the MOS at least 3–5x more input tracks than your target limit.
Objectives like variety, min-separation, order-by, max-match, match-max-run, range-max-run, continuity, and distribution look at what's already in the playlist. These are what make the MOS fundamentally different from a filter+sort pipeline. Use them for sequencing goals that depend on track order and relationships.
mo_score to debugAfter running, preview the output and look at the mo_score attribute. Scores near 0 mean tracks fit your objectives well. If scores start climbing toward the end, it means the pool is running low on good candidates — consider loosening your constraints or providing a larger input pool.
The MOS works well as part of a larger program. Use filters upstream to narrow the pool (remove explicit tracks, limit to a genre), then let the MOS handle the sequencing.