Understanding Bluesky Starter Packs at the Protocol Level
Bluesky starter packs appear to users as a single, shareable page designed to let new users follow a curated group of accounts with a single tap. However, at the AT Protocol level, these packs are composed of three distinct records, linked together by references. When creating these packs programmatically, particularly as part of an automated outreach pipeline, it’s crucial to understand how this decomposition impacts updatability and creation constraints. A naive scripting approach can quietly create a mess if these nuances are ignored.
The Three Core AT Protocol Records
The creation of starter packs relies on standard com.atproto.repo.createRecord and putRecord calls against your own Personal Data Server (PDS). There is no specialized API surface required for this functionality.
1. The List Record (app.bsky.graph.list)
The foundation of a starter pack is an ordinary Bluesky list. This list is defined with the purpose app.bsky.graph.defs#referencelist. The list record itself contains essential metadata, including its name, purpose, creation timestamp (createdAt), and optional fields for a description and an avatar. The actual members of the list are stored in a separate record type.
2. The Membership Records (app.bsky.graph.listitem)
These records define which accounts belong to a specific list. Each listitem record is associated with a particular list (via a reference to the list record) and a specific account (via a reference to the account's DID). A starter pack list can contain numerous listitem records, each linking a follower account to the main list. These records are typically created in bulk when a starter pack is assembled.
3. The Profile Record (app.bsky.actor.profile)
While not strictly part of the starter pack's *functionality* in terms of curating follows, a user's profile record is essential for discoverability and presentation. When a starter pack is created programmatically, it often involves updating the user's profile to include a link or reference to the starter pack list. This could be through a custom facet in the bio or a dedicated section if the Bluesky client evolves to support such features more explicitly. For now, it’s about ensuring the creator's profile points to the list.
The Non-Idempotency Trap: `createRecord` vs. `putRecord`
The critical nuance when programmatically creating starter packs lies in the distinction between createRecord and putRecord operations, and how they interact with the AT Protocol's data model. A createRecord operation is inherently non-idempotent. This means if you attempt to create the exact same record twice, the second attempt will fail because the record identifier (a combination of the collection, repository, and record key) already exists.
Starter pack lists and their associated membership items are often created using createRecord, especially when they are first generated. If a script is designed to simply iterate and create records without checking for existing ones, it will fail on subsequent runs if the pack already exists. This is particularly problematic for automated pipelines that might re-run to update or verify pack integrity.
Conversely, putRecord is idempotent. It will create a record if it doesn't exist or update it if it does. However, using putRecord for the initial creation of a list or membership record might not be ideal if you need strict control over the creation process or want to ensure a record is truly new. The AT Protocol's design encourages specific use cases for each operation. For starter packs, this often means a careful sequence: use createRecord for initial setup, and then potentially use putRecord for subsequent updates if the protocol allows for it on specific record types.
The challenge arises when a script, perhaps designed to create a starter pack for a new user, is run multiple times. If it uses createRecord for the list and its members, the second execution will halt at the first record creation that already exists. A robust solution needs to handle this. It might involve checking for the existence of a record before attempting creation, or structuring the process so that putRecord is used where appropriate for updates.
Managing Updates and Deletions
Understanding the mutability of each record type is key to managing starter packs post-creation.
app.bsky.graph.list: This record is generally mutable. Fields like description and avatar can be updated usingputRecord. However, the core identity and purpose of the list are fixed.app.bsky.graph.listitem: These records are also mutable to a degree, but their primary function is to establish a link. While the AT Protocol allows for updates to existing records, adding or removing members is typically handled by creating newlistitemrecords or deleting existing ones. Direct updates to change which user is linked would be unusual and likely not supported by client applications.app.bsky.actor.profile: The user's profile record is highly mutable, allowing for updates to bio, display name, avatar, banner, etc.
The non-idempotency of createRecord means that if you want to update a starter pack programmatically, you cannot simply re-run the creation script. You must first determine which records exist, identify which ones need modification (e.g., adding new members, changing the description), and then use the appropriate operation (likely putRecord for updates, or createRecord for new members if their specific listitem doesn't exist).
This decomposition is not just an implementation detail; it's a fundamental aspect of how data is managed on the AT Protocol. It dictates the operational capabilities and potential pitfalls for developers building features like starter packs.
Why This Matters for Automation
For developers building automated pipelines, such as those for onboarding new users or managing community features, these details are paramount. A starter pack script that fails silently or with an error on subsequent runs due to non-idempotent creations is a liability. It requires careful state management: knowing what has already been created, what needs to be added, and what might need updating.
Consider an outreach pipeline that offers a starter pack to newly signed-up users. If the pipeline uses a simple createRecord flow, the first time it runs for a user, it works. The second time it runs (perhaps a day later for a follow-up or verification), it will error out if the records already exist. This necessitates a more sophisticated approach that checks for existing records before attempting creation or uses putRecord strategically.
The AT Protocol's design, with its distinct record types and operational semantics, mirrors real-world data management challenges. Understanding the difference between creating a new entity and updating an existing one, and how that translates to API calls like createRecord and putRecord, is fundamental to building reliable decentralized applications.
What nobody has fully addressed yet is the long-term strategy for managing these linked record sets when a user decides to drastically change their starter pack's focus or disband it entirely. Deleting records, especially in a decentralized system with potential replication, presents its own set of challenges and implications for data persistence and discoverability.
