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
- Read the status and body fragment in the message first: 401/403 = token problem, 404 = sharing problem, 400 = filter shape, 429 = rate limit
- For 404 object_not_found, open the database in Notion, use '...' > Connections, add your integration, then retry the same call
- For 401, regenerate the internal integration secret at notion.so/my-integrations and update the stored api_key
- 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
- 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
- Share every database/page the integration must see via Notion 'Connections' before first use
- Store the integration token in secrets config and rotate it on a schedule
- Validate filter payloads against a freshly read database schema instead of hardcoding
- Cache query results and rate-limit your own calls to stay under ~3 req/s
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
- Notion read_page failed ({status}): {truncated}
- Notion create_page failed ({status}): {truncated}
- Notion update_page failed ({status}): {truncated}
- Notion search failed ({status}): {truncated}
- Failed to initiate call: {}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/88799c232875764a.
Report an issue: GitHub.