Header menu logo FSharp.ATProto

Feeds

Read timelines, author feeds, liked posts, and bookmarks through the Bluesky module.

All examples use taskResult {} -- see the Error Handling guide for details.

Domain Types

FeedItem

A single item in a feed or timeline, pairing a post with the reason it appeared.

Field

Type

Description

Post

TimelinePost

The post content and metadata

Reason

FeedReason option

Why this item appeared (repost, pin, or None for organic)

FeedReason

Discriminated union indicating why a post appeared in a feed.

Case

Payload

Description

Repost

by : ProfileSummary

Someone reposted this post

Pin

--

The post is pinned to the author's profile

TimelinePost

A flattened, ergonomic view of a post. Engagement counts are plain int64 (not Option), and viewer state is exposed as simple booleans.

Field

Type

Description

Uri

AtUri

The AT-URI of the post record

Cid

Cid

The CID of the post record version

Author

ProfileSummary

The post author

Text

string

The post text content

Facets

Facet list

Rich text facets (mentions, links, hashtags)

LikeCount

int64

Number of likes

RepostCount

int64

Number of reposts

ReplyCount

int64

Number of replies

QuoteCount

int64

Number of quote posts

IndexedAt

DateTimeOffset

When the post was indexed

IsLiked

bool

Whether the authenticated user liked this post

IsReposted

bool

Whether the authenticated user reposted this post

IsBookmarked

bool

Whether the authenticated user bookmarked this post

Page<'T>

A paginated result containing a list of items and an optional cursor for the next page.

Field

Type

Description

Items

'T list

The items in this page

Cursor

string option

Cursor for the next page, or None if this is the last page

FeedGenerator

Metadata about a custom feed generator.

Field

Type

Description

Uri

AtUri

Feed generator AT-URI

Did

Did

Feed generator service DID

Creator

ProfileSummary

Feed creator

DisplayName

string

Display name

Description

string option

Description

Avatar

string option

Avatar URL

LikeCount

int64

Number of likes

IsOnline

bool

Whether the generator is currently online

IsValid

bool

Whether the generator is valid

Functions

Reading Feeds

Function

Accepts

Returns

Description

Bluesky.getTimeline

agent, limit: int64 option, cursor: string option

Result<Page<FeedItem>, XrpcError>

Fetch the authenticated user's home timeline

Bluesky.getAuthorFeed

agent, actor, limit: int64 option, cursor: string option

Result<Page<FeedItem>, XrpcError>

Fetch posts by a specific user

Bluesky.getActorLikes

agent, actor, limit: int64 option, cursor: string option

Result<Page<FeedItem>, XrpcError>

Fetch posts that a specific user has liked

Bluesky.getBookmarks

agent, limit: int64 option, cursor: string option

Result<Page<TimelinePost>, XrpcError>

Fetch the authenticated user's bookmarked posts

Bluesky.getFeed

agent, feed: AtUri, limit: int64 option, cursor: string option

Result<Page<FeedItem>, XrpcError>

Fetch posts from a custom feed generator

Bluesky.getActorFeeds

agent, actor, limit: int64 option, cursor: string option

Result<Page<FeedGenerator>, XrpcError>

List feed generators created by a user (SRTP)

Bluesky.getListFeed

agent, list: AtUri, limit: int64 option, cursor: string option

Result<Page<FeedItem>, XrpcError>

Fetch posts from a list-based feed

Bluesky.getFeedGenerator

agent, feed: AtUri

Result<FeedGenerator, XrpcError>

Get metadata for a feed generator

SRTP: getAuthorFeed, getActorLikes, and getActorFeeds accept ProfileSummary, Profile, Handle, or Did for the actor parameter.

taskResult {
    let! page = Bluesky.getTimeline agent (Some 25L) None

    for item in page.Items do
        let author = Handle.value item.Post.Author.Handle
        printfn "@%s: %s" author item.Post.Text
}

taskResult {
    // Pass a ProfileSummary directly -- no need to extract a handle string
    let! page = Bluesky.getAuthorFeed agent someProfile (Some 25L) None

    for item in page.Items do
        printfn "%s (%d likes)" item.Post.Text item.Post.LikeCount
}

Bookmarks

Function

Accepts

Returns

Description

Bluesky.addBookmark

agent, target

Result<unit, XrpcError>

Add a post to your bookmarks

Bluesky.removeBookmark

agent, target

Result<unit, XrpcError>

Remove a post from your bookmarks

SRTP: addBookmark accepts PostRef or TimelinePost. removeBookmark accepts AtUri, PostRef, or TimelinePost.

taskResult {
    let! _ = Bluesky.addBookmark agent timelinePost
    let! _ = Bluesky.removeBookmark agent timelinePost
    return ()
}

Pagination

Function

Accepts

Returns

Description

Bluesky.paginateTimeline

agent, pageSize: int64 option

IAsyncEnumerable<Result<Page<FeedItem>, XrpcError>>

Lazily paginate the home timeline

Bluesky.paginateFollowers

agent, actor, pageSize: int64 option

IAsyncEnumerable<Result<Page<ProfileSummary>, XrpcError>>

Lazily paginate an actor's followers

Bluesky.paginateNotifications

agent, pageSize: int64 option

IAsyncEnumerable<Result<Page<Notification>, XrpcError>>

Lazily paginate notifications

Bluesky.paginateBlocks

agent, pageSize: int64 option

IAsyncEnumerable<Result<Page<ProfileSummary>, XrpcError>>

Paginate blocked users

Bluesky.paginateMutes

agent, pageSize: int64 option

IAsyncEnumerable<Result<Page<ProfileSummary>, XrpcError>>

Paginate muted users

Bluesky.paginateFeed

agent, feed: AtUri, pageSize: int64 option

IAsyncEnumerable<Result<Page<FeedItem>, XrpcError>>

Paginate a custom feed

Bluesky.paginateListFeed

agent, list: AtUri, pageSize: int64 option

IAsyncEnumerable<Result<Page<FeedItem>, XrpcError>>

Paginate a list-based feed

SRTP: paginateFollowers accepts ProfileSummary, Profile, Handle, or Did for the actor parameter.

Paginators return an IAsyncEnumerable that fetches pages lazily and stops when the server returns no cursor. Iterate with await foreach:

task {
    let pages = Bluesky.paginateTimeline agent (Some 50L)

    // Consume from F# using TaskSeq or manual IAsyncEnumerator
    let enumerator = pages.GetAsyncEnumerator()

    let rec loop () = task {
        let! hasNext = enumerator.MoveNextAsync()
        if hasNext then
            match enumerator.Current with
            | Ok page ->
                for item in page.Items do
                    printfn "@%s: %s" (Handle.value item.Post.Author.Handle) item.Post.Text
            | Error err ->
                printfn "Error: %A" err
            do! loop ()
    }

    do! loop ()
    do! enumerator.DisposeAsync()
}

For endpoints without a pre-built paginator, use Xrpc.paginate directly. See the Pagination guide for full details.

Matching Feed Reasons

Match on FeedReason to distinguish reposts and pins from organic posts:

for item in page.Items do
    match item.Reason with
    | Some (FeedReason.Repost (by = reposter)) ->
        printfn "Reposted by @%s: %s"
            (Handle.value reposter.Handle) item.Post.Text
    | Some FeedReason.Pin ->
        printfn "[Pinned] %s" item.Post.Text
    | None ->
        printfn "@%s: %s" (Handle.value item.Post.Author.Handle) item.Post.Text

Custom Feeds

Custom feeds (algorithmic feeds by third parties) are identified by an AT-URI. Use Bluesky.getFeed for convenience, or the raw XRPC wrapper for full control:

taskResult {
    let! output =
        AppBskyFeed.GetFeed.query
            agent
            { Feed = feedUri; Cursor = None; Limit = Some 25L }

    for item in output.Feed do
        printfn "@%s: %s" (Handle.value item.Post.Author.Handle) item.Post.Text
}

Power Users: Raw XRPC

The raw AppBskyFeed.GetAuthorFeed.query gives access to filter and pin options:

taskResult {
    let! output =
        AppBskyFeed.GetAuthorFeed.query
            agent
            { Actor = "someone.bsky.social"
              Cursor = None
              Filter = Some AppBskyFeed.GetAuthorFeed.PostsNoReplies
              IncludePins = Some true
              Limit = Some 25L }

    for item in output.Feed do
        printfn "%s" item.Post.Text
}

Filter Value

Description

PostsWithReplies

All posts including replies (default)

PostsNoReplies

Original posts only, no replies

PostsWithMedia

Only posts with images or video

PostsAndAuthorThreads

Posts and the author's own reply threads

PostsWithVideo

Only posts with video

Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
namespace FSharp.ATProto
namespace FSharp.ATProto.Syntax
namespace FSharp.ATProto.Core
namespace FSharp.ATProto.Bluesky
val agent: AtpAgent
module Unchecked from Microsoft.FSharp.Core.Operators
val defaultof<'T> : 'T
Multiple items
module AtpAgent from FSharp.ATProto.Core
<summary> Functions for creating and authenticating <see cref="AtpAgent" /> instances. </summary>

--------------------
type AtpAgent = { HttpClient: HttpClient mutable BaseUrl: Uri mutable Session: AtpSession option ExtraHeaders: (string * string) list AuthenticateRequest: (HttpRequestMessage -> unit) option RefreshAuthentication: (unit -> Task<Result<unit,XrpcError>>) option OnSessionChanged: (unit -> unit) option }
<summary> Client agent for communicating with an AT Protocol Personal Data Server (PDS). Holds the HTTP client, base URL, optional authenticated session, and extra headers. </summary>
<remarks> Create an agent with <see cref="AtpAgent.create" /> or <see cref="AtpAgent.createWithClient" />, then authenticate with <see cref="AtpAgent.login" />. The agent's <see cref="Session" /> field is mutable: it is updated automatically on login and token refresh. </remarks>
<example><code> let agent = AtpAgent.create "https://bsky.social" let! session = AtpAgent.login "my-handle.bsky.social" "app-password-here" agent </code></example>
val someProfile: Profile
Multiple items
module Profile from FSharp.ATProto.Bluesky

--------------------
type Profile = { Did: Did Handle: Handle DisplayName: string Description: string Avatar: string option Banner: string option PostsCount: int64 FollowersCount: int64 FollowsCount: int64 IsFollowing: bool ... }
<summary> A full user profile with engagement counts and relationship state. Maps from <c>ProfileViewDetailed</c>. </summary>
val timelinePost: TimelinePost
Multiple items
module TimelinePost from FSharp.ATProto.Bluesky

--------------------
type TimelinePost = { Uri: AtUri Cid: Cid Author: ProfileSummary Text: string Facets: Facet list LikeCount: int64 RepostCount: int64 ReplyCount: int64 QuoteCount: int64 IndexedAt: DateTimeOffset ... }
<summary> A post with engagement counts and viewer state, used in feeds and timelines. Maps from <c>PostView</c>. </summary>
val feedUri: AtUri
Multiple items
module AtUri from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="AtUri" /> values. </summary>

--------------------
type AtUri = private | AtUri of string override ToString: unit -> string
<summary> An AT-URI that identifies a resource in the AT Protocol network. AT-URIs use the scheme <c>at://</c> followed by an authority (DID or handle), an optional collection (NSID), and an optional record key. Format: <c>at://&lt;authority&gt;[/&lt;collection&gt;[/&lt;rkey&gt;]]</c>. Maximum length is 8192 characters. </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/at-uri-scheme </remarks>
val page: Page<FeedItem>
type Page<'T> = { Items: 'T list Cursor: string option }
<summary>A paginated result containing a list of items and an optional cursor for the next page.</summary>
Multiple items
module FeedItem from FSharp.ATProto.Bluesky

--------------------
type FeedItem = { Post: TimelinePost Reason: FeedReason option ReplyParent: TimelinePost option }
<summary>A single item in a feed or timeline.</summary>
val taskResult: TaskResultBuilder
module Bluesky from FSharp.ATProto.Bluesky
<summary> High-level convenience methods for common Bluesky operations: posting, replying, liking, reposting, following, blocking, uploading blobs, and deleting records. All methods require an authenticated <see cref="AtpAgent" />. </summary>
val getTimeline: agent: AtpAgent -> limit: int64 option -> cursor: string option -> System.Threading.Tasks.Task<Result<Page<FeedItem>,XrpcError>>
<summary> Get the authenticated user's home timeline. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="limit">Maximum number of posts to return (optional, pass <c>None</c> for server default).</param>
<param name="cursor">Pagination cursor from a previous response (optional, pass <c>None</c> to start from the beginning).</param>
<returns>A page of <see cref="FeedItem" /> with an optional cursor, or an <see cref="XrpcError" />.</returns>
union case Option.Some: Value: 'T -> Option<'T>
union case Option.None: Option<'T>
val item: FeedItem
Page.Items: FeedItem list
val author: string
Multiple items
module Handle from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="Handle" /> values. </summary>

--------------------
type Handle = private | Handle of string override ToString: unit -> string
<summary> A handle (domain name) used as a human-readable identifier in the AT Protocol. Handles are DNS-based names (e.g. <c>my-handle.bsky.social</c>) that resolve to a <see cref="Did" />. They must be valid domain names with at least two segments and a maximum length of 253 characters. </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/handle </remarks>
val value: Handle -> string
<summary> Extract the string representation of a handle. </summary>
<param name="handle">The handle to extract the value from.</param>
<returns>The full handle string (e.g. <c>"my-handle.bsky.social"</c>).</returns>
FeedItem.Post: TimelinePost
TimelinePost.Author: ProfileSummary
ProfileSummary.Handle: Handle
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
TimelinePost.Text: string
val getAuthorFeed: agent: AtpAgent -> actor: 'a -> limit: int64 option -> cursor: string option -> System.Threading.Tasks.Task<Result<Page<FeedItem>,XrpcError>> (requires member ToActorString)
<summary> Get a specific user's feed (posts by that actor). Accepts a <see cref="Handle" />, <see cref="Did" />, <see cref="ProfileSummary" />, or <see cref="Profile" />. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="actor">A <see cref="Handle" />, <see cref="Did" />, <see cref="ProfileSummary" />, or <see cref="Profile" />.</param>
<param name="limit">Maximum number of posts to return (optional).</param>
<param name="cursor">Pagination cursor from a previous response (optional).</param>
<returns>A page of <see cref="FeedItem" /> with an optional cursor, or an <see cref="XrpcError" />.</returns>
TimelinePost.LikeCount: int64
val addBookmark: agent: AtpAgent -> target: 'a -> System.Threading.Tasks.Task<Result<unit,XrpcError>> (requires member ToPostRef)
<summary> Add a post to your bookmarks. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="target">A <see cref="PostRef" /> or <see cref="TimelinePost" /> identifying the post to bookmark.</param>
<returns><c>unit</c> on success, or an <see cref="XrpcError" />.</returns>
val removeBookmark: agent: AtpAgent -> target: 'a -> System.Threading.Tasks.Task<Result<unit,XrpcError>> (requires member ToAtUri)
<summary> Remove a post from your bookmarks. Accepts an <see cref="AtUri" />, <see cref="PostRef" />, or <see cref="TimelinePost" />. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="target">The bookmarked post to remove (an <see cref="AtUri" />, <see cref="PostRef" />, or <see cref="TimelinePost" />).</param>
<returns><c>unit</c> on success, or an <see cref="XrpcError" />.</returns>
val task: TaskBuilder
val pages: System.Collections.Generic.IAsyncEnumerable<Result<Page<FeedItem>,XrpcError>>
val paginateTimeline: agent: AtpAgent -> pageSize: int64 option -> System.Collections.Generic.IAsyncEnumerable<Result<Page<FeedItem>,XrpcError>>
<summary> Paginate the home timeline. Returns an async enumerable of pages. Each element is a <c>Result</c> containing one page of feed items. Pagination stops automatically when the server returns no cursor. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="pageSize">Maximum number of posts per page (optional, pass <c>None</c> for server default).</param>
<returns>An <see cref="System.Collections.Generic.IAsyncEnumerable{T}" /> of paginated results.</returns>
val enumerator: System.Collections.Generic.IAsyncEnumerator<Result<Page<FeedItem>,XrpcError>>
System.Collections.Generic.IAsyncEnumerable.GetAsyncEnumerator(?cancellationToken: System.Threading.CancellationToken) : System.Collections.Generic.IAsyncEnumerator<Result<Page<FeedItem>,XrpcError>>
val loop: unit -> System.Threading.Tasks.Task<unit>
val hasNext: bool
System.Collections.Generic.IAsyncEnumerator.MoveNextAsync() : System.Threading.Tasks.ValueTask<bool>
property System.Collections.Generic.IAsyncEnumerator.Current: Result<Page<FeedItem>,XrpcError> with get
<summary>Gets the element in the collection at the current position of the enumerator.</summary>
<returns>The element in the collection at the current position of the enumerator.</returns>
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val err: XrpcError
System.IAsyncDisposable.DisposeAsync() : System.Threading.Tasks.ValueTask
FeedItem.Reason: FeedReason option
type FeedReason = | Repost of by: ProfileSummary | Pin
<summary>Reason a post appeared in a feed.</summary>
union case FeedReason.Repost: by: ProfileSummary -> FeedReason
val reposter: ProfileSummary
union case FeedReason.Pin: FeedReason
val output: AppBskyFeed.GetFeed.Output
module AppBskyFeed from FSharp.ATProto.Bluesky
module GetFeed from FSharp.ATProto.Bluesky.AppBskyFeed
val query: agent: AtpAgent -> parameters: AppBskyFeed.GetFeed.Params -> System.Threading.Tasks.Task<Result<AppBskyFeed.GetFeed.Output,XrpcError>>
val item: AppBskyFeed.Defs.FeedViewPost
AppBskyFeed.GetFeed.Output.Feed: AppBskyFeed.Defs.FeedViewPost list
AppBskyFeed.Defs.FeedViewPost.Post: AppBskyFeed.Defs.PostView
AppBskyFeed.Defs.PostView.Author: AppBskyActor.Defs.ProfileViewBasic
AppBskyActor.Defs.ProfileViewBasic.Handle: Handle
property AppBskyFeed.Defs.PostView.Text: string with get
<summary> Post text (empty string if not a post record). </summary>
val output: AppBskyFeed.GetAuthorFeed.Output
module GetAuthorFeed from FSharp.ATProto.Bluesky.AppBskyFeed
val query: agent: AtpAgent -> parameters: AppBskyFeed.GetAuthorFeed.Params -> System.Threading.Tasks.Task<Result<AppBskyFeed.GetAuthorFeed.Output,XrpcError>>
union case AppBskyFeed.GetAuthorFeed.ParamsFilter.PostsNoReplies: AppBskyFeed.GetAuthorFeed.ParamsFilter
AppBskyFeed.GetAuthorFeed.Output.Feed: AppBskyFeed.Defs.FeedViewPost list

Type something to start searching.