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

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

Error message

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

What it means

The Notion tool's update_page action sent PATCH https://api.notion.com/v1/pages/{page_id} and got a non-2xx response. The wrapped status and body excerpt identify the rejection cause, almost always a property payload that does not match the page's database schema or an archived page.

Source

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

        page_id: &str,
        properties: &serde_json::Value,
    ) -> anyhow::Result<serde_json::Value> {
        let url = format!("{NOTION_API_BASE}/pages/{page_id}");
        let body = json!({ "properties": properties });
        let resp = self
            .http
            .patch(&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 update_page failed ({status}): {truncated}");
        }
        resp.json().await.map_err(Into::into)
    }

    /// Search the Notion workspace by query string.
    async fn search(&self, query: &str) -> anyhow::Result<serde_json::Value> {
        let url = format!("{NOTION_API_BASE}/search");
        let body = json!({ "query": query });
        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() {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Match the body excerpt to the failing property name and fix its value type (select needs {select:{name}}, numbers need {number:n})
  2. If the page was archived, restore it in Notion or drop it from the update queue
  3. Re-read the page to refresh property names/types before building the update payload
  4. For 404, fix sharing; for 401, rotate the token

Example fix

// before ('Status' is a select property)
{"action":"update_page","page_id":"<id>","properties":{"Status":{"rich_text":[{"text":{"content":"Done"}}]}}}
// after
{"action":"update_page","page_id":"<id>","properties":{"Status":{"select":{"name":"Done"}}}}
Defensive patterns

Strategy: validation

Validate before calling

// Before updating, confirm the page is live and capture property types
let page = notion.execute(json!({"action":"read_page","page_id":id})).await?;
if page.get("archived") == Some(&json!(true)) { /* skip */ }

Type guard

fn is_editable_page(v: &serde_json::Value) -> bool {
    v.get("archived").and_then(|a| a.as_bool()) == Some(false)
}

Try / catch

Err(e) if e.to_string().starts_with("Notion update_page failed (400)") => {
    // parse body excerpt for property name, re-read schema, coerce type, retry once
}

Prevention

When it happens

Trigger: Sending a value with the wrong property type (e.g. rich_text for a select or number property), referencing a property that no longer exists, updating an archived/trashed page (400), page not shared with the integration (404), or invalid token (401).

Common situations: Sync jobs that push field updates into Notion break after schema drift: someone retyped a field from text to select, so the old payload shape now fails. Archived pages still referenced by stored ids are another frequent source.

Related errors


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