
Building an AI Agent That Writes to Notion: Schema Design & API Logic
A technical guide on integrating AI agents with Notion. Covers database schema design, API authentication, and the Python logic required to structure and save agent outputs automatically.
The UI Problem in AI Automation
Most developers get stuck in the chat interface trap. You build a capable agent, it generates a brilliant comprehensive analysis or a code snippet, and then... it sits in the terminal or a chat window until you copy-paste it.
That is not a system. That is a toy.
To build a real AI operating system, your agents need a persistent layer—a place to dump state, store results, and trigger human review. For many of us, Notion is that UI. It allows us to treat a database as a queue.
In this guide, we are going to build a Writeback Agent. This isn't just about sending text to a page; it's about designing a database schema that allows an AI to categorize its own work, update statuses, and structure data for your review.
Part 1: The Schema Strategy
Before writing a single line of Python, we need to design the target. If you dump everything into a blank page, you lose the ability to filter and sort. We need a Database.
Create a new Database in Notion (Table View) and set up the following properties. This specific schema is designed for a Content/Research Agent, but the logic applies to any domain.
The Database Properties
- Name (Title): The headline or subject of the task.
- Status (Select):
Inbox,Processing,Draft Ready,Published. This is crucial. It allows the agent to move cards across a Kanban board. - Topic (Multi-select): Tags like
Python,SaaS,Automation. - URL (URL): If the agent is summarizing a website, this stores the source.
- Confidence Score (Number): A metric (0-1) where the agent self-evaluates its output quality.
Note: Get the Database ID from the URL. It is the 32-character string between the workspace name and the query parameters (?v=...).
Part 2: The Notion API Payload
The Notion API is powerful but verbose. It treats every paragraph as a "Block" and every database cell as a "Property." You cannot simply send a string. You must send a structured JSON object.
Authentication Prerequisites
- Go to Notion Developers and create an Internal Integration.
- Copy the Internal Integration Secret (starts with
secret_). - Go to your Notion Database page, click the three dots > Connections > Add your new integration. (This is where 90% of builds fail—don't forget to share the page with the bot).
The Payload Structure
When creating a page, we hit the https://api.notion.com/v1/pages endpoint. The payload has two distinct parts:
- Properties: The metadata (Status, Tags, URL).
- Children: The actual page content (Headings, Paragraphs, Code Blocks).
Part 3: The Builder Script (Python)
Let's write the connector. We will use requests standard library. I prefer this over the notion-client SDK for simple agents because it offers more transparency into the JSON structure.
import requests
import json
from datetime import datetime
NOTION_TOKEN = "secret_your_token_here"
DATABASE_ID = "your_database_id_here"
headers = {
"Authorization": "Bearer " + NOTION_TOKEN,
"Content-Type": "application/json",
"Notion-Version": "2022-06-28"
}
def create_notion_entry(title, content_blocks, tags, source_url):
"""
Creates a new page in the database with defined schema and body content.
"""
create_url = "https://api.notion.com/v1/pages"
# 1. Map the Database Schema (Properties)
payload = {
"parent": {"database_id": DATABASE_ID},
"properties": {
"Name": {
"title": [
{
"text": {
"content": title
}
}
]
},
"Status": {
"select": {
"name": "Draft Ready" # Auto-setting status to Ready
}
},
"Topic": {
"multi_select": [{"name": tag} for tag in tags]
},
"URL": {
"url": source_url
}
},
# 2. Map the Page Body (Children Blocks)
"children": content_blocks
}
response = requests.post(create_url, headers=headers, json=payload)
if response.status_code == 200:
print(f"[SUCCESS] Page '{title}' created in Notion.")
return response.json()
else:
print(f"[ERROR] {response.status_code}: {response.text}")
return NoneFormatting the "Children" (Body Content)
The trickiest part of Notion automation is formatting the body content. You can't just pass the LLM string. You need a helper function to convert text into Notion Blocks.
def text_to_blocks(text_content):
"""
Splits raw text into Notion paragraph blocks.
Real-world usage: You would parse markdown headers here.
"""
blocks = []
# Split by double newlines to create distinct paragraphs
paragraphs = text_content.split("\n\n")
for p in paragraphs:
if len(p.strip()) > 0:
blocks.append({
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [
{
"type": "text",
"text": {
"content": p[:2000] # Notion block limit check
}
}
]
}
})
return blocksPart 4: Connecting the Agent
Now we integrate this with an LLM. Assuming you have a function generate_insight(topic) that calls OpenAI or Anthropic, here is how the pipeline flows.
We are not just generating text; we are generating structured data.
# Simulation of an Agent's Workflow
def run_agent_workflow(user_topic):
print(f"Agent working on: {user_topic}...")
# 1. LLM Generation (Mock)
# In prod, this is: response = openai.chat.completions.create(...)
llm_title = f"Deep Dive into {user_topic}"
llm_body = "Notion API is robust.\n\nIt allows for complex schema manipulation."
llm_tags = ["Tech", "Automation"]
# 2. Format content for Notion
formatted_blocks = text_to_blocks(llm_body)
# 3. Add a specialized block (e.g., a Callout for the Agent's thought process)
formatted_blocks.insert(0, {
"object": "block",
"type": "callout",
"callout": {
"rich_text": [{
"type": "text",
"text": {"content": "Generated by Agent v1.0"}
}],
"icon": {"emoji": "🤖"}
}
})
# 4. Writeback to DB
create_notion_entry(
title=llm_title,
content_blocks=formatted_blocks,
tags=llm_tags,
source_url="https://github.com/avnishtech"
)
run_agent_workflow("Notion Schema Design")Why This Matters
By mapping the agent's output to specific schema properties (Status, Tags), you are building a system where the AI does the heavy lifting of organization, not just creation.
The Status field is the most important component here. An agent can read a database looking for items marked Inbox, process them, and then rewrite them with the status Draft Ready. This creates a circular, autonomous loop.
Next Steps for Optimization
- Markdown Parsers: The helper function above is basic. For production, write a parser that converts Markdown headers (
#) into Notion H1/H2 blocks. - Error Handling: Notion has strict limits (2000 characters per text block). Your script must slice strings or the API will throw a 400 error.
- Latency: The Notion API is not instant. If processing bulk items, implement a
time.sleep(0.5)between requests to avoid rate limiting.
Build the schema first. The code follows the structure.
Comments
Loading comments...