Notion API automation: summaries and tags for a database with Claude

A Python script reads Notion database entries, gets a summary and tags from Claude and writes them to page properties. Covers data sources and Notion API limits.

5 min · · автоматизация · development

Notes, meeting minutes and incoming requests pile up in Notion quickly, and sorting them by hand takes time. Notion API automation fixes that: a script takes new database entries, sends the text to Claude and writes back a short summary and tags. Below is a working Python script using the current notion-client, which works with databases through data sources.

What you need

  • Python 3.10+ and packages:
bash
pip install notion-client openai python-dotenv
  • A Notion token. According to the Notion documentation there are two options: an internal connection, a separate bot user that must be given access to the database, or a personal access token that acts with your own permissions. Both are created in the Notion Developer portal. For team automations the documentation recommends an internal connection.
  • A Zukko key of the form fetch_... from the dashboard.

If you use an internal connection, open the database in Notion and grant the connection access to it: otherwise the API returns "not found" even with a correct ID. Access to a parent page is inherited by its children.

Preparing the database

In the example the database has these properties:

PropertyTypePurpose
NameTitleentry title
StatusSelectvalues To process and Processed
SummaryTextthe summary goes here
TagsMulti-selecttags go here

If your status column is of type Status (not Select), replace the "select" key with "status" in the code, both in the filter and in the update.

The database ID comes from its address: the 32 characters before ?v=. It's easier to pass the full address to the script: extract_database_id from notion_client.helpers pulls the ID out itself.

The .env file:

NOTION_TOKEN=ntn_... NOTION_DATABASE=https://www.notion.so/workspace/Inbox-0123456789abcdef0123456789abcdef?v=... ZUKKO_API_KEY=fetch_... ZUKKO_MODEL=claude-sonnet-5

Notion API automation: databases and data sources

Since Notion API version 2025-09-03, a database can have several data sources, and entries are queried from a data source rather than the database: POST /v1/data_sources/{data_source_id}/query. The old databases.query method is gone from the current notion-client (the 3.x line), so code from older examples fails with AttributeError.

The order, as in the upgrade guide:

  1. notion.databases.retrieve(database_id=...) returns the database with a data_sources list.
  2. Take the id of the data source you need (a regular database has one).
  3. notion.data_sources.query(data_source_id=..., filter=...) returns the pages.

Notion responses are paginated (has_more, next_cursor). The collect_paginated_api helper from notion_client.helpers walks all result pages for you.

Page text

Page content is made of blocks: notion.blocks.children.list(block_id=page_id). For text blocks (paragraphs, headings, list items, quotes) the text sits in rich_text, an array of fragments. We join the plain_text of all fragments, not just the first; otherwise everything after the first link or bold text is cut off.

This code doesn't walk nested blocks (toggle contents, nested lists). If they matter, call blocks.children.list recursively for blocks with has_children: true.

Full script

bash
import json
import os

from dotenv import load_dotenv
from notion_client import Client
from notion_client.helpers import collect_paginated_api, extract_database_id
from openai import APIStatusError, OpenAI

load_dotenv()

notion = Client(auth=os.environ["NOTION_TOKEN"])
llm = OpenAI(base_url="https://api.zukko.pro/v1", api_key=os.environ["ZUKKO_API_KEY"])

DATABASE_ID = extract_database_id(os.environ["NOTION_DATABASE"])

A few decisions in the code:

  • One request instead of two. The summary and tags come in a single JSON answer: half the requests and no resending of the same text.
  • Length cap. MAX_CHARS keeps a huge page from being sent to the model in full. The exact token count is in the usage field of the response or the token counting endpoint.
  • Notion limits. Notion limits request rate (per the documentation, an average of 3 requests per second per connection on most plans) and the length of a text fragment in a property (2000 characters). The current notion-client retries automatically after a 429; the summary is cut to 2000 characters.
  • Commas in tags. Notion doesn't accept commas in multi-select option names, so we replace them with spaces.

Running on a schedule

The script only processes entries with status To process and changes the status afterwards, so it's safe to run repeatedly. For example, cron once an hour:

bash
0 * * * * cd /opt/notion-bot && .venv/bin/python notion_summary.py >> run.log 2>&1

Scheduled GitHub Actions work too; then keep NOTION_TOKEN and ZUKKO_API_KEY in repository secrets.

Cost

Zukko prices per 1M tokens (input/output): claude-sonnet-5 $0.30 / $0.30, gpt-5.6-luna $0.10 / $0.10, glm-5.3-flash $0.04 / $0.04. The current list is at GET https://api.zukko.pro/v1/models or on the "Models" page in the dashboard.

Example: 100 notes, each 3,000 input tokens and 200 output tokens.

  • claude-sonnet-5: 100 × (3,000 + 200) × $0.30 / 1,000,000 = $0.096.
  • glm-5.3-flash: 100 × (3,000 + 200) × $0.04 / 1,000,000 = $0.0128.

Test which model gives suitable summaries and tags for your notes on your own tasks: run 20 entries through two models and compare. The task is simple, and a cheap model may turn out to be enough.

FAQ

The Notion API says "Could not find database"

Most often the connection has no access to the database: grant it from the database page menu or from the connection's access tab in the Developer portal. The second cause is a page ID instead of a database ID: for an inline database, open it as a full page and take the address from there.

Why doesn't notion.databases.query work?

notion-client 3.x doesn't have it: since API version 2025-09-03, entries are queried through a data source with notion.data_sources.query. The data source ID comes from notion.databases.retrieve.

The model sometimes returns text instead of JSON

The code skips such entries and leaves their status as To process, so they are processed again on the next run. To reduce errors, keep the prompt short and say "only JSON" explicitly.

Can I write the result into the page body instead of a property?

Yes, via notion.blocks.children.append(block_id=page_id, children=[...]) with a paragraph block. A property is more convenient for a summary because you can filter and sort the database by it. More on model requests in the chat completions reference.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

Notion API automation with the Claude AI model · Zukko