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

Notion update_page failed

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.

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.

Appendix: 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)