zeroclaw-labs/zeroclaw · error · anyhow::Error

Notion query_database failed ({status}): {truncated}

Error message

Notion query_database failed ({status}): {truncated}

What it means

The Notion tool's query_database action sent POST https://api.notion.com/v1/databases/{database_id}/query (with a Bearer token and Notion-Version 2022-06-28 header) and Notion answered with a non-2xx HTTP status. The message embeds the status code plus up to 500 characters of Notion's JSON error body, which carries the real reason (unauthorized, object_not_found, validation_error, rate_limited).

Source

Thrown at crates/zeroclaw-tools/src/notion_tool.rs:77

        let url = format!("{NOTION_API_BASE}/databases/{database_id}/query");
        let mut body = json!({});
        if let Some(f) = filter {
            body["filter"] = f.clone();
        }
        let resp = self
            .http
            .post(&url)
            .headers(self.headers()?)
            .json(&body)
            .timeout(std::time::Duration::from_secs(NOTION_REQUEST_TIMEOUT_SECS))
            .send()
            .await?;
        let status = resp.status();
        if !status.is_success() {
            let text = resp.text().await.unwrap_or_default();
            let truncated =
                crate::util_helpers::truncate_with_ellipsis(&text, MAX_ERROR_BODY_CHARS);
            anyhow::bail!("Notion query_database failed ({status}): {truncated}");
        }
        resp.json().await.map_err(Into::into)
    }

    /// Read a single Notion page by ID.
    async fn read_page(&self, page_id: &str) -> anyhow::Result<serde_json::Value> {
        let url = format!("{NOTION_API_BASE}/pages/{page_id}");
        let resp = self
            .http
            .get(&url)
            .headers(self.headers()?)
            .timeout(std::time::Duration::from_secs(NOTION_REQUEST_TIMEOUT_SECS))
            .send()
            .await?;
        let status = resp.status();
        if !status.is_success() {
            let text = resp.text().await.unwrap_or_default();
            let truncated =

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Read the status and body fragment in the message first: 401/403 = token problem, 404 = sharing problem, 400 = filter shape, 429 = rate limit
  2. For 404 object_not_found, open the database in Notion, use '...' > Connections, add your integration, then retry the same call
  3. For 401, regenerate the internal integration secret at notion.so/my-integrations and update the stored api_key
  4. For 400 validation_error, strip the filter down to one property, verify the property name/type by reading the database first, then re-add clauses
  5. For 429, add exponential backoff between queries (roughly 3 requests/second ceiling per integration)

Example fix

// before (tool args)
{"action":"query_database","database_id":"my-tasks-db","filter":{"property":"Due","date":{"after":"2026-01-01"}}}
// 404 object_not_found -> share the database with the integration, or use the real id:
{"action":"query_database","database_id":"8a4f9c2e1234567890abcdef12345678","filter":{"property":"Due","date":{"after":"2026-01-01"}}}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before querying, sanity-check the database id shape (Notion ids are 32 hex chars)
fn valid_notion_id(id: &str) -> bool {
    let hex: String = id.chars().filter(|c| c != '-').collect();
    hex.len() == 32 && hex.chars().all(|c| c.is_ascii_hexdigit())
}

Try / catch

match tool.execute(args).await {
    Ok(out) => { /* ... */ }
    Err(e) => {
        let msg = e.to_string();
        if msg.starts_with("Notion query_database failed (429)") {
            tokio::time::sleep(backoff.next()).await; /* retry */
        } else if msg.starts_with("Notion query_database failed (404)") {
            // sharing problem: surface to operator, do not retry
        } else {
            return Err(e);
        }
    }
}

Prevention

When it happens

Trigger: Querying a database_id that is malformed, deleted, or not shared with the integration (404 object_not_found); a missing/invalid/expired integration token (401); a token without access to that workspace (403); a filter JSON that does not match the database's property names or types (400 validation_error); or exceeding Notion's ~3 requests/second per integration (429).

Common situations: The classic case: the integration was created in Notion but the database was never shared with it via '...' > Connections, so a database that visibly exists returns 404. Others: rotated token not updated in zeroclaw secrets config, filter built against stale property names after a schema change, and scripts looping queries until rate limited.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/88799c232875764a. Report an issue: GitHub.