Header menu logo FSharp.ATProto

Ozone

The Ozone module provides convenience methods for Ozone moderation tooling (tools.ozone.* endpoints). These are used by labeler operators and moderation teams to take actions on content and accounts.

All Ozone functions require a serviceDid parameter -- the DID of the Ozone moderation service. The proxy header is applied automatically; you do not need to configure it manually.

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

open FSharp.ATProto.Core
open FSharp.ATProto.Bluesky
open FSharp.ATProto.Syntax

Key Types

OzoneSubject

The target of a moderation action:

type OzoneSubject =
    | Account of Did      // an entire account
    | Record of PostRef   // a specific record (post, etc.)

ModerationAction

The action to perform:

Case

Description

Takedown (comment, durationInHours)

Take down a subject permanently or temporarily

ReverseTakedown (comment)

Reverse a previous takedown

Acknowledge (comment)

Acknowledge a report

Escalate (comment)

Escalate for higher-level review

Label (comment, createLabels, negateLabels)

Add or remove labels

Comment (comment, sticky)

Add a comment to a subject

Mute (comment, durationInHours)

Mute incoming reports on a subject

Unmute (comment)

Unmute incoming reports

MuteReporter (comment, durationInHours)

Mute incoming reports from a reporter

UnmuteReporter (comment)

Unmute a reporter

Tag (comment, add, remove)

Add or remove tags

ResolveAppeal (comment)

Resolve an appeal

TeamRole

type TeamRole = Admin | Moderator | Triage | Verifier

TeamMember

type TeamMember =
    { Did : Did
      Role : TeamRole
      Disabled : bool
      CreatedAt : DateTimeOffset option
      UpdatedAt : DateTimeOffset option }

Moderation Events

Function

Description

Ozone.emitEvent

Emit a moderation event on a subject

Ozone.getEvent

Get a moderation event by ID

Ozone.queryEvents

Query moderation events with optional filters

Ozone.queryStatuses

Query the moderation review queue

Ozone.getSubjects

Get detailed subject information

Taking Down a Post and Adding a Label

taskResult {
    let! agent = Bluesky.login "https://bsky.social" "mod-handle.bsky.social" "app-password"

    // Take down a post
    let! _ =
        Ozone.emitEvent agent serviceDid
            (OzoneSubject.Record offendingPostRef)
            (ModerationAction.Takedown (Some "Violates community guidelines", None))

    // Add a label to an account
    let! _ =
        Ozone.emitEvent agent serviceDid
            (OzoneSubject.Account userDid)
            (ModerationAction.Label (Some "Content warning", [ "nsfw" ], []))

    // Reverse the takedown later
    let! _ =
        Ozone.emitEvent agent serviceDid
            (OzoneSubject.Record offendingPostRef)
            (ModerationAction.ReverseTakedown (Some "Reviewed and cleared"))

    return ()
}

Repository Inspection

Function

Description

Ozone.getRepo

Get detailed moderation view for an account by DID

Ozone.getRecord

Get detailed moderation view for a record by AT-URI

Ozone.searchRepos

Search accounts in the moderation system

taskResult {
    let! repoDetail = Ozone.getRepo agent serviceDid userDid
    let! recordDetail = Ozone.getRecord agent serviceDid recordUri
    let! searchResults = Ozone.searchRepos agent serviceDid "spam" (Some 10L) None
    return ()
}

Team Management

Function

Description

Ozone.listMembers

List moderation team members

Ozone.addMember

Add a team member with a role

Ozone.removeMember

Remove a team member

Ozone.updateMember

Update a member's role or disabled status

taskResult {
    // Add a new moderator
    let! _newMember = Ozone.addMember agent serviceDid newModeratorDid TeamRole.Moderator

    // Promote to admin
    let! _ = Ozone.updateMember agent serviceDid newModeratorDid (Some TeamRole.Admin) None

    // List all team members
    let! page = Ozone.listMembers agent serviceDid None None

    for m in page.Items do
        printfn "%s - %A" (Did.value m.Did) m.Role
}

Communication Templates

Templates for standardized moderation communications:

Function

Description

Ozone.listTemplates

List all communication templates

Ozone.createTemplate

Create a new template

Ozone.updateTemplate

Update an existing template

Ozone.deleteTemplate

Delete a template by ID

taskResult {
    let! template =
        Ozone.createTemplate agent serviceDid
            "Spam Warning"
            "Your content was flagged as spam. Please review our community guidelines."
            "Content Policy Notice"

    printfn "Created template: %s" template.Id

    // Update the template
    let! _ = Ozone.updateTemplate agent serviceDid template.Id
                (Some "Updated Spam Warning") None None None

    // Delete it
    do! Ozone.deleteTemplate agent serviceDid template.Id
}
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 serviceDid: Did
Multiple items
module Did from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="Did" /> values. </summary>

--------------------
type Did = private | Did of string override ToString: unit -> string
<summary> A decentralized identifier (DID) as defined by the AT Protocol. DIDs are the primary stable identifier for accounts. Two methods are currently supported: <c>did:plc:</c> (hosted, managed by PLC directory) and <c>did:web:</c> (self-hosted, DNS-based). </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/did and the W3C DID specification: https://www.w3.org/TR/did-core/ </remarks>
val offendingPostRef: PostRef
type PostRef = { Uri: AtUri Cid: Cid }
<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>
val userDid: Did
val recordUri: 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 newModeratorDid: Did
type OzoneSubject = | Account of obj | Record of obj
type TeamRole = | Admin | Moderator | Triage | Verifier
type TeamMember = { Did: obj Role: TeamRole Disabled: bool CreatedAt: obj UpdatedAt: obj }
type bool = System.Boolean
type 'T option = Option<'T>
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 login: baseUrl: string -> identifier: string -> password: string -> System.Threading.Tasks.Task<Result<AtpAgent,XrpcError>>
module Ozone from FSharp.ATProto.Bluesky
<summary> Convenience methods for Ozone moderation tooling (<c>tools.ozone.*</c> endpoints). Wraps the generated XRPC types with a simplified, type-safe API. <para> Ozone endpoints require the agent to be proxied to the moderation service. Pass the labeler/moderation service DID via the <c>serviceDid</c> parameter. The proxy header (<c>atproto-proxy: {did}#atproto_labeler</c>) is applied automatically -- callers do not need to configure proxy headers manually. </para></summary>
val emitEvent: agent: AtpAgent -> serviceDid: Did -> subject: OzoneSubject -> action: ModerationAction -> System.Threading.Tasks.Task<Result<ToolsOzoneModeration.Defs.ModEventView,XrpcError>>
<summary> Emit a moderation event on a subject. This is the primary way to take moderation actions in Ozone: takedowns, labels, flags, acknowledgments, escalations, and more. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="serviceDid">The DID of the Ozone moderation service to proxy to.</param>
<param name="subject">The subject of the moderation action (account or record).</param>
<param name="action">The moderation action to perform.</param>
<returns>The emitted moderation event view, or an <see cref="XrpcError" />.</returns>
type OzoneSubject = | Account of Did | Record of PostRef
<summary> The subject of an Ozone moderation action: either an account (by DID) or a record (by AT-URI + CID). </summary>
union case OzoneSubject.Record: PostRef -> OzoneSubject
type ModerationAction = | Takedown of comment: string option * durationInHours: int64 option | ReverseTakedown of comment: string option | Acknowledge of comment: string option | Escalate of comment: string option | Label of comment: string option * createLabels: string list * negateLabels: string list | Comment of comment: string * sticky: bool option | Mute of comment: string option * durationInHours: int64 | Unmute of comment: string option | MuteReporter of comment: string option * durationInHours: int64 option | UnmuteReporter of comment: string option ...
<summary> A type-safe union of moderation events that can be emitted via <c>Ozone.emitEvent</c>. Each case maps to the corresponding <c>tools.ozone.moderation.defs#modEvent*</c> type. </summary>
union case ModerationAction.Takedown: comment: string option * durationInHours: int64 option -> ModerationAction
<summary> Take down a subject permanently or temporarily. </summary>
union case Option.Some: Value: 'T -> Option<'T>
union case Option.None: Option<'T>
union case OzoneSubject.Account: Did -> OzoneSubject
union case ModerationAction.Label: comment: string option * createLabels: string list * negateLabels: string list -> ModerationAction
<summary> Add or remove labels on a subject. </summary>
union case ModerationAction.ReverseTakedown: comment: string option -> ModerationAction
<summary> Reverse a previous takedown action. </summary>
val repoDetail: ToolsOzoneModeration.Defs.RepoViewDetail
val getRepo: agent: AtpAgent -> serviceDid: Did -> did: Did -> System.Threading.Tasks.Task<Result<ToolsOzoneModeration.Defs.RepoViewDetail,XrpcError>>
<summary> Get the detailed moderation view for a specific repo (account) by DID. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="serviceDid">The DID of the Ozone moderation service.</param>
<param name="did">The DID of the account to look up.</param>
<returns>The detailed repo view, or an <see cref="XrpcError" />.</returns>
val recordDetail: ToolsOzoneModeration.Defs.RecordViewDetail
val getRecord: agent: AtpAgent -> serviceDid: Did -> uri: AtUri -> System.Threading.Tasks.Task<Result<ToolsOzoneModeration.Defs.RecordViewDetail,XrpcError>>
<summary> Get the detailed moderation view for a specific record by AT-URI. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="serviceDid">The DID of the Ozone moderation service.</param>
<param name="uri">The AT-URI of the record to look up.</param>
<returns>The detailed record view, or an <see cref="XrpcError" />.</returns>
val searchResults: Page<ToolsOzoneModeration.Defs.RepoView>
val searchRepos: agent: AtpAgent -> serviceDid: Did -> query: string -> limit: int64 option -> cursor: string option -> System.Threading.Tasks.Task<Result<Page<ToolsOzoneModeration.Defs.RepoView>,XrpcError>>
<summary> Search repos (accounts) in the moderation system by query string. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="serviceDid">The DID of the Ozone moderation service.</param>
<param name="query">The search query string.</param>
<param name="limit">Maximum number of results to return.</param>
<param name="cursor">Pagination cursor from a previous response.</param>
<returns>A page of repo views, or an <see cref="XrpcError" />.</returns>
val _newMember: TeamMember
val addMember: agent: AtpAgent -> serviceDid: Did -> memberDid: Did -> role: TeamRole -> System.Threading.Tasks.Task<Result<TeamMember,XrpcError>>
<summary> Add a new member to the Ozone moderation team. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="serviceDid">The DID of the Ozone moderation service.</param>
<param name="memberDid">The DID of the user to add as a team member.</param>
<param name="role">The role to assign to the new member.</param>
<returns>The newly created team member, or an <see cref="XrpcError" />.</returns>
type TeamRole = | Admin | Moderator | Triage | Verifier
<summary> The role of a team member in an Ozone moderation service. </summary>
union case TeamRole.Moderator: TeamRole
val updateMember: agent: AtpAgent -> serviceDid: Did -> memberDid: Did -> role: TeamRole option -> disabled: bool option -> System.Threading.Tasks.Task<Result<TeamMember,XrpcError>>
<summary> Update an existing team member's role or disabled status. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="serviceDid">The DID of the Ozone moderation service.</param>
<param name="memberDid">The DID of the member to update.</param>
<param name="role">The new role for the member, or <c>None</c> to leave unchanged.</param>
<param name="disabled">Whether the member should be disabled, or <c>None</c> to leave unchanged.</param>
<returns>The updated team member, or an <see cref="XrpcError" />.</returns>
union case TeamRole.Admin: TeamRole
val page: Page<TeamMember>
val listMembers: agent: AtpAgent -> serviceDid: Did -> limit: int64 option -> cursor: string option -> System.Threading.Tasks.Task<Result<Page<TeamMember>,XrpcError>>
<summary> List the members of the Ozone moderation team. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="serviceDid">The DID of the Ozone moderation service.</param>
<param name="limit">Maximum number of members to return.</param>
<param name="cursor">Pagination cursor from a previous response.</param>
<returns>A page of team members, or an <see cref="XrpcError" />.</returns>
val m: TeamMember
Page.Items: TeamMember list
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
val value: Did -> string
<summary> Extract the string representation of a DID. </summary>
<param name="did">The DID to extract the value from.</param>
<returns>The full DID string (e.g. <c>"did:plc:z72i7hdynmk6r22z27h6tvur"</c>).</returns>
TeamMember.Did: Did
TeamMember.Role: TeamRole
val template: CommunicationTemplate
val createTemplate: agent: AtpAgent -> serviceDid: Did -> name: string -> contentMarkdown: string -> subject: string -> System.Threading.Tasks.Task<Result<CommunicationTemplate,XrpcError>>
<summary> Create a new communication template. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="serviceDid">The DID of the Ozone moderation service.</param>
<param name="name">The name of the template.</param>
<param name="contentMarkdown">The template content in Markdown format.</param>
<param name="subject">Optional subject line for the template.</param>
<returns>The newly created template, or an <see cref="XrpcError" />.</returns>
CommunicationTemplate.Id: string
val updateTemplate: agent: AtpAgent -> serviceDid: Did -> id: string -> name: string option -> contentMarkdown: string option -> subject: string option -> disabled: bool option -> System.Threading.Tasks.Task<Result<CommunicationTemplate,XrpcError>>
<summary> Update an existing communication template. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="serviceDid">The DID of the Ozone moderation service.</param>
<param name="id">The ID of the template to update.</param>
<param name="name">New name, or <c>None</c> to leave unchanged.</param>
<param name="contentMarkdown">New content, or <c>None</c> to leave unchanged.</param>
<param name="subject">New subject, or <c>None</c> to leave unchanged.</param>
<param name="disabled">Whether to disable the template, or <c>None</c> to leave unchanged.</param>
<returns>The updated template, or an <see cref="XrpcError" />.</returns>
val deleteTemplate: agent: AtpAgent -> serviceDid: Did -> id: string -> System.Threading.Tasks.Task<Result<unit,XrpcError>>
<summary> Delete a communication template by ID. </summary>
<param name="agent">An authenticated <see cref="AtpAgent" />.</param>
<param name="serviceDid">The DID of the Ozone moderation service.</param>
<param name="id">The ID of the template to delete.</param>
<returns><c>Ok ()</c> on success, or an <see cref="XrpcError" />.</returns>

Type something to start searching.