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.
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 10 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 Minimum Separation to prevent the same artist from appearing back-to-back. 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 |
Leave the slots field empty (the default) to apply the objective to all positions.
Context-aware objectives (order-by, variety, min-separation, max-match) 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 10 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 | true | If true, select tracks spanning the full range of values |
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.
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.
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 50-track playlist that rises in energy for the first half and fades out in the second half.
medium, slots: 1-25medium, slots: 26-50unbreakableThe two order-by objectives operate independently — the descending half starts fresh from the highest available energy, not from where the ascending half left off. The min-separation objective has no slots, so it applies globally across the entire playlist.
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-15highest, slots: 16-30highmediumThe contains objectives steer genre selection within their slot ranges. The global variety and order-by objectives apply throughout, keeping artists spread out and energy building across both halves.
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. 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, and max-match 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.