# Working with Lists and Starter Packs

## Introduction

Bluesky lists and starter packs are two features for organizing and sharing groups of users (and content). A list (lexicon type `app.bsky.graph.list`) is a curated set of user accounts (actors) – similar to Twitter lists – that can be used for curation or moderation purposes. A starter pack (lexicon type `app.bsky.graph.starterpack`) is a shareable collection of accounts (and optionally algorithmic feeds) intended to help onboard new users by suggesting who to follow and which feeds to try. In practice, a starter pack is backed by a list of accounts and up to a few feeds.

In this vignette, we demonstrate how to use `bskyr` to programmatically interact with Bluesky lists and starter packs: fetching existing lists/packs, creating and managing lists, and using lists as the basis for starter packs.

Authentication: Before proceeding, ensure you are authenticated with Bluesky. Assuming you have already configured your Bluesky credentials (via `set_bluesky_user()` and `set_bluesky_pass()` or environment variables), you can authenticate once at the start:

    library(bskyr)
    auth <- bs_auth(user = bs_get_user(), pass = bs_get_pass())

## Fetching and Exploring Lists

To retrieve the lists belonging to a specific user (actor), use `bs_get_actor_lists()`. This function returns a tibble of the actor’s list records. Each list entry includes fields like its `uri` (the unique AT URI for the list record), `name`, `purpose`, and any description:

    # Fetch all lists created by a given user (actor)
    user_lists <- bs_get_actor_lists('bskyr.bsky.social')
    user_lists
    #> # A tibble: 47 × 19
    #>    uri    cid   name  purpose list_itemcount indexed_at viewer_muted creator_did
    #>    <chr>  <chr> <chr> <chr>   <chr>          <chr>      <chr>        <chr>
    #>  1 at://… bafy… test… app.bs… 0              2025-10-2… FALSE        did:plc:5c…
    #>  2 at://… bafy… bsky… app.bs… 0              2025-10-2… FALSE        did:plc:5c…
    #>  3 at://… bafy… bsky… app.bs… 0              2025-09-0… FALSE        did:plc:5c…
    #>  4 at://… bafy… bsky… app.bs… 0              2025-09-0… FALSE        did:plc:5c…
    #>  5 at://… bafy… test… app.bs… 0              2025-07-2… FALSE        did:plc:5c…
    #>  6 at://… bafy… bsky… app.bs… 0              2025-07-2… FALSE        did:plc:5c…
    #>  7 at://… bafy… test… app.bs… 0              2025-07-2… FALSE        did:plc:5c…
    #>  8 at://… bafy… bsky… app.bs… 0              2025-07-2… FALSE        did:plc:5c…
    #>  9 at://… bafy… test… app.bs… 0              2025-07-2… FALSE        did:plc:5c…
    #> 10 at://… bafy… bsky… app.bs… 0              2025-07-2… FALSE        did:plc:5c…
    #> # ℹ 37 more rows
    #> # ℹ 11 more variables: creator_handle <chr>, creator_display_name <chr>,
    #> #   creator_avatar <chr>,
    #> #   creator_associated_activity_subscription.allowsubscriptions <chr>,
    #> #   creator_viewer_muted <chr>, creator_viewer_blocked_by <chr>,
    #> #   creator_created_at <chr>, creator_description <chr>,
    #> #   creator_indexed_at <chr>, avatar <chr>, description <chr>

You can inspect the details of a particular list using its URI. For example, to get a view of the first list from the above results:

    first_list_uri <- user_lists$uri[1] # AT URI of the first list
    first_list <- bs_get_list(first_list_uri) # retrieve detailed list info
    first_list
    #> # A tibble: 0 × 8
    #> # ℹ 8 variables: list_uri <chr>, list_cid <chr>, list_name <chr>,
    #> #   list_purpose <chr>, list_list_itemcount <int>, list_indexed_at <chr>,
    #> #   list_viewer_muted <lgl>, list_creator <list>

The `bs_get_list()` function returns the list’s metadata and its members. If `clean = TRUE`, it will be formatted as a tibble for convenience. In addition, Bluesky provides mechanisms to mute or block entire lists. You can retrieve lists that the current user has muted or blocked via:

    bs_get_muted_lists()
    #> # A tibble: 0 × 0
    bs_get_blocked_lists()
    #> # A tibble: 0 × 0

These return `tibble` of list records that you have muted or blocked, which is useful for moderation or content filtering.

## Creating and Managing Lists

To create a new list, use `bs_new_list()`. At minimum you must provide a `name` and a `purpose`. The `purpose` should be one of `'curatelist'`, `'modlist'`, or `'referencelist'`, distinguishing a normal curated list from a moderation list, etc. For typical user-curated lists, use `'curatelist'`. You may also include an optional `description` (and even an `avatar` image file if desired).

Here we’ll make a list on redistricting experts. My day job is redistricting research, so I’ll add some of my colleagues to the list.

    # Create a new curated list
    new_list <- bs_new_list(
      name = '[vignette] Redistricting Experts',
      purpose = 'curatelist',
      description = 'A list of interesting people in redistricting'
    )

After creating a list, you can add members to it using `bs_new_list_item()`. Each call adds a single account, identified by handle or DID, to the specified list. You must provide the `subject` (the account to add) and the list’s `uri`:

    # Add members to the list
    bs_new_list_item(
      subject = 'chriskenny.bsky.social',
      uri = new_list$uri
    )
    #> # A tibble: 1 × 5
    #>   uri                              cid   commit_cid commit_rev validation_status
    #>   <chr>                            <chr> <chr>      <chr>      <chr>
    #> 1 at://did:plc:5c2r73erhng4bszmxl… bafy… bafyreicp… 3m3xsqomw… valid
    bs_new_list_item(
      subject = 'simko.bsky.social',
      uri = new_list$uri
    )
    #> # A tibble: 1 × 5
    #>   uri                              cid   commit_cid commit_rev validation_status
    #>   <chr>                            <chr> <chr>      <chr>      <chr>
    #> 1 at://did:plc:5c2r73erhng4bszmxl… bafy… bafyreiel… 3m3xsqorq… valid
    bs_new_list_item(
      subject = 'corymccartan.com',
      uri = new_list$uri
    )
    #> # A tibble: 1 × 5
    #>   uri                              cid   commit_cid commit_rev validation_status
    #>   <chr>                            <chr> <chr>      <chr>      <chr>
    #> 1 at://did:plc:5c2r73erhng4bszmxl… bafy… bafyreigs… 3m3xsqovz… valid

If needed, you can remove a member from a list with `bs_delete_list_item()`. This function requires the record key (`rkey`) of the list item (the membership record). Typically, you can extract that from the `uri` of the item returned when it was added.

Here, we’ll add a new member to the list and then remove it. I’ll use this testing account (`bskyr.bsky.social`) as an example:

    fourth_item <- bs_new_list_item(
      subject = 'bskyr.bsky.social', uri = new_list$uri
    )
    item_rkey <- bs_extract_record_key(fourth_item$uri)
    bs_delete_list_item(item_rkey)
    #> [1] 200

Finally, to delete an entire list, use `bs_delete_list()` with the list’s record key. As with items, you can obtain the list’s record key from its `uri`:

    list_rkey <- bs_extract_record_key(new_list$uri)
    bs_delete_list(list_rkey)
    #> [1] 200

After deletion, the list and its memberships are removed from your Bluesky account.

## Understanding Starter Packs

Starter packs are curated bundles of recommendations. A starter pack typically includes a set of accounts, often centered around a theme or community, and can also include references to up to three custom feeds. Users can use a starter pack to quickly follow a group of people and subscribe to feeds with one click during onboarding. Under the hood, a starter pack record points to a list of accounts and an optional set of feed URIs. In other words, creating a starter pack is essentially packaging an existing list (of actors) and some feeds into a shareable recommendation set.

## Fetching Starter Packs

To retrieve starter packs created by a specific user, use `bs_get_actor_starter_packs()`. This is analogous to fetching lists, but for packs. It returns a `tibble` of the user’s starter pack records:

    user_packs <- bs_get_actor_starter_packs('chriskenny.bsky.social')
    user_packs |>
      dplyr::select(record_name, record_description)
    #> # A tibble: 1 × 2
    #>   record_name              record_description
    #>   <chr>                    <chr>
    #> 1 Political Methodologists Making a differences-in-differences in the world

If you have the AT URI of a particular starter pack, you can fetch its details with `bs_get_starter_pack()`. This will return the pack’s metadata along with the contents (the list of accounts and any feeds it includes):

    bs_get_starter_pack('https://bsky.app/starter-pack/jkertzer.bsky.social/3laywns2q2v27') |>
      dplyr::select(record_name, record_description)
    #> # A tibble: 1 × 2
    #>   record_name        record_description
    #>   <chr>              <chr>
    #> 1 Harvard Government The CGIS Cafe may be closed, but Bluesky is open

For convenience, `bskyr` also provides `bs_get_starter_packs()`, which can fetch multiple packs in one call. You can supply a vector of starter pack URIs to `bs_get_starter_packs()` to retrieve them all at once:

    bs_get_starter_packs(c(
      'at://did:plc:bmc56x6ksb7o7sdkq2fgm7se/app.bsky.graph.starterpack/3laywns2q2v27',
      'https://bsky.app/starter-pack/chriskenny.bsky.social/3lb3g5veo2z2r'
    )) |>
      dplyr::select(record_name, record_description)
    #> # A tibble: 2 × 2
    #>   record_name              record_description
    #>   <chr>                    <chr>
    #> 1 Harvard Government       The CGIS Cafe may be closed, but Bluesky is open
    #> 2 Political Methodologists Making a differences-in-differences in the world

## Creating and Managing Starter Packs

Creating a new starter pack is done with `bs_new_starter_pack()`. The required arguments are a display `name` for the pack and a `list` to base the pack on. The `list` should be the AT URI of a Bluesky list record containing the accounts you want in the starter pack. If you don’t already have a list prepared, you can omit the `list` argument – in that case, `bs_new_starter_pack()` will create a new (empty) list for you behind the scenes. You would then need to add accounts to it with `bs_new_list_item()`. You can also provide an optional `description` for the pack, as well as a `feeds` vector of up to 3 feed generator URIs to include.

Here’s an example:

    pack <- bs_new_starter_pack(
      name = '[vignette] Redistricting people',
      list = new_list$uri, # use an existing list of accounts
      description = 'A starter pack of interesting redistricters'
    )

After running the above, `pack` will contain the newly created starter pack record (including its own `uri`). The pack references the accounts in `new_list` and the specified feeds. If you omitted the `list` parameter, remember to add accounts to the pack’s list (accessible via `pack$list` URI) using `bs_new_list_item()` calls.

If a starter pack is no longer needed, you can delete it with `bs_delete_starter_pack()`. As with lists, you must supply the record key of the pack. For example:

    # Delete the starter pack by its record key
    pack_rkey <- bs_extract_record_key(pack$uri)
    bs_delete_starter_pack(pack_rkey)
    #> [1] 200

Deleting the pack removes the starter pack record, nbut does not delete the underlying list or its members. You may delete the list separately if desired.

## Conclusion

In this vignette, we covered how to work with Bluesky lists and starter packs using `bskyr`. We saw how to fetch and view existing lists, create new lists and manage their membership, and how lists serve as the basis for starter packs. We also demonstrated creating and deleting starter packs, including how to incorporate custom feeds. Together, these tools enable you to programmatically organize accounts into lists and craft shareable starter packs for recommending content to others.

DISCLAIMER: This vignette has been written with help from ChatGPT 4o. It has been reviewed for correctness and edited for clarity by the package author. Please note any issues at https://github.com/christopherkenny/bskyr/issues.