Querying Ozone
Reference guide for the 10 Ozone MCP tools available through the skywatch-mcp server. Covers parameter patterns, filter combinations, pagination, and common recipes. Load this skill when you need to interact with the Ozone moderation API.
Delegation Pattern
Ozone read queries (queue pulls, event history) and write actions (labelling, acknowledging, escalating) should be dispatched to subagents rather than executed inline by the supervisory agent. Pass the subagent the parameters it needs — subject, label, comment, batchId — and let it execute. This preserves the supervisory agent's context window for decision-making.
- Read queries: Dispatch to the
data-analystsubagent with the research question. Receive a structured summary. - Write actions: Dispatch to the
general-purpose-minisubagent with a pre-built action manifest. Mechanical execution, no judgment required. - ⚠️ Subagent data loss risk: When subagents are dispatched to collect data (fetch records, profiles, media), intermediate collection results are lost if the subagent fails to return structured data in its final message. Only the final message content persists in the task log. Ensure subagent prompts include explicit instructions to return structured summaries — if the subagent's output is unstructured or missing data, re-dispatch with a leaner, more focused prompt.
Rate limit note: Burst-launching 5+ subagents simultaneously can trigger 429 rate limit errors. Cap dispatch waves at 3 parallel launches; stagger retries.
Credentials
Ozone tools require five environment variables: OZONE_HANDLE, OZONE_ADMIN_PASSWORD, OZONE_DID, OZONE_PDS, OZONE_SERVICE_URL. All are set in .envrc in the project root. All tools fail gracefully with a clear error if credentials are missing.
Auth goes through the PDS (via atproto-proxy header), not directly to the Ozone service. The ozoneRequest helper automatically retries on ExpiredToken with a session refresh — no manual retry logic needed.
Direct HTTP API Access (No MCP Server)
On Letta Cloud (or any environment where the skywatch-mcp stdio MCP server is not available), Ozone API calls can be made directly via HTTP using curl or Python. The auth flow and endpoint mapping are documented in references/http-api-access.md.
Key points:
- Auth goes through the PDS: create a session via
com.atproto.server.createSession, then use theaccessJwtwithatproto-proxy: [OZONE_DID]#atproto_labelerheader - Writes MUST go through PDS proxy, not direct service URL: All write operations (label, acknowledge, comment, escalate, tag, mute) use
tools.ozone.moderation.emitEventviaPOST https://{pds}/xrpc/...— direct POSTs toOZONE_SERVICE_URLreturn401 BadJwtType - Read operations:
queryStatusesandqueryEventsare GET requests.queryStatuses?unresolvedOnly=trueis silently ignored on this deployment — filter byreviewStatemanually (full lexicon ref, e.g.tools.ozone.moderation.defs#reviewOpen) queryStatuses.labelsprojection is empty even when labels applied —queryLabelsis BROKEN on this deployment (501 via service URL,MethodNotImplementedvia PDS proxy, as of 2026-08-27); verify labels viaqueryEventswithtypes=tools.ozone.moderation.defs#modEventLabelinsteadqueryLabelsrequiresuriPatterns(noturi) on this deployment's lexicon — historical note; the endpoint is currently 501 anyway- Newer-version response shape: the response key is
subjectStatuses(notsubjects); SubjectStatus items have NOreportsarray and NOsubjectProfile; report reasons surface intagsasreport:<reasonType>; reporter identity and report comments require a separatequeryEvents?types=modEventReportcall (comments live at.event.comment, NOT.event.report.comment) - Token expiry must be handled manually (no auto-retry like the MCP server's
ozoneRequesthelper) - ClickHouse queries go through SSH + Docker (see
references/http-api-access.mdfor the command pattern)
When MCP tools are available, prefer them — they handle auth, retries, and type safety automatically. Use direct HTTP only when the MCP server is not configured.
Subject Format
All tools accept a subject parameter. Two formats:
- Account-level: Pass a DID (
did:plc:...). Actions apply to the account. - Post-level: Pass an AT-URI (
at://did:plc:.../app.bsky.feed.post/...). Requirescidparameter — resolve it viacom.atproto.repo.getRecordif not already known.
When in doubt, use account-level subjects. Post-level operations are for labelling or acting on specific content rather than the account as a whole.
Read Tools
mcp__skywatch-mcp__ozone_query_statuses
Query the moderation queue. Returns subjects with their current review state, tags, and metadata.
Parameters (all optional):
| Parameter | Type | Description |
|---|---|---|
subject |
string | Filter to a specific DID or AT-URI |
review_state |
string | Filter by review state (e.g., open, escalated, closed) |
tags |
array | Filter by tags applied to subjects |
takendown |
bool | Filter for subjects that have been taken down |
appealed |
bool | Filter for subjects with active appeals |
limit |
number | Results per page |
cursor |
string | Pagination cursor from previous response |
sort_field |
string | Field to sort by (e.g., lastReportedAt, lastReviewedAt) |
sort_direction |
string | asc or desc |
Common recipes:
| Goal | Parameters |
|---|---|
| Recent user reports | sort_field: "lastReportedAt", sort_direction: "desc", limit: 20 |
| Open appeals | appealed: true, sort_direction: "desc" |
| Unreviewed subjects | review_state: "open", sort_field: "lastReportedAt", sort_direction: "desc" |
| All reports for one account | subject: "did:plc:..." |
| Tagged for follow-up | tags: ["follow-up"], review_state: "open" |
| Escalated subjects | review_state: "escalated" |
Pagination: When results exceed limit, the response includes a cursor. Pass it in the next call to get the next page. Continue until no cursor is returned.
⚠️ Newer-version response shape (2026-08-17): This deployment returns a newer Ozone API shape that diverges from the MCP abstraction. Underlying response key is subjectStatuses (NOT subjects — jq '.subjects | length' returns 0 on null and silently masks real results). SubjectStatus items do NOT embed a reports array or subjectProfile; report reasons surface in the tags array as report:<reasonType> (e.g. report:harassment-other, report:misleading-scam). Reporter identity and report comments require a separate queryEvents call with types=modEventReport — report comments are at .event.comment on those events, NOT .event.report.comment (which silently returns null). queryStatuses?subject=<did> returns ONLY the account subject, not the account's reported records — query each post AT-URI separately. Acknowledged record-level subjects vanish from queryStatuses entirely — verify post-level acks via the emitEvent response ID, not a later subject query.
mcp__skywatch-mcp__ozone_query_events
Query moderation event history. Returns a log of all moderation actions taken on subjects.
Parameters (all optional):
| Parameter | Type | Description |
|---|---|---|
subject |
string | Filter to a specific DID or AT-URI |
types |
array | Filter by event type (e.g., label, acknowledge, comment, escalate, tag, mute, report) |
created_by |
string | Filter by moderator DID |
created_after |
string | ISO datetime — events after this time |
created_before |
string | ISO datetime — events before this time |
added_labels |
array | Filter for events that applied specific labels |
has_comment |
bool | Filter for events with comments |
limit |
number | Results per page |
cursor |
string | Pagination cursor |
sort_direction |
string | asc or desc |
Common recipes:
| Goal | Parameters |
|---|---|
| Full moderation history for an account | subject: "did:plc:...", sort_direction: "asc" |
| Recent label actions | types: ["label"], sort_direction: "desc", limit: 50 |
| Events by a specific moderator | created_by: "did:plc:...", sort_direction: "desc" |
| All actions in a time window | created_after: "2026-04-01T00:00:00Z", created_before: "2026-04-28T00:00:00Z" |
| Check if a label was ever applied | subject: "did:plc:...", added_labels: ["spam"] |
| Events with moderator notes | has_comment: true, sort_direction: "desc" |
Write Tools
Common Conventions
batchId: All write tools accept an optional batchId (UUID string). Use a single batchId to group related operations — e.g., all labels applied during one queue triage session share one batchId. Generate a new UUID for each logical batch of work. Different types of actions (labelling vs. acknowledging) within the same session may use the same batchId if they're part of the same workflow.
comment: Most write tools accept an optional comment. Use it to record reasoning for the action. Comments become part of the permanent moderation event history.
Metadata: All write tools automatically include modTool metadata (name: "skywatch-mcp") for traceability.
mcp__skywatch-mcp__ozone_label
Apply or remove a moderation label.
| Parameter | Required | Description |
|---|---|---|
subject |
yes | DID or AT-URI |
label |
yes | Label string to apply or remove |
action |
yes | "apply" or "remove" |
cid |
post-level only | Content hash for AT-URI subjects |
comment |
no | Reasoning for the label action |
batchId |
no | UUID grouping related operations |
duration_in_hours |
no | For temporary labels — auto-expires after this duration |
When to use: After reviewing evidence and determining a policy violation. Use duration_in_hours for temporary labels on borderline cases or time-limited enforcement.
mcp__skywatch-mcp__ozone_acknowledge
Move a subject from open/reported to reviewed. Closes reports.
| Parameter | Required | Description |
|---|---|---|
subject |
yes | DID or AT-URI |
acknowledgeAccountSubjects |
no | true to acknowledge all reported content by this account |
comment |
no | Reasoning |
cid |
post-level only | Content hash |
batchId |
no | UUID |
When to use: After reviewing a subject and completing all actions (labelling, no-action, etc.). Acknowledgement closes the report in the queue. Use acknowledgeAccountSubjects: true to bulk-close all reports for an account in one call.
mcp__skywatch-mcp__ozone_comment
Add a comment to a subject's moderation record.
| Parameter | Required | Description |
|---|---|---|
subject |
yes | DID or AT-URI |
comment |
yes | Comment text |
sticky |
no | true for persistent visibility in the moderation UI |
cid |
post-level only | Content hash |
batchId |
no | UUID |
When to use: To record observations, investigation notes, or context that other moderators should see. Use sticky: true for important context that should remain visible (e.g., "this account is part of a coordinated network — see investigation report YYYY-MM-DD").
mcp__skywatch-mcp__ozone_escalate
Escalate a subject for higher-level review.
| Parameter | Required | Description |
|---|---|---|
subject |
yes | DID or AT-URI |
comment |
no | Reasoning for escalation |
cid |
post-level only | Content hash |
batchId |
no | UUID |
When to use: When a subject requires review by someone with more authority or context — policy edge cases, high-profile accounts, potential legal issues.
mcp__skywatch-mcp__ozone_tag
Add or remove tags from a subject's moderation record.
| Parameter | Required | Description |
|---|---|---|
subject |
yes | DID or AT-URI |
add |
no | Array of tag strings to add |
remove |
no | Array of tag strings to remove |
comment |
no | Reasoning |
cid |
post-level only | Content hash |
batchId |
no | UUID |
At least one of add or remove is required.
When to use: For categorisation, tracking, or workflow routing. Tags are searchable via mcp__skywatch-mcp__ozone_query_statuses.
mcp__skywatch-mcp__ozone_mute
Mute a subject for a specified duration.
| Parameter | Required | Description |
|---|---|---|
subject |
yes | DID or AT-URI |
duration_in_hours |
yes | How long to mute |
comment |
no | Reasoning |
cid |
post-level only | Content hash |
batchId |
no | UUID |
When to use: To suppress a subject from the queue temporarily — e.g., an account that's been reviewed but may need re-evaluation after a cooling period.
mcp__skywatch-mcp__ozone_unmute
Remove a mute from a previously muted subject.
| Parameter | Required | Description |
|---|---|---|
subject |
yes | DID or AT-URI |
comment |
no | Reasoning |
cid |
post-level only | Content hash |
batchId |
no | UUID |
mcp__skywatch-mcp__ozone_resolve_appeal
Resolve an appeal on a subject.
| Parameter | Required | Description |
|---|---|---|
subject |
yes | DID or AT-URI |
comment |
yes | Explanation of the resolution (required) |
cid |
post-level only | Content hash |
batchId |
no | UUID |
When to use: When a user has appealed a moderation action and you've reviewed the appeal. The comment is mandatory — document why the appeal was upheld or denied.
Gotchas
- Credentials via direnv — non-secret Ozone settings live in
.envrc;OZONE_ADMIN_PASSWORDlives insecrets/ozone.env, loaded by.envrcviasource_env_if_exists. Theskywatch-mcpPolytoken MCP server should be launched throughdirenv exec /path/to/project ...so Ozone tools receiveOZONE_HANDLE,OZONE_ADMIN_PASSWORD,OZONE_DID,OZONE_PDS, andOZONE_SERVICE_URLat server startup. - Auth route: PDS proxy, not direct Ozone connection — direct writes to
OZONE_SERVICE_URLwith proxied session JWTs return401 BadJwtType - cid required for post-level: If you have an AT-URI but no cid, resolve it first via
com.atproto.repo.getRecord - Auto-retry on token expiry: The
ozoneRequesthelper handlesExpiredTokenautomatically — don't add manual retry logic - Acknowledge closes reports: Acknowledgement is the "done" action — use it after all other actions are complete, not before
- Always acknowledge after labeling (subjects AND posts): Labeling without acknowledging leaves the report open in the queue.
queryStatuses?subject=<did>returns only the account subject — reported posts remain open and unacknowledged and must be handled individually. Never leave labeled subjects unacknowledged. - Batch labeling in ≤50-account chunks: Ozone rate-limits bulk labeling; batches of 200+ accounts may timeout. If a batch times out, check how many were labeled before the failure, then resume with the remaining accounts.
- modEventLabel uses OLD field names:
createLabelVals/negateLabelVals(notcreate/negate), andnegateLabelValsis required — send[]when negating nothing.createdByis a TOP-LEVEL body property (old lexicon), not inside the event object. Comments must be a separatemodEventCommentevent. - Verify labels via queryEvents, not queryLabels:
queryStatuses.labelsis empty even when labels applied, andqueryLabelsis BROKEN on this deployment as of 2026-08-27 (501 via service URL,MethodNotImplementedvia PDS proxy). Confirm subject DID,createLabelVals, andcreatedByonqueryEvents?types=tools.ozone.moderation.defs#modEventLabelevents instead. - batchId is optional but recommended: Makes audit trails traceable — group related operations under one UUID. Note
batchIdinside the event body is silently dropped (not in the lexicon); persist it via top-levelmodTool.meta.batchId. - ALWAYS resolve actor DIDs via
com.atproto.identity.resolveHandleimmediately before emitting labels: never write a target DID from memory, session context, or notes — DID conflation mislabels innocent accounts (three incidents: julian-reichelt 2026-08-20, aragornreedsyhere feed-extraction 2026-08-20, 13bullits 2026-08-30 — the last was a label applied to the wrong DID because the DID was carried over from earlier query results; caught 40 s later via post-emit verification). For 0-post or newly-created subjects, verify DID→handle→displayName against the investigation BEFORE emitting, and always verify the applied label by subjectHandle via queryEvents afterwards.