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:
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:
| Property | Type | Purpose |
|---|---|---|
Name | Title | entry title |
Status | Select | values To process and Processed |
Summary | Text | the summary goes here |
Tags | Multi-select | tags 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:
notion.databases.retrieve(database_id=...)returns the database with adata_sourceslist.- Take the
idof the data source you need (a regular database has one). 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
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_CHARSkeeps a huge page from being sent to the model in full. The exact token count is in theusagefield 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-clientretries 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:
0 * * * * cd /opt/notion-bot && .venv/bin/python notion_summary.py >> run.log 2>&1Scheduled 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.
Related articles
- Translating subtitles with a neural network: SRT without broken timecodesThe main risk when translating SRT with a neural network is a broken file structure. A scheme where timecodes never reach the model, and a Python script with validation.
- Editing texts with a neural network: proofreading, style and cuttingDifferent kinds of editing need different prompts. Wording for proofreading, style and cutting, how to get edits with explanations and how not to lose the author's voice.
- A neural network for writing scripts: Bash, PowerShell and PythonA model writes scripts for routine fast, but running them blindly is risky. How to state the task, demand a safe dry-run mode and check the result before launch.
- Dify in Docker: installation, first-run setup and connecting modelsInstall Dify with Docker Compose, create the admin account and connect Claude, GPT and DeepSeek models on one key. Plus what to do when containers will not start.