# Skywatch Querying Ozone

> This skill should be used when working with the Ozone moderation API — query patterns, filter combinations, pagination, write tool conventions, and common recipes. Covers both MCP tool access and direct HTTP API access when MCP server is unavailable. Use when querying or writing to Ozone via MCP tools or HTTP. Does not prescribe a workflow, see working-the-queue for queue triage methodology.

- Skill: `skywatch-bsky/skywatch-querying-ozone` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add skywatch-bsky/skywatch-querying-ozone`
- Raw SKILL.md: https://api.skillmd.com/api/skills/skywatch-bsky/skywatch-querying-ozone/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: skywatch-bsky (https://skillmd.com/u/skywatch-bsky)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/skywatch-bsky/skywatch-querying-ozone

---


# 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-analyst` subagent with the research question. Receive a structured summary.
- **Write actions:** Dispatch to the `general-purpose-mini` subagent 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 the `accessJwt` with `atproto-proxy: [OZONE_DID]#atproto_labeler` header
- **Writes MUST go through PDS proxy, not direct service URL:** All write operations (label, acknowledge, comment, escalate, tag, mute) use `tools.ozone.moderation.emitEvent` via `POST https://{pds}/xrpc/...` — direct POSTs to `OZONE_SERVICE_URL` return `401 BadJwtType`
- **Read operations:** `queryStatuses` and `queryEvents` are GET requests. `queryStatuses?unresolvedOnly=true` is silently ignored on this deployment — filter by `reviewState` manually (full lexicon ref, e.g. `tools.ozone.moderation.defs#reviewOpen`)
- `queryStatuses.labels` projection is empty even when labels applied — **`queryLabels` is BROKEN on this deployment (501 via service URL, `MethodNotImplemented` via PDS proxy, as of 2026-08-27); verify labels via `queryEvents` with `types=tools.ozone.moderation.defs#modEventLabel`** instead
- **`queryLabels` requires `uriPatterns` (not `uri`)** on this deployment's lexicon — historical note; the endpoint is currently 501 anyway
- **Newer-version response shape:** the response key is `subjectStatuses` (not `subjects`); SubjectStatus items have NO `reports` array and NO `subjectProfile`; report reasons surface in `tags` as `report:<reasonType>`; reporter identity and report comments require a separate `queryEvents?types=modEventReport` call (comments live at `.event.comment`, NOT `.event.report.comment`)
- Token expiry must be handled manually (no auto-retry like the MCP server's `ozoneRequest` helper)
- ClickHouse queries go through SSH + Docker (see `references/http-api-access.md` for 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/...`). Requires `cid` parameter — resolve it via `com.atproto.repo.getRecord` if 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_PASSWORD` lives in `secrets/ozone.env`, loaded by `.envrc` via `source_env_if_exists`. The `skywatch-mcp` Polytoken MCP server should be launched through `direnv exec /path/to/project ...` so Ozone tools receive `OZONE_HANDLE`, `OZONE_ADMIN_PASSWORD`, `OZONE_DID`, `OZONE_PDS`, and `OZONE_SERVICE_URL` at server startup.
- **Auth route:** PDS proxy, not direct Ozone connection — direct writes to `OZONE_SERVICE_URL` with proxied session JWTs return `401 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 `ozoneRequest` helper handles `ExpiredToken` automatically — 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` (not `create` / `negate`), and `negateLabelVals` is required — send `[]` when negating nothing. `createdBy` is a TOP-LEVEL body property (old lexicon), not inside the event object. Comments must be a separate `modEventComment` event.
- **Verify labels via queryEvents, not queryLabels:** `queryStatuses.labels` is empty even when labels applied, and `queryLabels` is BROKEN on this deployment as of 2026-08-27 (501 via service URL, `MethodNotImplemented` via PDS proxy). Confirm subject DID, `createLabelVals`, and `createdBy` on `queryEvents?types=tools.ozone.moderation.defs#modEventLabel` events instead.
- **batchId is optional but recommended:** Makes audit trails traceable — group related operations under one UUID. Note `batchId` inside the event body is silently dropped (not in the lexicon); persist it via top-level `modTool.meta.batchId`.
- **ALWAYS resolve actor DIDs via `com.atproto.identity.resolveHandle` immediately 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.

