anytype 0.5.0

An ergonomic Anytype API client in rust
Documentation

anytype

An ergonomic Anytype API client in Rust.

release docs.rs crates.io

Home   |   Documentation   |   Examples

Overview

anytype provides a fluent Rust client for Anytype. It supports listing, search, and CRUD operations for objects, properties, spaces, tags, types, members, views, files, and chats, with credential storage and client-side caching. REST is preferred when it has equivalent functionality; gRPC supplies capabilities that REST does not expose or represents with less fidelity.

HTTP calls require an access token. gRPC calls require an account key or session token. The library can generate and store both credential families in a KeyStore.

Call AnytypeError::is_authentication() when an embedding application needs stable authentication guidance. The predicate recognizes direct HTTP and configuration failures plus structurally typed nested gRPC authentication failures without exposing or parsing response messages, URLs, or credentials, and callers do not need a direct anytype-rpc dependency.

The first grpc_client() call selects a nonempty stored session token before falling back to an account key and initializes one cached channel. Concurrent first callers share that initialization. find_grpc() discovers a local Anytype listener on Linux and macOS by filtering lsof listeners and probing candidate ports in order. Each candidate gets one two-second local budget for both connection and the unauthenticated AppGetVersion probe; unsupported platforms and unavailable discovery return None.

Features

  • Broad coverage of the Anytype REST API 2025-11-08: nearly every documented REST operation is called directly over HTTP (see Status and Compatibility for the known exceptions)
  • gRPC back end provides rich file operations, structured chat messages and streams, typed body blocks, archived-object cleanup, space backup, and process watching
  • Paginated responses and async Streams
  • Integrates with OS Keyring for secure storage of credentials (HTTP + gRPC)
  • HTTP middleware with secret-safe metadata logging, retries, and rate limit handling
  • Client-side caching (spaces, properties, types)
  • Space administration through typed APIs for chat-space creation, deletion, invitations, and sharing controls
  • Deterministic name and ID resolution for spaces, types, templates, chats, views, properties, and tags, with bounded scans and actionable ambiguity errors
  • Typed, bounded body-block reads (body module): validated block trees with exact IDs and order over gRPC ObjectShow, plus verified typed create, append, update, delete, move, and bounded non-transactional batch operations
  • Nested filter expression builder
  • Parameter validation
  • Metrics
  • Used by anyr for Anytype automation from the command line and any-edit for editing Anytype documents as Markdown

Numeric filters support eq, ne, lt, lte, gt, and gte; checkbox filters support eq and ne. Typed values pass through unchanged in search expressions and become canonical number text or lowercase boolean text only where a list endpoint requires URL query values. The client does not coerce strings to numbers or booleans, accept checkbox 1/0 aliases, or emulate server filtering after pagination. When typed list filters include one positive type filter, the client maps it to search's dedicated type selector instead of sending it as a generic property condition.

REST model fidelity

Type, Property, Tag, and Member retain the REST response's object discriminator. Responses that omit it use the model's expected discriminator for compatibility, while an observed value is preserved. Member.icon uses the typed Icon model.

gRPC file details accept an integral numeric addedDate as Unix seconds as well as the established RFC 3339 string form. FileObject::target_object_id is populated only from targetObjectId. createdInContext remains upload context.

Bounded HTTP responses

Buffered REST responses have finite byte ceilings. Ordinary JSON defaults to 8 MiB, single-object/document JSON to 64 MiB, bounded error bodies to 64 KiB, and raw file downloads to a separate 256 MiB policy. Truthful oversized Content-Length responses are rejected before their body is read; responses without a usable length are stopped at the first byte over the ceiling. SSE chat events remain incremental rather than buffered as JSON, but each pending event (including its delimiter) has a separate 1 MiB default ceiling. Incoming transport chunks are consumed without copying them into the event buffer, and one chunk may contain several independently bounded events. Overflow terminates the stream with AnytypeError::ChatSseEventTooLarge before the one-over byte is appended. Stream space and chat IDs are validated as path-safe before URL construction or diagnostic logging.

Applications can lower or raise the defaults within the library's hard maxima. An individual object read can choose a smaller ceiling but cannot exceed the configured document allowance:

use anytype::prelude::*;

# async fn example() -> Result<(), AnytypeError> {
let config = ClientConfig {
    response_limits: ResponseLimits {
        json_bytes: 4 * 1024 * 1024,
        document_bytes: 24 * 1024 * 1024,
        error_bytes: 32 * 1024,
        file_bytes: 128 * 1024 * 1024,
        chat_sse_event_bytes: 512 * 1024,
    },
    ..ClientConfig::default()
};
let client = AnytypeClient::with_config(config)?;
let object = client
    .object("space-id", "object-id")
    .response_limit_bytes(12 * 1024 * 1024)
    .get()
    .await?;
# let _ = object;
# Ok(())
# }

The 64 MiB document default accommodates worst-case JSON escaping of a valid 10 MiB outgoing markdown body. The hard maxima are 64 MiB for ordinary and document JSON and chat SSE events, 1 MiB for error bodies, and 1 GiB for raw files. AnytypeError::ResponseTooLarge contains only the selected ceiling and optional declared length; it never retains a response body, URL, request payload, or credential.

Retry safety

Automatic response, rate-limit, and transport retries are restricted to HTTP reads: GET, HEAD, and OPTIONS. POST, PATCH, mutation DELETE, and PUT without documented endpoint-specific replay approval are sent exactly once. A logical deadline or transport failure after dispatch returns an indeterminate mutation outcome. A 408, 429, 504, or other server failure is also indeterminate because the server may have applied the write before returning or losing the response. Observe fresh server state before deciding whether to retry. Connection-establishment and request-construction failures occur before any possible dispatch, keep their typed transport error and reqwest source, and are safe to retry immediately.

The client disables reqwest's lower-level retry and redirect handling so every additional send passes through this method-aware policy and its metrics. A 3xx response is returned as an API error without forwarding the bearer credential or request body to the Location. Consequently, redirect or retry policies set on a ClientBuilder passed to AnytypeClient::with_client are intentionally overridden; timeout, proxy, DNS, TLS, and user-agent customization is retained.

ClientConfig::rate_limit_max_retries continues to control consecutive 429 retries for replay-safe requests; zero disables that rate-limit-specific cap. Independently, one cumulative ceiling permits at most six physical attempts across 429, retryable-status, and connection failures, and the counter never resets when the failure class changes. Caller transport timeouts terminate the logical request so the shorter caller boundary wins. Retry count does not opt mutation requests into replay. HTTP metrics expose independent logical_operations and physical_attempts counters; the existing total_requests field retains its physical-request meaning.

HTTP deadlines

Each REST request has one absolute logical deadline that covers every physical send, retry wait, rate-limit delay, response header, buffered body, and JSON decode. Ordinary requests default to 120 seconds. File and multipart requests default to 600 seconds. Each paginated page receives a fresh ordinary deadline. AnytypeClient::with_config also installs a fixed 30-second connection timeout. AnytypeClient::with_client preserves the caller's connection and request timeouts while applying the logical policy.

Set an explicit policy when an embedding application owns different request or stream boundaries:

use anytype::prelude::*;
use std::time::Duration;

# fn client() -> Result<AnytypeClient, AnytypeError> {
let policy = HttpTimeoutPolicy {
    standard_operation: Some(Duration::from_secs(60)),
    long_operation: Some(Duration::from_secs(900)),
    sse_open: Some(Duration::from_secs(60)),
    sse_error_body: Some(Duration::from_secs(30)),
    sse_idle: Some(Duration::from_secs(90)),
    sse_total_lifetime: None,
};
let config = ClientConfig::default().http_timeouts(policy);
AnytypeClient::with_config(config)
# }

Finite values range from one through 3,600 seconds. None in an explicit policy disables that boundary. Without an explicit policy, ANYTYPE_HTTP_TIMEOUT_SECS=1..3600 replaces the four buffered/open defaults with one value, while 0 disables those four logical boundaries. Malformed, non-Unicode, signed, whitespace-bearing, overflowed, and larger values reject client construction. The environment never enables established SSE idle or lifetime limits.

Successful SSE headers disarm the open deadline before the response body is returned. Non-success bodies receive a fresh error-body deadline. Established streams have no idle or total-lifetime deadline by default; when configured, any nonempty transport chunk resets the idle timer and the lifetime timer never resets. AnytypeError::HttpTimeout and error.diagnostic() report a closed class and outcome, sanitized method and path, elapsed time, and physical attempt count. Timeout metrics are available through http_metrics().timeout, transport_timeouts, and timeout_outcome_count.

http_credential_generation() exposes only a monotonic process-local number. It advances whenever the in-memory HTTP key is set or cleared, allowing principal-bound caches to invalidate entries without reading, retaining, or hashing the credential itself. Credential replacement and generation advance share one synchronization boundary; no observer can see a mixed pair.

gRPC deadlines

ClientConfig::grpc_timeouts configures the logical gRPC policy used by the client's cached AnytypeGrpcClient; the fluent grpc_timeouts(...) builder sets an explicit policy. With no explicit policy, ANYTYPE_GRPC_TIMEOUT_SECS=1..3600 supplies one inherited credential, ordinary, long-operation, and stream-setup value, while 0 disables those four. The environment does not enable established-stream idle or lifetime limits and does not alter the five-second cleanup default. Without either setting, the defaults are 120 seconds for credential, ordinary, and stream setup, 30 minutes for long operations, and five seconds for cleanup.

An explicit policy ignores the environment. None disables an individual boundary; finite values are validated before keystore or network side effects. Credential, ordinary, setup, idle, and lifetime values may be at most one hour, long operations two hours, and cleanup 30 seconds. Invalid programmatic or environment policy rejects client construction with AnytypeError::Validation.

Ordinary and long reads return an aborted-read outcome on expiry. A mutation that may have been dispatched returns mutation_indeterminate; inspect fresh server state before deciding whether retry is safe. Cleanup uses its own short bound and is also indeterminate after possible dispatch. Runtime deadline and stream-control failures remain structurally available below AnytypeError::Grpc without placing peer status text in standard diagnostics. Deadline-service transport failures consume and discard the original error value after deriving a closed tonic status code. The generic error-type marker remains for source compatibility, while standard source traversal exposes only a synthetic status with that code and fixed redacted text.

Each request uses the earliest policy duration, absolute enclosing workflow deadline, or existing tighter grpc-timeout, including time spent waiting for service readiness. The library's StreamSetup profile stops at successful response headers and is deliberately not propagated as grpc-timeout, which tonic treats as a whole-stream limit. For this class, only an explicit caller whole-call timeout is propagated, reduced to its remaining absolute budget after readiness; the library setup and enclosing budgets remain local.

Applications that compose several client operations can call scope_grpc_deadline with one absolute Tokio instant. Every nested generated gRPC call observes the remaining budget and propagates only that remainder where the method profile permits it. Channel connection keeps its separate fixed 30-second boundary. The scope does not change the one-way dependency: callers use the public anytype API rather than depending on anytype-rpc directly.

Configured stream idle is reset by raw nonempty transport progress before message decoding; total lifetime and enclosing deadlines never reset. chat_stream keeps capped exponential reconnect backoff, resubscription, and watermark catch-up inside those bounds. Once a raw event has been decoded, its chat events enter a private pending queue and are delivered before an already-ready close, transport, or saturation boundary is handled. Output backpressure therefore does not discard them. An idle expiry can interrupt delivery; retained items drain first after reconnect, while a lifetime or enclosing expiry terminates the workflow. Watermarks advance only for delivered items. The reconnect-attempt counter resets after exactly two delivered decoded events. An interrupted control mutation is not replayed and may be indeterminate. ProcessWatcher logs only the status code for stream-read failures and numeric progress counters, not peer status text, process IDs, or progress messages.

grpc_client().client_commands() is deadline-aware. Callers that deliberately obtain the underlying raw channel() bypass these logical boundaries; use deadline_channel() when constructing another generated tonic client. The dependency direction is unchanged: anytype uses anytype-rpc, while anytype-rpc remains independent of this crate.

Secret-safe HTTP diagnostics

The library-owned HTTP diagnostics remain metadata-only at every RUST_LOG level. The anytype::http target reports stable error variants with an HTTP status, validated method, and bounded path-only context when available. anytype::http_json=trace adds request/response byte counts and query-field counts, but never logs request or response bodies.

No directive for those two HTTP targets enables query values, headers, full URLs, bearer tokens, credential-bearing URL components, or Anytype document content. This guarantee is HTTP-specific: other anytype tracing targets are outside its scope, so applications enabling them need an appropriate filter.

Standard AnytypeError Display and Debug output and its error source chain exclude all free-form messages, identities, candidate values, last errors, paths from malformed targets, and typed upstream sources that could contain request or document content. Use error.diagnostic() for structured application logs. Raw public fields, including ApiError::message, RateLimitExceeded::header, validation messages, resolver identities, and typed sources, remain available through explicit variant matching and must not be logged without an application policy.

Quick start

use anytype::prelude::*;

# async fn example() -> Result<(), AnytypeError> {
let client = AnytypeClient::new("my-app")?;
let spaces = client.spaces().list().await?;
let Some(space) = spaces.iter().next() else {
    return Ok(());
};

let page = client
    .new_object(&space.id, "page")
    .name("Meeting notes")
    .body("# Decisions")
    .create()
    .await?;

let results = client
    .search_in(&space.id)
    .text("meeting notes")
    .types(["page", "note"])
    .sort_desc("last_modified_date")
    .limit(10)
    .execute()
    .await?;
for object in results.iter() {
    println!("{}", object.name.as_deref().unwrap_or("(unnamed)"));
}

client.object(&space.id, &page.id).delete().await?;
# Ok(())
# }

Search pagination limits must be between 1 and 1000 inclusive. Both global and space-scoped search reject 0 or larger values with AnytypeError::Validation before sending an HTTP request.

See the Examples folder for more code samples.

Universal object links are constructed locally and do not call Heart's retired ObjectShareByLink RPC. Use object.get_link() for an object returned by the API, or client.get_share_link(space_id, object_id)? when both validated IDs are already known. object.get_link_shared(cid, key)? adds an existing space invite to the link.

For soft-delete workflows that reconcile uncertain responses themselves, client.object(space_id, object_id).delete_once() sends exactly one HTTP request attempt. Ordinary delete() retains the client's replay-safe DELETE retry policy.

Anytype's canonical Markdown read representation is not always safe to send back unchanged: for example, a literal underscore in a plain line is returned escaped. objects::plain_markdown_representation provides separate wire() and canonical() forms for the deliberately closed subset of empty bodies and single plain lines containing alphanumeric characters, internal ASCII spaces, and underscores. It accepts either raw or already-canonical values and is idempotent on replay. It returns None for punctuation, multiline Markdown, and ambiguous backslash forms; callers must reject or separately verify those forms rather than guess at Markdown equivalence or blindly replay export bytes.

The ignored disposable-space matrix in tests/test_markdown_fidelity.rs characterizes the current server's narrower export/replacement behavior with two stable REST reads and two fresh ObjectShow reads on each side of an exact exported-Markdown replacement. Representative headings, bullet/numbered lists, checkboxes, a one-line quote, a link, Unicode, and multiline paragraphs retain byte-identical exports. Consecutive quote lines, fenced code, tables, literal underscores, and explicit backslash escapes drift at the byte and typed-block-content levels; they have no replay-stability contract. Every tested PATCH also replaces block IDs, even when exported bytes stay identical, so exported-Markdown replacement never promises block identity. The matrix currently establishes no intermediate typed-semantic-only cohort.

Archived Object Cleanup

use anytype::prelude::*;

# async fn example(client: &AnytypeClient, space_id: &str) -> Result<(), AnytypeError> {
let count = client.count_archived(space_id).await?;
println!("archived before delete: {count}");

// Use a page budget when exhaustive work is not acceptable. The budget
// includes the empty continuation probe needed to prove an exact full page.
let bounded_count = client.count_archived_bounded(space_id, 3).await?;
println!("exact archived count within three logical pages: {bounded_count}");

let deleted = client.delete_all_archived(space_id).await?;
println!("deleted archived objects: {deleted}");
# Ok(())
# }

count_archived retains its exhaustive behavior. count_archived_bounded returns a count only after proving exhaustion within max_pages; each logical page can make two gRPC requests while probing the supported archive-relation key, and an exact multiple of 500 rows needs one additional empty probe page. Offset scans assume archive membership and ordering remain stable for the duration of the count. The archived search adapter validates ID-only type metadata but cannot construct the complete key required by Type, so listed archived objects leave r#type unset instead of constructing a partial type.

Files

Simple uploads, byte downloads, and deletion use REST. File listing, search, metadata, preload, URL upload, and uploads with style/context options use gRPC.

let space_id = "space_id";
let file_id = "file_object_id";
let bytes = client.files().download_bytes(space_id, file_id).await?;
tokio::fs::write("/tmp/download", bytes).await?;

For image variants, byte ranges, cache validators, or response metadata, use the configurable request API. It preserves 206 Partial Content, 304 Not Modified, 412 Precondition Failed, and 416 Range Not Satisfiable statuses for the caller to handle:

let response = client
    .files()
    .download_request(space_id, file_id)
    .width(640)
    .byte_range(0, 4096)
    .response_limit_bytes(4097)
    .error_limit_bytes(64 * 1024)
    .header_evidence_limit_bytes(4096)
    .max_attempts(6)
    .if_none_match("\"cached-etag\"")
    .download()
    .await?;

println!("status: {}, type: {:?}", response.status, response.metadata.content_type);

These controls are per request: they never widen or mutate the configured global response limits. Successful GETs require one canonical Content-Length that matches the buffered body. Partial responses additionally require one canonical Content-Range consistent with the requested range and body. Content-Type, ETag, Last-Modified, and Accept-Ranges are parsed and validated; duplicates, non-UTF-8 values, contradictions, truncation, and allowlisted header evidence over the selected ceiling fail with typed, secret-safe errors. The header ceiling is checked independently before body or retry processing on every physical response, including intermediate 429 and retryable-status responses. The attempt ceiling is cumulative across 429, retryable status, and connection replays.

Use files().metadata(space_id, file_id) for a simple HEAD request. File deletion moves the object to the bin by default; permanent deletion is explicit:

client
    .files()
    .delete_request(space_id, file_id)
    .permanently()
    .delete()
    .await?;

Server compatibility, verified against anytype-cli 0.3.6 (API 2025-11-08): the file endpoint advertises Accept-Ranges: bytes and returns 206, 412, and 416, but supplies neither ETag nor Last-Modified, so 304 Not Modified cannot be triggered there. File requests use the 600-second long-operation deadline by default; permanent deletion's independent live regression guard remains 180 seconds.

files().upload(space).from_path(path).upload() selects REST for a simple path upload and returns a normalized FileObject. Adding file_type, style, details, or creation-context options selects the richer gRPC upload.

Callers that already hold an authorized asynchronous reader can stream it without reopening a path or buffering the complete payload:

let file = tokio::fs::File::from_std(opened_file);
let uploaded = client
    .files()
    .upload(space_id)
    .reader("report.bin", file, exact_length)
    .mime("application/octet-stream")
    .multipart_limit_bytes(exact_length + 1024 * 1024)
    .upload()
    .await?;

The declared length participates in the complete multipart ceiling. Reader uploads fail if the source ends early or yields an extra byte, use REST, and reject gRPC-only rich options.

REST uploads can apply request-local ceilings without changing the client configuration:

let file = client
    .files()
    .upload(space_id)
    .bytes("report.txt", b"bounded bytes".to_vec())
    .mime("text/plain")
    .multipart_limit_bytes(71_680)
    .response_limit_bytes(65_536)
    .error_limit_bytes(65_536)
    .upload()
    .await?;

The multipart ceiling includes the complete boundary and part headers and is checked before authentication or network I/O. The successful and error-body ceilings are independent, and the REST upload POST is sent at most once.

Call resolve_space_id_bounded(reference, page_limit) when a workflow needs a request-local ceiling on every name-resolution page. Stable space IDs still return without I/O; names retain the normal finite scan and ambiguity rules.

files().preload(space) accepts either from_path(path) or from_url(url) as its source and always runs over gRPC, returning the preload file id.

Attached Discussions (REST + gRPC)

Pages and notes can own one derived discussion object. This is not an ordinary space chat: scope begins with the exact parent, and successful discovery proves the derived object's space, discussion smart-block type, discussion layout, and deterministic discussion-<parent_id> unique key.

use anytype::prelude::*;

# async fn example(client: &AnytypeClient) -> Result<(), AnytypeError> {
let current = client
    .attached_discussion("space_id", "parent_object_id")
    .get()
    .await?;

if current.discussion_id().is_none() {
    let attached = client
        .attached_discussion("space_id", "parent_object_id")
        .ensure()
        .await?;
    println!("{}", attached.discussion_id().unwrap_or_default());
}
# Ok(())
# }

get returns the closed AttachedDiscussion::Absent or AttachedDiscussion::Attached state after a cache-independent REST parent preflight and bounded gRPC reads. The exact REST wire requires an explicit layout, and only Basic- and Note-layout parents are accepted. ensure reads first and never calls the upstream attachment RPC for an already attached parent. When absent, it dispatches at most one mutation and then rereads the parent and independently verifies the derived discussion; transport errors, malformed evidence, and an unconfirmed final state are not retried. Once dispatch begins, reconciliation continues in an owned task even if the caller cancels its future. Each gRPC call has a finite deadline capped at five seconds, the whole operation has a caller-adjustable absolute deadline capped at thirty seconds. A show that returned a usable view or has an indeterminate dispatch outcome owns a separate bounded close; a definitive pre-acceptance authentication or permission rejection returns directly without manufacturing a close that could mask the original error. The total budget reserves time for each owned close and, once a write is admitted, for one fresh reconciliation read.

AttachedDiscussionErrorKind provides closed, payload-free classifications for unsupported layouts, malformed identity evidence, RPC and operation deadlines, cleanup failure, upstream failure, owned-task failure, and indeterminate mutation outcomes. gRPC unauthenticated and permission-denied statuses remain structural authentication errors without retaining status text. Use client.attached_discussion_metrics() to inspect cumulative parent GET, show, accepted-show, close, successful-close, write-dispatch, and reconciliation counters.

Chats

Space-scoped chat listing, creation, plain-message CRUD, lookup/search, reactions, read state, and per-chat SSE streams use REST:

use futures::StreamExt;

let chats = client.chats().in_space("space_id");
let page = chats
    .list()
    .filter(Filter::text_contains("name", "team"))
    .limit(20)
    .list()
    .await?;
let message_id = chats
    .add_message("chat_id", MessageContent::new().bold("Hello"))
    .send()
    .await?;
let first_history = chats.older_messages("chat_id").limit(8).get().await?;
if let Some(before) = first_history.next_before {
    let older = chats
        .older_messages("chat_id")
        .before(before)
        .limit(8)
        .get()
        .await?;
    println!("{} older messages", older.messages.len());
}
let edit = chats
    .edit_message(
        "chat_id",
        &message_id,
        MessageContent::new().italic("Edited"),
    )
    .send_verified()
    .await?;
assert!(edit.after.modified_at > edit.before.modified_at);
let mut events = chats
    .message_stream("chat_id")
    .limit(20)
    .heartbeat_seconds(15)
    .open()
    .await?;
while let Some(event) = events.next().await {
    if let ChatHttpEvent::MessageAdded { message } = event? {
        println!("{}", message.content.text);
    }
}

Structured message blocks, full-fidelity reads, cross-chat previews, reconnect watermarks, and dynamic subscription control remain available as gRPC extensions because the REST representation omits blocks and per-user state.

ChatClient::read_all_account is account-global. Heart's ChatReadAll request has no space or chat field, and its handler traverses every chat known to the current session. Only run this operation when the account's complete chat inventory is safe to mark read. The deprecated read_all(space_id) form validates its argument but does not send it on the wire. A separate live tier runs the global mutation alone against a fresh account and tears down its server process tree afterward.

Older REST history uses a typed page with a 1 through 12 item limit. Its next_before value is an equality-only opaque server token limited to 256 ASCII graphic bytes. Pass it only to the next older_messages request; do not parse or sort it. Each returned window preserves Heart's oldest-to-newest order, while continuation moves to an older window. Message timestamps fail closed when the server value is out of range and format canonically with UTC millisecond precision through canonical_chat_timestamp. send_verified performs GET, PATCH, and an independent GET and fails when the supported edit does not strictly advance modified_at.

Rich Chat Streaming (gRPC)

use anytype::prelude::*;
use futures::StreamExt;

// print chat messages as they arrive
async fn follow_chat(client: AnytypeClient, chat_obj_id: &str) -> Result<(), AnytypeError> {
    let ChatStreamHandle { mut events, .. } = client
        .chat_stream()
        .subscribe_chat(chat_obj_id)
        .build();

    while let Some(event) = events.next().await {
        if let ChatEvent::MessageAdded { chat_id, message } = event {
            println!("[{chat_id}] {}: {}", message.creator, message.content.text);
        }
    }
    Ok(())
}

Body Blocks (gRPC)

The body module reads the rich body of an object (paragraphs, headings, lists, callouts, tables, bookmarks, LaTeX/Mermaid/YouTube embeds) as a typed, bounded tree with exact block IDs and exact child order:

use anytype::prelude::*;

async fn print_body(client: &AnytypeClient) -> Result<(), AnytypeError> {
    let snapshot = client.blocks().body("space_id", "object_id").fetch().await?;
    for block in snapshot.iter() {
        if let BlockContent::Text(text) = &block.content {
            println!("{:?}: {}", text.style, text.text);
        }
    }
    Ok(())
}

Reads are fail-closed: duplicate, cyclic, orphaned, dangling, oversized, or malformed block graphs fail whole with a typed AnytypeError::BodyGraph error. A partial or truncated tree is never returned. Per-request BodyLimits can tighten (never widen) the hard ceilings on block count, depth, fanout, text size, and mark count. Content the typed layer does not model (dataviews, widgets, unknown styles or marks from newer servers) reads as an explicit Unsupported marker carrying only a content-free structural summary, so trees from newer hearts stay complete, ordered, and honest. Every possibly accepted ObjectShow owns bounded cleanup established before the show is polled. A complete foreground ObjectClose is required for success; cancellation or drop may start at most one bounded fallback close on the current Tokio runtime. Cleanup failure takes precedence over the show or application response.

BodyRpcConfig supplies one absolute deadline, a per-RPC timeout, decoder limits, and a cloneable BodyRpcMetrics observer. ObjectShow is capped at 4,194,304 decoded bytes and every mutation and close response at 65,536 bytes; callers may tighten but never raise those limits. Reuse one configuration for the body read and editor when a workflow needs one deadline and one exact set of payload-free counters:

use std::time::Duration;
use anytype::prelude::*;

async fn append_with_one_budget(client: &AnytypeClient) -> Result<(), AnytypeError> {
    let rpc = BodyRpcConfig::for_timeout(Duration::from_secs(10));
    let snapshot = client
        .blocks()
        .body("space_id", "object_id")
        .rpc_config(rpc.clone())
        .fetch()
        .await?;
    snapshot
        .edit(client)
        .rpc_config(rpc.clone())
        .append(NewBlock::paragraph("bounded write")?)
        .await?;
    assert_eq!(rpc.metrics().snapshot().write_polls, 1);
    Ok(())
}

The write counter advances immediately before the one write future is first polled. A zero counter therefore proves that validation, authentication, acquisition, deadline, or cancellation stopped the operation before dispatch. Higher-level workflows whose steps need independent absolute deadlines can attach clones of one BodyRpcMetrics observer with BodyRpcConfig::with_metrics; its snapshot accounts for every configured step without retaining payloads or identifiers. After it advances, transport failure, timeout, malformed or oversized response, cleanup failure, and exhausted verification are BodyMutationIndeterminate; callers must reread before considering a retry. Inline emoji marks and callout emoji are 1..64 UTF-8 bytes and control-free. Mark start and end values are independently validated as ordered, in-bounds UTF-16 offsets at Unicode scalar boundaries.

Downstream contract suites may opt into the disabled-by-default test-fixtures Cargo feature. It exposes narrow, production-validated typed snapshot constructors for exact block-count, read-restriction, and canonical-table boundary tests. The same feature provides a boolean-only keystore check that proves a test-owned byte buffer contains none of the configured HTTP or gRPC credential bytes without returning those credentials. It does not add deserialization or a general snapshot-forging API and must not be enabled by production dependents.

Downstream HTTP contract suites may instead opt into scripted-http-fixture. It provides a finite loopback HTTP script that records bounded method, path, and body bytes in arrival order. Each script has fixed request, header, path, body, and response ceilings; its errors and Debug implementations report only categories and sizes, leaving payload access explicit. This feature is also disabled by default and must not be enabled by production dependents.

Mutations start from a snapshot and accept only typed constructors and targets that belong to that snapshot. Every write is sent once, then a bounded fresh ObjectShow read must prove the exact ID, rich state, and sibling/parent position before success is returned:

use anytype::prelude::*;

async fn append_checked_item(
    client: &AnytypeClient,
    snapshot: &BodySnapshot,
) -> Result<BlockMutation, AnytypeError> {
    snapshot
        .edit(client)
        .append(NewBlock::checkbox("verified task", false)?)
        .await
}

apply_all is explicitly non-transactional: it returns verified receipts for the completed prefix, the first failure, and the untouched suffix. Timeout, transport uncertainty, or verification exhaustion returns BodyMutationIndeterminate with the last complete snapshot when available; callers must reread before retrying. Bookmark creation has an SSRF-safe policy: it validates and stores an unfetched absolute HTTP(S) URL but never invokes the server's URL-fetch RPC. YouTube embeds accept only canonical-izable HTTPS youtube.com/youtu.be video URLs. Divider style and the complete link-card appearance (card style, icon size, description mode, and bounded relation-key list) are typed updates. System singleton, file, table-structural, unsupported, and operation-restricted targets are rejected before dispatch. That fail-closed anchor policy also applies to a sibling target's parent and the existing first child used to encode a first-child insertion. Verified table creation proves the canonical ordered columns/rows layout regions, direct column and row membership, dimensions, exact first-row header state, and Heart's sparse initial cells: no cells without a header, or one ordered empty paragraph leaf with grey background per column under the header row only. Missing, extra, misplaced, nonempty, nested, structurally typed, or noncanonical-presentation cells fail receipt verification; aggregate descendant counts are never accepted as table evidence.

Cache-independent Space Reads

Use client.space(space_id).get_direct() when an exact mutation preflight or read-after-write check must bypass the process space cache. It performs one scoped REST GET, rejects a response carrying a different space ID, and returns the exact result without reading or priming the cache.

Property and tag mutation builders also provide no_cache_refresh(). The default behavior continues to refresh a primed property cache, including all tag pages for select properties. The cache-independent mode performs no hidden tag reads after the write and instead invalidates that space's property cache; use property(...).get_direct() and an explicitly limited tags(...).limit(n) page for bounded semantic readback.

Space Description Updates

client.update_space(id) keeps three operations distinct. Not calling description(..) omits the field and leaves the description untouched; description("text") replaces it; clear_description() sends "description": "", the only wire form that clears on current servers (a JSON null is silently ignored upstream and is never sent). Servers always return description as a string: a cleared description and a never-set one both read back as Some(""), so callers should treat None and Some("") identically or use Space::description_text(), which maps both to None. The live test test_space_description keeps this normalization verified against a real server (anytype-cli v0.3.6, API 2025-11-08).

Type Property Classification (REST + gRPC)

Type.properties is the REST server's flattened visible list: featured properties appear before ordinary recommended properties, but the wire model does not expose the boundary and may omit system-featured definitions. Do not infer replaceability from list position or known property keys. Use the source-backed classification read when preparing or verifying an exact type property replacement:

use anytype::prelude::*;

# async fn example(client: &AnytypeClient) -> Result<(), AnytypeError> {
let properties = client
    .get_type("space_id", "type_id")
    .classify_properties()
    .await?;

for property in properties.replaceable() {
    println!("{} ({})", property.name, property.key);
}
# Ok(())
# }

The read does not inspect or prime the all-types or all-properties caches. It combines one cache-independent REST type GET with one gRPC ObjectShow of the same type and reconciles the REST definitions against Heart's authoritative recommendedFeaturedRelations and recommendedRelations source lists. The returned recommended list is the complete non-featured set replaced by UpdateTypeRequest::properties and cleared by clear_properties.

ObjectShow and its exact matching ObjectClose both carry tonic deadlines and outer timeouts. A close guard is armed before show dispatch; cancellation or timeout during either boundary starts at most one detached five-second close fallback. classify_properties() uses the five-second Show maximum, while classify_properties_with_deadline() accepts a nonzero Show budget of at most five seconds. Every explicit or detached close owns a fresh independent five-second window, even when a caller's readback budget has expired. Public counters expose Show, Close, fallback, and confirmed cleanup success/failure work without retaining payloads. Cleanup failures take precedence over Show response errors.

The source lists are capped at 1,000 combined links. Duplicate, overlapping, malformed, missing, extra, or cross-source-inconsistent evidence fails the whole read rather than truncating or guessing. The transports are not an atomic snapshot, so a concurrent edit or eventual-consistency window may require rereading. gRPC credentials are required. featured_ids preserves the exact source list; featured contains only definitions visible on the REST type. Hidden and file recommendation lists are separate Heart concepts and are not part of this replaceable-property model.

Members

List members with client.members(space_id).list() and read one exact member with client.member(space_id, member_id).get(). The exact-read builder accepts the REST API's object-shaped IDs, _participant IDs, and network identities; the value must remain a URL-unreserved path segment of at most 256 bytes.

Direct Collection Membership

Saved collection views can hide members through filters and pagination. Use observe_collection_membership when a workflow needs bounded evidence about one exact object in one exact manual collection:

use anytype::prelude::*;

# async fn example(client: &AnytypeClient) -> anytype::Result<()> {
let observation = client
    .observe_collection_membership("space-id", "collection-id", "object-id")
    .await?;
match observation.state {
    CollectionMembershipState::Present => println!("direct member"),
    CollectionMembershipState::Absent => println!("not a direct member"),
}
# Ok(())
# }

The read exact-checks the REST collection and object identities and rejects Set/query lists. It runs an independent unscoped exact-object query before the collection-scoped query; an absent result also requires the same unscoped proof afterward. This control/scoped/control sequence prevents a transient missing index row from being misreported as absence. Saved view filters and sorts are never consulted. Each app-global Heart subscription has a unique client-owned ID, a finite deadline, and cancellation-resilient bounded cleanup. Missing counters, malformed identities, cleanup failures, or incomplete control evidence return an error rather than Absent. After a mutation has been dispatched, callers must treat every such error as an indeterminate mutation outcome and perform a fresh read before deciding whether retry is safe.

Use collection_member_add when a workflow must add exactly one member and classify a completed HTTP rejection conservatively:

use anytype::prelude::*;

# async fn example(client: &AnytypeClient) -> anytype::Result<()> {
match client
    .collection_member_add("space-id", "collection-id", "object-id")
    .await?
{
    CollectionMemberAddOutcome::Acknowledged => {}
    CollectionMemberAddOutcome::Rejected { status } => eprintln!("HTTP {status}"),
    CollectionMemberAddOutcome::Indeterminate { status } => {
        eprintln!("HTTP {status}; observe membership before retrying")
    }
}
# Ok(())
# }

The method sends one POST attempt, never follows a redirect, and returns the exact completed non-success status without reading or exposing its response body. HTTP 408, 429, 504, and all server failures are indeterminate and require a fresh membership observation before retry. A transport failure, incomplete or oversized success response, or malformed success body remains an error for the same reason. view_add_objects remains the general multi-object API and does not provide this status-preserving contract.

Use collection_membership_page to enumerate the same canonical direct membership scope without consulting a selected or saved view:

use anytype::prelude::*;

# async fn example(client: &AnytypeClient) -> anytype::Result<()> {
let first = client
    .collection_membership_page("space-id", "collection-id", 20, None)
    .await?;
if let Some(next) = first.continuation {
    let second = client
        .collection_membership_page("space-id", "collection-id", 20, Some(next))
        .await?;
    println!("{} direct members so far", first.object_ids.len() + second.object_ids.len());
}
# Ok(())
# }

Public pages contain at most 61 validated 1..256-byte safe entity IDs in Heart's direct collection order. Collection scopes ignore an id sort, so the request carries no sort and the client preserves the returned order without post-sorting. Each page performs one cache-independent logical HTTP GET (one through six physical attempts through the shared no-seventh-send pipeline), one non-replayed Heart subscribe, and one foreground unsubscribe; an interrupted or failed cleanup can arm only one bounded drop fallback. A continuation reads one private overlap row to prove its prior boundary and total are unchanged, then discards that row. Real Heart offset windows report the complete total while leaving both relative counters at zero, so checked total/offset/row arithmetic determines whether another page exists. Changed totals or boundaries, overlap-only results, malformed or nonzero relative counters, unexpected dependencies, cleanup failure, and Set/query targets fail closed instead of producing an empty or truncated page. Separate pages are not snapshot-isolated; restart from the first page after concurrent membership changes.

client.collection_membership_metrics() returns cumulative, payload-free counters for validated direct-observer query phases, membership query rounds, subscribe attempts, foreground close attempts and successes, fallback close attempts, and collection add/remove dispatches. The observer count starts only after the exact REST collection and object identities pass validation, so a Set/query rejection can be distinguished from a canonical membership query. Cloned clients share the same counters; the snapshot never retains collection, object, or subscription identifiers.

Status and Compatibility

The crate targets the Anytype REST API dated 2025-11-08. Coverage is described in two parts, because the two transports do not cover the same ground:

objects(space).filter(...).list() keeps ordinary filters on the documented object-list endpoint. Requests containing number or checkbox filters use the space-scoped REST search endpoint internally, because the object-list query parser in anytype-cli 0.3.6 rejects those typed values after URL decoding. The public builder, AND composition, HTTP-only authentication, pagination, and archived-object behavior remain unchanged.

  • Direct REST coverage - operations the crate performs over HTTP against the documented REST surface. Nearly every documented operation is covered directly, including auth, spaces, types, properties, tags, objects, templates, views, members, search, basic file transfer (upload, byte download, metadata, ranges, conditional requests, delete), and space-scoped chats (list/create, plain message add/edit/get/list/search/delete, reactions, read state, and the single-chat Server-Sent Events stream). No exact percentage is published, because the upstream operation list changes with each Anytype release, and a few surfaces (such as cross-space chat discovery) are deliberately reached only through gRPC.
  • gRPC-equivalent coverage - capabilities reached through anytype-heart's gRPC service where REST has no operation or returns less information. These are additional coverage, not a substitute for a missing REST call, and they require gRPC credentials at runtime.

The current transport mapping - which method uses which transport, and why - is recorded in API surface.

Plus:

  • View Layouts (grid, kanban, calendar, gallery, graph) implemented in the desktop app but not in the api spec 2025-11-08.

  • gRPC back-end provides API extensions for features not available in the REST api:

    • File metadata, listing/search, preload, URL upload, and rich upload options.
    • Structured chat blocks, full-fidelity message reads, chat-object search, name resolution, cross-chat previews, and reconnecting subscriptions.
    • Exact featured versus replaceable type-property classification.

Apis not covered

The current Anytype http backend api does not provide access to some data in Anytype vaults.

  • Files Update: REST supports basic transfer; gRPC supplies richer file operations.
  • Chats and Messages Update: REST supports chat management and plain message operations; gRPC supplies structured messages and richer streams.
  • Blocks. Pages and other document-like objects can be exported as markdown, but markdown export is somewhat lossy, for example, in tables, markdown export preserves table layout, with bold and italic styling, but foreground and background colors are lost.
  • Relationships - only a subset of relation types are available in the REST api.

Cargo features

The crate has no default features (default = []), and there is no grpc Cargo feature. anytype-rpc is an unconditional dependency, so every gRPC-backed method is always compiled and callable; what a gRPC-backed method needs is gRPC credentials in the keystore at run time, not a build-time flag. Building with --no-default-features therefore changes nothing.

Both optional features are disabled by default and reserved for tests: test-fixtures exposes narrow typed snapshot constructors and a boolean-only credential-leak check, while scripted-http-fixture exposes the finite loopback HTTP script. Production dependents must not enable either feature.

Keystore

A Keystore stores authentication tokens for http and grpc endpoints. Various implementations store keys in memory, on disk, or in the OS Keyring

GrpcCredentials::from_cli_config reads account credentials from the Anytype CLI's default ~/.anytype/config.json, or from an explicit path, without storing them. A missing file is reported separately from malformed or unreadable configuration so account-bootstrap callers can fail safely.

See the keystore reference for backend selection, environment credentials, and encrypted file storage.

Known issues & Troubleshooting

See Troubleshooting

For keystore-related issues, see the keystore reference.

Eventual Consistency

Anytype servers have "eventual consistency" (This is a feature of practical distributed systems, not a bug!). How you might encounter this in your programs:

  • Create a new property and then immediately create a type with the property, and get an error that the property does not exist.
  • Create a new type and then create an object with the type, and get an error that the type does not exist.
  • Delete an object, then immediately search for it, and find it.

The amount of time needed for "settling" seems to be 1 second or less.

anytype can perform validation checks after creating objects (objects, types, properties, and spaces) to ensure they are present before create() returns. Since this verification can cause delays, it's opt-in. While there are some knobs you can tune to adjust backoff time and number of retries, the easiest way to add verification is to call ensure_available() before create for critical calls:

let obj = client.new_object("space_id", "page").name("Quick note").ensure_available().create().await?;

For mutation workflows that must confirm more than availability, use verify_semantic with a predicate over a freshly fetched value. It retries successful-but-stale values as well as transient not-found, transport, retry, and server failures. Verification always has both a wall-clock deadline and a validated nonzero attempt cap no larger than MAX_VERIFY_ATTEMPTS; legacy zero and oversized values safely clamp to that hard ceiling, and zero-delay configurations remain finite and cancellation-safe. Fetched values and upstream error text are never retained in the terminal verification timeout.

To enable verification for all new objects, types, and properties, add .ensure_available(VerifyConfig::default()) to the config when creating the client. Setting this in the client configuration is not recommended except for an environment like unit tests where you're hammering the server and need to get results immediately. If verification is enabled in the client config, it will be applied to all create calls, unless disabled on a per-call basis by using .no_verify():

let obj = client.new_object("space_id", "page").name("Quicker note").no_verify().create().await?;

Building

Requirements:

  • protoc (from the protobuf package) in your PATH. On macos, brew install protobuf
  • libgit2 in your library path.
cargo build

Testing

The maintained HTTP/gRPC coverage inventory separates direct unit and live coverage from cross-crate integration evidence and records the remaining blocked or deferred gaps.

Set environment flags for unit and integration tests. You'll also need a running Anytype server (CLI or desktop).

# HTTP endpoint. Default: http://127.0.0.1:31012
#    Headless cli default port is 31012. Desktop app uses port 31009
export ANYTYPE_URL=http://127.0.0.1:31012
# Set the same for ANYTYPE_TEST_URL
export ANYTYPE_TEST_URL=$ANYTYPE_URL
# optional: set keystore to custom path
export ANYTYPE_KEYSTORE=file:path=$HOME/.local/state/anytype-test-keys.db
# required: prefix for uniquely named, cleanup-owned integration-test spaces
export ANYTYPE_TEST_SPACE_PREFIX=xtest
# optional: enable debug logging. Default "info"
export RUST_LOG=
# optional: disable rate limits. If not disabled, tests will take longer to run
export ANYTYPE_DISABLE_RATE_LIMIT=1

Keystore modifiers use :key=value boundaries. Path values may contain a Windows drive colon or ordinary colons that are not followed by another modifier key and =.

Test helpers honor ANYTYPE_KEYSTORE when it is set and use the in-memory env keystore otherwise. Set the required HTTP and optional gRPC credentials in the environment when tests need authenticated server access. Each shared integration-test context creates a fresh space whose name starts with ANYTYPE_TEST_SPACE_PREFIX, then deletes that exact space after the test, including callback error and panic paths. Reserve the prefix for automated tests. A missing or invalid prefix fails setup before authentication with a configuration error; no ambient space-ID environment variable is consulted. The disposable-space recovery harness stores its ledgers in a private runtime directory: Unix ownership and permissions are verified from open handles. On Windows, the owner and every access-granting ACL entry must name the process user, LocalSystem, or Built-in Administrators. Links and reparse points fail closed. Unauthenticated control tests explicitly use unique empty temporary file keystores, so ambient env credentials cannot change their expected result.

Run smoke test

cargo test --test smoke_test -- --nocapture

Run all tests

cargo test -- --nocapture

When the real server's mutation rate limit remains enabled, use cargo test -- --test-threads=1 to keep the full live suite from flooding its shared endpoint. Pagination coverage owns a uniquely filtered, cleanup-tracked object cohort and does not depend on unrelated ambient-space objects. Space-creation requests validate a nonempty bounded name before HTTP; validation coverage never probes this rule by creating an untracked unnamed space. Empty-filter coverage likewise owns its expected object rather than depending on pre-existing content in the configured test space.

Integration tests require a running Anytype server and environment variables. See src/client.rs for details.

On anytype-cli 0.3.6, DELETE ...?skip_bin=true can take about 154 seconds to return 204 No Content. The permanent-delete live test keeps the request under a finite 180-second wall-clock ceiling, matching the CLI live-test command budget while still preventing an unresponsive endpoint from wedging the suite.

The crate no longer ships a semantic gRPC mock server. Successful gRPC behavior is covered with cleanup-owned resources against the configured real Anytype server. Protocol and reducer edge cases use scripted transport handlers or constructed values without pretending to implement Anytype semantics. Disconnect, latency, and other connection-fault scenarios require the reviewed external fault-injection harness and are not emulated by an in-process gRPC service.

Chat resolver integration tests create cleanup-owned chats and messages in a fresh prefix-authorized space on the configured real HTTP and gRPC endpoints. Supporting REST reads and the REST SSE test use the same disposable tier so the resolver and stream files remain runnable when the server has no ambient spaces. Broader pre-existing REST CRUD, search, reaction, and read-state cases remain in the ambient test_chats tier and are not part of the mock migration. Every created message is registered immediately, before stream waits or assertions, and the gRPC stream worker is shut down before teardown.

Body reader integration tests create cleanup-owned objects in a fresh prefix-authorized space on the configured real HTTP endpoint, then verify typed reads, ordering, close-safe repeat reads, tightened limits, and missing-object failures through the configured gRPC endpoint. The adjacent dataview test was not formerly mock-backed, but shares the disposable tier so the body test file does not require ambient inventory.

The required tier also creates a disposable collection and a source-backed Set, then proves both server-created views and their object listings without reading or registering ambient list objects. The Set fixture uses the authenticated Heart creation RPC because REST object creation cannot supply its internal source. Run every required case through the admitted serial driver:

test -n "${ANY_MCP_HEADLESS_ENV_FILE:-}"
test -r "$ANY_MCP_HEADLESS_ENV_FILE"
set -a
source "$ANY_MCP_HEADLESS_ENV_FILE"
set +a
test "${ANYTYPE_KEYSTORE:-}" = env
test -n "${ANYTYPE_KEYSTORE_SERVICE:-}"
test -n "${ANYTYPE_KEY_HTTP_TOKEN:-}"
test -n "${ANYTYPE_KEY_SESSION_TOKEN:-${ANYTYPE_KEY_ACCOUNT_KEY:-}}"
export ANYTYPE_DISPOSABLE_TEST_PROCESS=1
python3 anytype-api/scripts/run-live-gate.py required anytype-api/tests/live-gate-manifest.toml

The checked-in live-gate manifest assigns every ignored test to the required, manual soak, or excluded tier. The manual workflow selector can run either live tier or both. Verify that closed inventory without a server:

cargo test --locked -p anytype --test live_gate_manifest

The driver runs every admitted entry in its own process and rejects zero-test and skip results. The required tier uses a sync-isolated server. The small soak tier uses a connected disposable server because Heart's space-sharing command calls its coordinator service; every created resource remains cleanup-owned. Sharing enablement retries only Heart's definitive NO_SUCH_SPACE response while a newly REST-created space enters the administration service.

With the same protected environment loaded, reproduce the two focused Set/view entries exactly:

cargo test --locked -p anytype --test test_views test_views_list_collection_and_set -- --ignored --exact --test-threads=1 --nocapture
cargo test --locked -p anytype --test test_views test_view_list_objects_collection_and_set -- --ignored --exact --test-threads=1 --nocapture

Process watcher import-finish coverage uses a real Markdown import in the fresh cleanup-owned space created by with_disposable_space_context. The watcher subscribes and unsubscribes from the configured gRPC server, accepts empty-space fallback events only for import requests that explicitly enable the fallback, and applies fixed timeouts to every live stage. The test is ignored under ordinary runs because it requires a configured real server and explicit disposable-process admission. The real server may complete the ordinary import process before publishing the import-finish event; the test uses the same subscription for a second bounded wait and proves that no new process was correlated while observing that fallback.

Tests that need a custom collection can use the hidden TestContext::create_collection_type_fixture helper. Anytype's REST type create/update contract rejects collection layout, so this test-only helper uses the narrow heart RPC, registers the returned type for cleanup before any follow-up read, and verifies it through the ordinary scoped REST getter.

Tests must create the object through TestContext::create_collection_fixture; ordinary cleanup registration does not grant view-mutation authority. This helper accepts only a collection type owned by the context, takes a complete type-scoped pre-create snapshot, and atomically records its cleanup dispatch and exact (space, object, type) provenance. Any collision with an authoritative cleanup ID or existing private claim is rejected without changing either registry. TestContext::create_collection_view_fixture then requires that provenance, requires the REST object to retain the exact proven type ID, and cross-checks every REST-visible default-view field against the exact ObjectShow root and dataview block, clones the full proto, and issues one BlockDataviewViewCreate RPC. It requires exactly one matching view-set event, a distinct server-assigned ID, and complete nested-view equality before a finite exact two-view REST verification. Collection teardown owns the added view; there is no general view-create production API. TestContext::add_collection_name_filter_fixture may then add one exact-name filter only to that cleanup-owned view. It accepts initially unfiltered REST and ObjectShow evidence, sends one authenticated filter-add RPC, and requires the assigned filter ID and complete value to reread identically through both surfaces. Collection teardown owns the filter with the view; this remains test infrastructure, not a production view-filter API.

Representative Kanban tests can use TestContext::create_kanban_fixture inside with_disposable_space_context. The helper creates and immediately registers a custom card type, its Select grouping property and two status options, a collection, an existing server view converted to Kanban, and three cards. It adds the grouping relation through heart before setting the layout, rejects pre-existing filters, resolves Heart's internal relation key separately from the REST property key, and independently rereads the exact relation format, view grouping key, tags, membership, and card values. Membership verification uses two-item pages so pagination is exercised rather than bypassed. move_kanban_item_fixture performs an ordinary object Select-property update and requires the moved card and complete board to reread exactly. Missing or wrong-format relations, removed options, filtered views, malformed pagination, or unregistered resources fail closed. Collection deletion owns view cleanup; property cleanup owns its options.

Tests that need disposable spaces should use TestContext::create_space_fixture. It creates through the authenticated REST API after taking a complete bounded inventory whose pagination, IDs, names, and uniqueness are validated. A response is registered exactly once only when its valid ID was absent from that inventory, its name exactly matches the request, and it is a regular space distinct from the context space. The private registry retains that exact ID/name provenance. Untrusted or ambiguous responses are allowed to leak rather than authorize deletion of ambient state. Registration occurs before follow-up verification. Teardown revalidates exact ID/name/model provenance through the same strict inventory before Anytype's irreversible SpaceDelete RPC, then requires complete bounded REST evidence that the ID is gone even when the delete response is uncertain. The test-only ownership registry remains separate from the explicit AnytypeClient::delete_space API, which callers must protect with their own confirmation policy.

Whole live suites should prefer with_disposable_space_context. It creates a fresh cleanup-owned space under the mandatory ANYTYPE_TEST_SPACE_PREFIX. That ASCII prefix is an explicit authorization to delete every space whose current name starts with it, case-insensitively; reserve it exclusively for tests. Missing or invalid configuration returns a typed DisposableRun::Skipped before credential access or filesystem I/O. One same-host backend-wide file lease serializes participating runs. An owner-private durable ledger and disk-backed enumerate-before-delete offset-pagination plans recover interrupted matching runs without a count ceiling or an in-memory inventory. Each fixed pagination window shares one deadline; a changing total discards the plan and restarts at offset zero. New names use 128 bits of operating-system randomness. Readiness has a hard 20-second and 50-attempt budget. It resolves the exact @page key without a cache, then direct-GETs that returned type through the same validated space path and requires identical ID, page key, and non-archived state. A failure reports only its final closed stage/category and completed attempt count. Create failures likewise expose only a closed setup stage and category, distinguishing rejected or indeterminate requests from invalid ID, model, name, or ambient-identity evidence without rendering response values. The numeric/checkbox acceptance callback similarly reports only a closed fixture or comparison stage and a payload-free TestError/API diagnostic category, which proves whether execution crossed the callback boundary without exposing fixture identities, queries, endpoints, or upstream bodies. Its ignored compatibility matrix executes all eleven fixed cases independently on both endpoints even when an earlier check fails, then reports all 22 static endpoint/case pairs in canonical order with only their closed categories and validated HTTP status/classes when available. The regression assertion is evaluated only after disposable cleanup completes. All three diagnostic paths store exhaustive enums rather than caller-provided strings, so Display, Debug, and accessors can render only the documented closed vocabulary. The filter fixture resolves its prerequisite due_date property through the bounded, cache-independent property resolver because the disposable client intentionally disables cache state. The two immediate pre-delete checks and final absence proof also use cache-disabled direct exact-ID reads. The helper cleans registered children first and retains callback, cleanup, deletion, absence, ledger, and panic outcomes; an unproven absence is always dominant without discarding the original typed error or simultaneous cleanup evidence. Remote backends require an equivalent scheduler lease and are otherwise rejected. Operators must not create, rename, or delete spaces through another client while the helper holds its lease. Disposable runs require ANYTYPE_KEYSTORE=env, an explicit ANYTYPE_KEYSTORE_SERVICE, a nonempty ANYTYPE_KEY_HTTP_TOKEN, and at least one nonempty gRPC session token or account key. They must run in a dedicated single-threaded integration-test process admitted with ANYTYPE_DISPOSABLE_TEST_PROCESS=1; the process must not mutate its environment. File, keyring, implicit, unknown, malformed, and over-budget credential forms skip before authentication, private state, or mutation. The helper creates no credential file. For a spawned production child, call ctx.disposable_child_environment().unwrap().configure(&mut command) before spawn, then register an idempotent stop-and-wait handle with ctx.spawn_owned_child(...). Configuration uses env_clear, reconstructs only the approved endpoints, finite limits, MCP settings, and exact accepted credential names, and rechecks the whole environment/argument block budget. The helper records child-running state before invoking the spawn closure and stops all registered children before resource cleanup and space deletion. Recovery refuses every cleanup plan and prefix sweep while a prior ledger says its child may still run. The first refusal durably records that the operator must prove the child stopped or is gone. Only after that proof may the operator set ANYTYPE_DISPOSABLE_RECOVER_STOPPED_RUN to the exact recorded .json run handle for one invocation; the helper persists the stopped transition before applying that ledger's plan, and rejects stale or repeated confirmations. Destructive execution is enabled only where owner and owner-only permissions can be proved for the runtime directory and every recovery target. Unix opens and removes exact components relative to verified directory handles with no-follow semantics. Windows creates protected ACLs, admits only the process user, LocalSystem, or Built-in Administrators as owner and access-granting principals, and rejects reparse points before recovery I/O. Recovery files are flushed before publication; NTFS supplies directory-entry persistence because Windows rejects FlushFileBuffers on directory handles.

Tests that need templates can use the hidden TestContext::create_template_fixtures helper with one to sixteen source names. It creates a private custom type and source object for each requested template, invokes the authenticated heart template-from-object RPC exactly once per source, and verifies the returned IDs through a finite complete type-scoped list plus exact GETs. Complete bounded type, space-wide active/archived object, and global template inventories prove create responses did not reuse pre-existing IDs; the global inventory also proves the new template is owned only by the expected type, while list and GET generic-template identities must agree. The helper registers every created ID before classifying the RPC response or reading it back. Teardown issues each template, source, and type archive request once in reverse dependency order, then proves the templates absent and the sources and type archived. Production consumers do not gain a template mutation API.

License

Apache License, Version 2.0

Contributing

Feedback, Issues and Pull Requests are welcome.