A Python CLI for programmatically updating and publishing Notion AI Agent instructions, bypassing the public API's inability to access workflow-parented blocks.
Notion AI Agents store their instruction pages under parent_table: workflow — an internal type not exposed by the public Notion API. This means tools like Gemini, Claude, and external MCP integrations cannot read or write these pages through normal means.
This tool uses two internal endpoints discovered via browser network interception:
POST /api/v3/saveTransactionsFanout— edit block contentPOST /api/v3/publishCustomAgentVersion— deploy the updated agent
Auth is handled by reading your existing Firefox session cookie (token_v2) — no separate credentials needed.
- Python 3.11+
- Firefox with an active Notion session (logged in)
pyyaml:pip install pyyaml
-
Add your agents to
cli/agents.yaml(the Librarian is pre-populated).To find IDs for a new agent, run this in the Notion browser console while on the agent's instruction page:
fetch('/api/v3/getRecordValues', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({requests:[{id:'<page-id-from-url>',table:'block'}]}) }).then(r=>r.json()).then(d=>{ const v = d.results[0].value; console.log('parent_id (workflow_id):', v.parent_id); console.log('block_id:', v.id); console.log('space_id:', v.space_id); });
-
Run from the project root.
# Update instructions and publish
python cli/update_agent.py librarian path/to/instructions.md
# Dry-run — print payloads, no API calls
python cli/update_agent.py librarian path/to/instructions.md --dry-run
# Update content without publishing
python cli/update_agent.py librarian path/to/instructions.md --no-publish
# Re-publish without changing content
python cli/update_agent.py librarian --publish-only
# Dump current instructions as Markdown
python cli/update_agent.py librarian --dumpThis CLI is designed to be callable as a tool from AI agent frameworks:
# Example tool definition for an external agent
def update_notion_agent(agent_name: str, instructions_markdown: str) -> str:
import tempfile, subprocess
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
f.write(instructions_markdown)
tmp = f.name
result = subprocess.run(
['python', 'cli/update_agent.py', agent_name, tmp],
capture_output=True, text=True,
cwd='/home/sam/projects/notion-forge'
)
return result.stdout + result.stderrIf the agent is calling the notion-agents MCP server directly, prefer the
file-based update_agent_from_file tool for long instruction documents. The
Notion write path is the same, but the MCP request stays small instead of
embedding the full Markdown body inline.
Use lab_query for broad read-only Lab questions where a direct database query
would return a large payload. It runs on MiniMax M2.5
(fireworks-minimax-m2.5) and acts as a compressed, Exa-like query surface over
Notion. Its answers must preserve the canonical answer set: exact counts should
state exact totals, matched counts, scanned counts, or limits, and it must not
call a view/search subset a full database total.
For direct MCP database work, call describe_database before query_database
when property names or property types are uncertain. Use count_database with
exact=True for precise counts instead of inferring totals from a single
query_database page.
Lab dispatch returns are exposed as two MCP paths:
handle_final_return— normal execution-plane return ingestion when a dispatch packet andrun_idexist.direct_closeout_return— fallback closeout when no GitHub issue, dispatch packet, or trustedrun_idis available. It generates an idempotency key if needed, appends result blocks, and stampsReturn Received At/Return Consumed Atso the Intake Clerk can continue the pipeline.
The instruction file should be standard Markdown:
| Syntax | Block type |
|---|---|
# Heading |
H1 |
## Heading |
H2 |
### Heading |
H3 |
- item |
Bulleted list |
1. item |
Numbered list |
```code``` |
Code block |
> text |
Callout |
--- |
Divider |
| Plain text | Paragraph |
**bold** *italic* `code` |
Inline formatting |
token_v2 is a long-lived browser session token that grants full access to your Notion workspace. This tool reads it directly from Firefox's local SQLite store and uses it only for the API calls you trigger. It is never logged, stored, or transmitted elsewhere. If you are concerned, revoke the session in Notion's settings after use.
jamalex/notion-py(MIT) — cookie auth patternnotion-enhancer/api(MIT) — modernsaveTransactionsenvelope formatkjk/notionapi(BSD-2) — XHR intercept methodology