Posts
Create, read, quote, reply to, search, and delete Bluesky posts.
Domain Types
TimelinePost
A post with engagement counts and viewer state, returned by feed, search, and thread functions.
Field |
Type |
Description |
|---|---|---|
|
|
AT-URI identifying the post record |
|
|
Content hash of this exact version |
|
|
Post author |
|
|
Post text content |
|
|
Rich text facets (mentions, links, hashtags) |
|
|
Number of likes |
|
|
Number of reposts |
|
|
Number of replies |
|
|
Number of quote posts |
|
|
When the post was indexed |
|
|
Whether you have liked this post |
|
|
Whether you have reposted this post |
|
|
Whether you have bookmarked this post |
|
|
Embedded content (images, video, link card, quoted post) |
PostRef
A reference to a specific version of a post record, returned when creating a post.
Field |
Type |
Description |
|---|---|---|
|
|
AT-URI of the post record |
|
|
Content hash of the post version |
FeedItem
A single item from a feed or timeline, pairing a post with an optional reason.
Field |
Type |
Description |
|---|---|---|
|
|
The post content |
|
|
Why the post appeared (repost, pin, or |
|
|
The parent post if this is a reply |
FeedReason
Discriminated union indicating why a post appeared in a feed.
Case |
Fields |
Description |
|---|---|---|
|
|
Someone reposted this post |
|
-- |
Post is pinned |
PostEmbed
Discriminated union for embedded content in a post. Uses [<RequireQualifiedAccess>].
Case |
Fields |
Description |
|---|---|---|
|
|
One or more attached images |
|
|
An attached video |
|
|
A link card preview |
|
|
A quoted post embed |
|
|
A quoted post with additional media |
|
-- |
An unrecognized embed type |
Supporting types: PostImage has Thumb, Fullsize, Alt (all string). PostVideo has Thumbnail, Playlist (both string option), Alt (string option). PostExternalLink has Uri, Title, Description (all string), Thumb (string option).
ThreadNode
Discriminated union representing a node in a post thread tree.
Case |
Fields |
Description |
|---|---|---|
|
|
An accessible post with thread context |
|
|
The post was deleted or does not exist |
|
|
The post is blocked |
ThreadPost
A post within a thread, with parent and reply context.
Field |
Type |
Description |
|---|---|---|
|
|
The post itself |
|
|
Parent post in the thread |
|
|
Reply posts |
Functions
Creating Posts
Function |
Accepts |
Returns |
Description |
|---|---|---|---|
|
|
|
Create a post with auto-detected rich text |
|
|
|
Create a post with pre-resolved facets |
|
|
|
Create a post with attached images |
|
|
|
Create a quote post |
|
|
|
Reply to a post (auto-resolves thread root) |
|
|
|
Reply with explicit parent and root |
|
|
|
Upload video, wait for processing, and post with caption and alt text |
Video upload involves three steps:
uploadVideoto initiate,awaitVideoProcessingto poll until ready, then creating a post with the video blob.postWithVideowraps all three steps.
taskResult {
// Simple post with auto-detected mentions, links, hashtags
let! postRef = Bluesky.post agent "Hello from F#! #atproto"
// Quote another post
let! quoteRef = Bluesky.quotePost agent "Great take" postRef
// Reply (thread root resolved automatically)
let! replyRef = Bluesky.replyTo agent "I agree!" postRef
// Post with images
let imageBytes = System.IO.File.ReadAllBytes "photo.jpg"
let! imagePost = Bluesky.postWithImages agent "Check this out!"
[ { Data = imageBytes; MimeType = Jpeg; AltText = "A photo" } ]
()
}
Reading Posts
Function |
Accepts |
Returns |
Description |
|---|---|---|---|
|
|
|
Fetch multiple posts by AT-URI |
|
|
|
Get full thread tree for pattern matching |
|
|
|
Get thread as |
|
|
|
Full-text post search |
|
|
|
Get posts that quote a given post |
taskResult {
let! posts = Bluesky.getPosts agent [ postRef.Uri ]
let! results = Bluesky.searchPosts agent "F# atproto" (Some 10L) None
// Thread with pattern matching
let! thread = Bluesky.getPostThread agent postRef (Some 6L) (Some 3L)
match thread with
| ThreadNode.Post tp -> printfn "Post: %s" tp.Post.Text
| ThreadNode.NotFound uri -> printfn "Not found: %s" (AtUri.value uri)
| ThreadNode.Blocked uri -> printfn "Blocked: %s" (AtUri.value uri)
// Or the simplified view
let! threadOpt = Bluesky.getPostThreadView agent postRef (Some 6L) (Some 3L)
match threadOpt with
| Some tp -> printfn "Post: %s with %d replies" tp.Post.Text tp.Replies.Length
| None -> printfn "Post not accessible"
}
Engagement
Function |
Accepts |
Returns |
Description |
|---|---|---|---|
|
|
|
Like a post |
|
|
|
Repost a post |
|
|
|
Unlike by post (looks up viewer state) |
|
|
|
Un-repost by post (looks up viewer state) |
|
|
|
Undo a like by its ref |
|
|
|
Undo a repost by its ref |
unlike and unrepost are also available as simpler alternatives that return unit instead of UndoResult. The generic Bluesky.undo accepts any ref type (LikeRef, RepostRef, FollowRef, BlockRef).
taskResult {
let! likeRef = Bluesky.like agent post
let! _ = Bluesky.undoLike agent likeRef
// Or without keeping the ref:
let! result = Bluesky.unlikePost agent post
match result with
| Undone -> printfn "Unliked"
| WasNotPresent -> printfn "Was not liked"
}
Bookmarks
Function |
Accepts |
Returns |
Description |
|---|---|---|---|
|
|
|
Bookmark a post |
|
|
|
Remove a bookmark |
|
|
|
List bookmarked posts |
taskResult {
do! Bluesky.addBookmark agent post
let! page = Bluesky.getBookmarks agent (Some 25L) None
()
}
Deleting
Function |
Accepts |
Returns |
Description |
|---|---|---|---|
|
|
|
Delete any record by AT-URI |
taskResult {
do! Bluesky.deleteRecord agent postRef
}
SRTP Polymorphism
Many post functions accept multiple types via SRTP (statically resolved type parameters):
like,repost,replyTo,quotePost,addBookmarkacceptTimelinePostorPostRefdeleteRecord,removeBookmark,getPostThread,getPostThreadView,getLikes,getRepostedBy,getQuotes,muteThread,unmuteThreadacceptTimelinePost,PostRef, orAtUri
Pass entities directly -- no need to extract .Uri or construct a PostRef:
taskResult {
let! page = Bluesky.getTimeline agent (Some 10L) None
let post = page.Items.Head.Post
// Pass the TimelinePost directly
let! likeRef = Bluesky.like agent post
let! thread = Bluesky.getPostThreadView agent post (Some 6L) None
()
}
namespace FSharp
--------------------
namespace Microsoft.FSharp
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>
<summary> A reference to a specific version of a post record. Contains both the AT-URI (identifying the record) and the CID (identifying the exact version). Accepted by <c>like</c>, <c>repost</c>, and <c>replyTo</c>. </summary>
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>
<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>
<summary> Create a post with automatic rich text detection. Mentions, links, and hashtags are automatically detected and resolved to facets. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="text">The post text. Mentions (<c>@handle</c>), links (<c>https://...</c>), and hashtags (<c>#tag</c>) are auto-detected.</param>
<returns>A <see cref="PostRef" /> with the AT-URI and CID on success, or an <see cref="XrpcError" />.</returns>
<remarks> Internally calls <see cref="RichText.parse" /> to detect and resolve facets before creating the post. Unresolvable mentions are silently omitted from facets. For pre-resolved facets, use <see cref="postWithFacets" /> instead. </remarks>
<example><code> let! result = Bluesky.post agent "Hello @my-handle.bsky.social! Check out https://example.com #atproto" </code></example>
<summary> Create a quote post. The quoted post appears as an embedded record below your text. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="text">The post text. Mentions, links, and hashtags are auto-detected.</param>
<param name="quoted">A <see cref="PostRef" /> or <see cref="TimelinePost" /> identifying the post to quote.</param>
<returns>A <see cref="PostRef" /> with the AT-URI and CID on success, or an <see cref="XrpcError" />.</returns>
<summary> Reply to a post. Fetches the parent to auto-resolve the thread root. This is the recommended way to reply: you only need the parent post's <see cref="PostRef" />. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="text">The reply text. Mentions, links, and hashtags are auto-detected.</param>
<param name="parent">A <see cref="PostRef" /> or <see cref="TimelinePost" /> for the post being replied to.</param>
<returns>A <see cref="PostRef" /> with the AT-URI and CID on success, or an <see cref="XrpcError" />.</returns>
<remarks> Fetches the parent post via <c>app.bsky.feed.getPosts</c> to determine the thread root. If the parent has a <c>reply</c> field, its root is used. Otherwise, the parent itself is the root. For full control over both parent and root, use <see cref="replyWithKnownRoot" /> instead. </remarks>
<summary>Provides static methods for the creation, copying, deletion, moving, and opening of a single file, and aids in the creation of <see cref="T:System.IO.FileStream" /> objects.</summary>
<summary> Create a post with attached images and automatic rich text detection. Uploads each image as a blob, then creates the post with an <c>app.bsky.embed.images</c> embed. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="text">The post text. Mentions, links, and hashtags are auto-detected.</param>
<param name="images"> A list of <see cref="ImageUpload" /> records describing the images to attach. Alt text is required for accessibility. Bluesky supports up to 4 images per post. </param>
<returns>A <see cref="PostRef" /> with the AT-URI and CID on success, or an <see cref="XrpcError" />.</returns>
<remarks> Images are uploaded sequentially. If any image upload fails, the entire operation returns the error without creating the post. </remarks>
<example><code> let imageBytes = System.IO.File.ReadAllBytes("photo.jpg") let! result = Bluesky.postWithImages agent "Check this out!" [ { Data = imageBytes; MimeType = Jpeg; AltText = "A photo" } ] </code></example>
<summary> Get multiple posts by their AT-URIs in a single request. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="uris">A list of AT-URIs identifying the posts to retrieve.</param>
<returns>A list of <see cref="TimelinePost" /> on success, or an <see cref="XrpcError" />.</returns>
<summary>The AT-URI of the post record.</summary>
<summary> Search for posts matching a query string. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="query">The search query string.</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="TimelinePost" /> with an optional cursor, or an <see cref="XrpcError" />.</returns>
<summary> Get a post thread, returning a <see cref="ThreadNode" /> tree. 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 post (an <see cref="AtUri" />, <see cref="PostRef" />, or <see cref="TimelinePost" />).</param>
<param name="depth">How many levels of replies to include (optional, pass <c>None</c> for server default).</param>
<param name="parentHeight">How many levels of parent context to include (optional, pass <c>None</c> for server default).</param>
<returns>A <see cref="ThreadNode" /> tree on success, or an <see cref="XrpcError" />.</returns>
module ThreadNode from FSharp.ATProto.Bluesky
--------------------
type ThreadNode = | Post of ThreadPost | NotFound of AtUri | Blocked of AtUri
<summary>A node in a post thread tree.</summary>
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://<authority>[/<collection>[/<rkey>]]</c>. Maximum length is 8192 characters. </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/at-uri-scheme </remarks>
<summary> Extract the string representation of an AT-URI. </summary>
<param name="atUri">The AT-URI to extract the value from.</param>
<returns>The full AT-URI string (e.g. <c>"at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3k2la3b"</c>).</returns>
<summary> Get a post thread, returning just the <see cref="ThreadPost" /> if available. Returns <c>Some threadPost</c> for normal threads, <c>None</c> for not-found or blocked posts. 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 post (an <see cref="AtUri" />, <see cref="PostRef" />, or <see cref="TimelinePost" />).</param>
<param name="depth">How many levels of replies to include (optional).</param>
<param name="parentHeight">How many levels of parent context to include (optional).</param>
<returns><c>Some ThreadPost</c> if the post is accessible, <c>None</c> if not found or blocked, or an <see cref="XrpcError" />.</returns>
<summary> Like a post or other record. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="target">A <see cref="PostRef" /> or <see cref="TimelinePost" /> identifying the record to like.</param>
<returns>A <see cref="LikeRef" /> on success, or an <see cref="XrpcError" />. Pass the <c>LikeRef</c> to <see cref="unlike" /> to undo.</returns>
<summary> Undo a like by deleting the like record. Returns <see cref="UndoResult.Undone" /> on success. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="likeRef">The <see cref="LikeRef" /> returned by <see cref="like" />.</param>
<returns><c>Ok Undone</c> on success, or an <see cref="XrpcError" />. Note: the AT Protocol's deleteRecord is idempotent, so this always returns <c>Undone</c> even if the record was already deleted. Only target-based functions (<see cref="unlikePost" />/<see cref="unrepostPost" />) can return <c>WasNotPresent</c>. </returns>
<summary> Unlike a post by its <see cref="PostRef" />, without needing the original <see cref="LikeRef" />. Fetches the post to find the current user's like URI from the viewer state, then deletes it. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="target">The post to unlike (a <see cref="PostRef" /> or <see cref="TimelinePost" />).</param>
<returns><c>Ok Undone</c> if the like was found and deleted, <c>Ok WasNotPresent</c> if the post was not liked by the current user, or an <see cref="XrpcError" /> on failure. </returns>
<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>
<summary> Get the authenticated user's bookmarked posts. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="limit">Maximum number of bookmarks to return (optional).</param>
<param name="cursor">Pagination cursor from a previous response (optional).</param>
<returns>A page of <see cref="TimelinePost" /> with an optional cursor, or an <see cref="XrpcError" />.</returns>
<summary> Delete a record by its AT-URI. Accepts an <see cref="AtUri" />, <see cref="PostRef" />, or <see cref="TimelinePost" />. Can be used to unlike, un-repost, unfollow, unblock, or delete a post. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="target">The AT-URI of the record to delete (an <see cref="AtUri" />, <see cref="PostRef" />, or <see cref="TimelinePost" />).</param>
<returns><c>Ok ()</c> on success, or an <see cref="XrpcError" />.</returns>
<remarks> The AT-URI is parsed to extract the repo DID, collection, and record key. This is a general-purpose delete; pass the AT-URI returned when the record was created. </remarks>
<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>