# Build a Backbone set

UI follows design-system/design-system.md v1.2 only. Gaps go to the design owner, never guessed.

Backbone is a set-list metronome. The songs live in the link. There is no account, no backend, and no skill to install. You gather the songs, open a `/new` URL on this same host, and give that link to the user. A valid link redirects to the set, already in Run.

Use the host that served this file. On the public site that is `https://backbonemetronome.com`. On any other host, including a preview, stay on that host.

## What to gather

For each song, in set order:

- **Name.** The title the band should see. Up to 80 characters.
- **BPM.** Quarter-note tempo, from 30 to 300. One decimal is allowed (`72.5`). Do not guess.
- **Meter.** One of `2/4`, `3/4`, `4/4`, `5/4`, `6/8`, `7/8`, `12/8`. Do not guess.
- **Rhythm.** Optional. One of `1`, `1/2`, `1/2T`, `1/4`, `1/4T`, `1/8`, `1/8T`, `1/16`, `1/16T`. It has to divide the meter. `1` is one click for the whole bar.

Also gather a **set title** when you know it (a service date or a plan name, 80 characters or fewer). Pass it as `title`. Backbone's link stores the songs, not a separate set title, so the band sees the song list. A blank or oversized title is rejected so a bad plan name is not dropped quietly. Omit `title` only when you truly do not have one.

At most 24 songs. Every song gets a 1 bar count-in and accent off, the same as a new song in the app. `/new` does not take count-in or accent.

## Rhythm when you leave it off

Compare the tempo after it is rounded to one decimal, which is how Backbone stores it.

| Meter | BPM | Rhythm you pass | What the set uses |
| --- | --- | --- | --- |
| 4/4 | below 110 | omitted | `1/8` |
| 4/4 | 110 or higher | omitted | `1/4` |
| 3/4 | 100 or below | omitted | `1/8` |
| 3/4 | above 100 | omitted | `1/4` |
| anything else | any | omitted | the app default |

The app default is `1/4` when that click divides the bar, otherwise `1/8`, otherwise `1` (one click per bar). So a 6/8 song with no rhythm becomes `1/4` (three clicks in the bar). Pass `1/8` yourself when you want eighth notes. A 7/8 song with no rhythm becomes `1/8`, because `1/4` does not divide 7/8.

An explicit rhythm always wins, including when it differs from the default. `Gratitude|72|4/4|1/4` stays on quarter notes even though 72 in 4/4 would otherwise be eighths.

## Pipe form

Repeat `song` once per song. Order is the set order.

```
/new?title=Sunday%20Oct%2025&song=Gratitude|72|4/4&song=Firm%20Foundation|136|4/4
```

Each `song` value is `Name|BPM|Meter` or `Name|BPM|Meter|Rhythm`.

- `Gratitude` at 72 in 4/4 leaves rhythm off, so the set uses `1/8`.
- `Firm Foundation` at 136 in 4/4 leaves rhythm off, so the set uses `1/4`.

Spaces, `&`, `#`, and non-ASCII have to be encoded. This is the same set with a name that contains `&`, a name that contains `#`, and a name with `ñ`. The last song passes `1/4` on purpose (100 in 3/4 would otherwise be `1/8`):

```
/new?title=Sunday%20Oct%2025&song=King%20%26%20Country|74|4/4&song=Key%20of%20C%23|90|4/4&song=Se%C3%B1or|100|3/4|1/4
```

## JSON form

Use this when a song name contains `|`, or when you already have structured data. One `set` parameter. Do not also pass `song` or `title`. The JSON is `{ "title": "...", "songs": [{ "name", "bpm", "meter", "rhythm?" }] }`. `title` and `rhythm` are optional. `bpm` may be a number or a numeric string.

```
/new?set=%7B%22title%22%3A%22Sunday%20Oct%2025%22%2C%22songs%22%3A%5B%7B%22name%22%3A%22Gratitude%22%2C%22bpm%22%3A72%2C%22meter%22%3A%224%2F4%22%7D%2C%7B%22name%22%3A%22Firm%20Foundation%22%2C%22bpm%22%3A136%2C%22meter%22%3A%224%2F4%22%2C%22rhythm%22%3A%221%2F8%22%7D%5D%7D
```

That second song forces `1/8` even though 136 in 4/4 would otherwise be `1/4`.

A name that contains `|` has to use this form. After decoding, the pipe form splits on every `|`, so a pipe inside a name cannot be escaped there.

```
/new?set=%7B%22title%22%3A%22Sunday%20Oct%2025%22%2C%22songs%22%3A%5B%7B%22name%22%3A%22Great%7CIs%20He%22%2C%22bpm%22%3A80%2C%22meter%22%3A%224%2F4%22%7D%5D%7D
```

## URL encoding

Build the query with a URL encoder (`URLSearchParams`, `encodeURIComponent`, or your client's equivalent). Do not paste raw song names into the query.

Encode each value. Leave the `?`, the `&` between parameters, and the `=` alone.

| Character | Write it as | Why |
| --- | --- | --- |
| space | `%20` | A raw space breaks the URL. `+` is also read as a space; prefer `%20`. |
| `&` | `%26` | A raw `&` starts the next parameter. |
| `#` | `%23` | A raw `#` starts the fragment and drops the rest of the link. |
| `%` | `%25` | A raw `%` is the start of an escape. |
| `+` | `%2B` | A raw `+` is read as a space. |
| `?` | `%3F` | A raw `?` starts a query. |
| non-ASCII | UTF-8 percent-encoding | `ñ` is `%C3%B1`. `é` is `%C3%A9`. Encode the characters; do not replace them with ASCII. |
| `/` | `/` or `%2F` | Meters like `4/4` may stay readable. |
| `\|` | see below | Separator in the pipe form. |

In the pipe form the separator is a literal `|` (or `%7C`, which decodes to the same separator). Splitting happens after decoding, so `%7C` does not hide a pipe inside a name. If the name contains `|`, use the JSON form.

Encoding the whole `song` value with `encodeURIComponent` is valid. Pipes become `%7C` and slashes become `%2F`. Backbone decodes the value, then splits on `|`.

Encode the JSON form once: `encodeURIComponent(JSON.stringify(object))` as the `set` value. Do not encode it twice, and do not put raw `{`, `"`, or `&` from the JSON into the URL.

## What to give the user

Give them the absolute `/new` URL you built, or follow the redirect and give them the final link. Either one opens the set.

A valid link replaces itself with `/#v2.…` on the same host. That is the canonical set link. The page shows a spinner until the redirect; it does not flash the editor. Do not build the `v2.` payload yourself.

An invalid link stays on `/new` and lists what is wrong. Fix every line. Nothing was saved. Do not guess a tempo or a meter to get past an error.

## Planning Center Services

Skip this section when you do not have Planning Center access. Ask the user for each song's name, BPM, and meter instead of guessing.

When you do have access, through an MCP connection or the HTTP API, you can fill the set from a service plan. The HTTP API root is `https://api.planningcenteronline.com`. Use the credentials you already have. Do not ask the user for a password.

The same calls exist as Planning Center MCP tools. `services_service_types` lists service types. `services_plans` lists plans (`filter` `future`, `order` `sort_date`). `services_plan_items` lists items (`include` `song,arrangement`, `per_page` `100`). Page with `page_after` until `has_more` is false. The HTTP shapes below are the same data.

1. **Pick the service type.** Load service types (`GET /services/v2/service_types`, or `services_service_types`). Use the one the user named. If they did not name one and a single service type is clearly the main Sunday service, use that one. Ask only when the choice is genuinely ambiguous, such as two Sunday services or a service and a rehearsal that could both be the one they mean. Do not assume a rehearsal is the Sunday service.

2. **Pick the plan.** If the user names a date or a title, find that plan. `sort_date` is a full ISO timestamp, such as `2026-10-25T15:20:00Z`, not a bare date. Compare the date part only (`2026-10-25`), or match the plan's `dates` string. A plan title is often null; the date still identifies it. If they do not name a plan, use the next upcoming plan in that service type: today's plan when one is scheduled today, otherwise the soonest plan after today (`GET /services/v2/service_types/{service_type_id}/plans?filter=future&order=sort_date`, or `services_plans` with `filter` `future` and `order` `sort_date`, and take the first result).

3. **Load the items in order.** `GET /services/v2/service_types/{service_type_id}/plans/{plan_id}/items?include=song,arrangement&per_page=100`, or `services_plan_items` with `include` `song,arrangement` and `per_page` `100`. Follow pagination (`links.next`, the next offset, or `page_after` while `has_more` is true) until every item is loaded. Keep the order the API returns. Do not sort by title.

4. **Keep songs only.** Skip headers, media, announcements, and any item whose `item_type` is not a song. Compare `item_type` case-insensitively: `song` and `Song` are both songs. The set is the songs in the order they appear.

5. **Read the song and the selected arrangement.** For each song item, use the Song record's title (`attributes.title` on the song related to that item), not the plan item's title. Those titles can differ. An item titled `Here Is Love (Everlasting Praise)` can point at a song titled `Here Is Love Vast As The Ocean (Everlasting Praise)`; the set uses the Song title. Take `bpm` and `meter` from the included Arrangement that the item's arrangement relationship points at. That is the arrangement selected on the plan, not whichever arrangement is first on the song. Ignore item length; it is seconds, not tempo.

6. **Do not guess.** If the song title, the BPM, or the meter is missing, ask the user for that song. If the meter is present but is not one of `2/4`, `3/4`, `4/4`, `5/4`, `6/8`, `7/8`, or `12/8`, ask. Leave rhythm off unless the user asks for one, so the defaults above apply.

7. **Title the set** with the plan title when it has one, otherwise the plan date (`dates` or `sort_date`).

Then build the `/new` URL and give it to the user.
