A self-hosted automation daemon that extends Notion with custom logic — running on your own hardware, on your own terms, no subscription required.
Notion's built-in automations are powerful but gated behind paid plans and limited to predefined actions. This daemon lets you write your own automation rules in Python and react to any change in your Notion databases within a configurable poll interval. Runs on any Linux system with systemd (Raspberry Pi, Ubuntu, Debian, etc.).
Duplicate the Notion template to get the pre-built database setup, then follow the steps below to connect the daemon.
- Closed Date Stamping — automatically stamps a Closed Date when a task is marked done, and clears it on reopen.
- Reopen Count — tracks how many times a task has been reopened.
- Field Tracking — records the first observed value of any configured field and counts how many times it changes.
- Recurring Tasks — keeps one open task per series at all times; creates the next task automatically when one is completed or cancelled. Supports habits, responsibilities, and more.
- Custom Automations — write your own rules in Python with full access to page properties and change history.
- Setup
- Closed Date Stamping
- Reopen Count
- Field Tracking
- Recurring Tasks
- Adding Your Own Automations
- Tuning Poll Interval
- How it compares to Notion's built-in automations
- Usage Guide
- Updating
- Future Plans
- Go to https://www.notion.so/my-integrations
- Click + New integration → give it a name → Submit
- Copy the Internal Integration Secret
- Open each Notion database you want to automate →
...menu → Add connections → select your integration
Open the database in Notion. The URL looks like:
https://www.notion.so/myworkspace/abc123def456...?v=...
The database ID is the UUID between your workspace name and ?v=. Copy it without dashes.
Linux (Raspberry Pi / Debian / Ubuntu) — install system dependencies if not already present:
sudo apt update && sudo apt install -y python3 python3-venv gitgit clone https://github.com/DreamShark-Bytes/Notion_Automator
cd Notion_Automator
python3 -m venv venv
venv/bin/pip install -r requirements.txtWindows — install Python if not already present:
- Download Python 3.11+ from python.org — check "Add Python to PATH" during install
- (Optional) Install Git from git-scm.com to clone via terminal, or download the repo as a ZIP
git clone https://github.com/DreamShark-Bytes/Notion_Automator
cd Notion_Automator
python -m venv venv
venv\Scripts\pip install -r requirements.txtrequirements.txt includes the pinned Notion_API dependency — no separate install step needed.
Compatibility
| Notion Automator | Notion API |
|---|---|
| v1.x | v1.x |
Linux:
cp config_example.toml config.toml
nano config.tomlWindows:
copy config_example.toml config.toml
notepad config.tomlAt minimum, set your integration token and add one [[databases]] block for each database you want to automate:
token = "ntn_your_token_here"
[[databases]]
id = "your-database-id"Enable individual features per database by adding flags to the block. See each feature section below for the exact flags.
Linux:
venv/bin/python daemon.pyWindows:
venv\Scripts\python daemon.pyYou should see polling logs every 60 seconds. Make a change in Notion and watch it react.
Edit the service file before copying it — replace both placeholder values:
| Placeholder | Replace with |
|---|---|
YOUR_USER |
your Linux username (e.g. vince) |
/path/to/Notion_Automator |
absolute path to this repo (e.g. /home/vince/Documents/Notion_Automator) |
nano notion-daemon.service
sudo cp notion-daemon.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable notion-daemon
sudo systemctl start notion-daemon
# Check status / logs
sudo systemctl status notion-daemon
journalctl -u notion-daemon -fNSSM (Non-Sucking Service Manager) wraps the daemon as a proper Windows service — auto-starts on boot, restarts on failure, runs while locked.
-
Download NSSM from nssm.cc/download — extract and put
nssm.exesomewhere permanent (e.g.C:\Tools\nssm.exe) -
Open
install-service.ps1in a text editor and set$ProjectDirand$NssmPathto match your machine. -
Open PowerShell as Administrator and run:
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\install-service.ps1- Manage the service from PowerShell:
Start-Service NotionAutomator
Stop-Service NotionAutomator
Get-Service NotionAutomatorTo list all NSSM-managed services (useful if you forget the service name):
Get-CimInstance Win32_Service | Where-Object PathName -match 'nssm.exe' | Format-Table Name, DisplayName, StateOr open services.msc and find Notion Automator in the list.
Note: Avoid using
scin PowerShell — it is aliased toSet-Content, not the Service Control Manager. UseStart-Service/Stop-Service/Get-Serviceinstead, or runsc.exe(with the.exeextension) if you prefer thescsyntax.
Logs are written to notion_daemon.log in the project directory.
Automatically stamps a Closed Date field when a task moves to the Done status group, and clears it if the task is reopened. If you pre-fill Closed Date before closing (e.g. to backdate a completion), the bot leaves your value in place.
On daemon startup, any task already in Done without a Closed Date gets backfilled from its last_edited_time.
Add a Date field named Closed Date to your task database.
[[databases]]
id = "your-database-id"
closed_date = trueIncrements a Reopen Count field each time a task moves out of the Done status group. Useful for tracking how often a task gets re-opened after being marked complete.
Requires Closed Date Stamping to be enabled, since reopen detection relies on the same status transition logic.
Daemon-downtime recovery: if a task was closed while the daemon was running (Closed Date stamped), then the user reopened it while the daemon was offline, the daemon will see a non-Complete task that still has a Closed Date set when it restarts. It treats this as a missed reopen: Reopen Count is incremented and Closed Date is cleared. This is self-healing — no manual correction needed.
Add a Number field named Reopen Count to your task database.
[[databases]]
id = "your-database-id"
closed_date = true # required
reopen_count = trueTwo config keys tell the bot which fields to watch. Both use naming conventions so no explicit field mapping is needed.
For each field listed in first_value_fields, the bot looks for a column named First [Field Name] in the database and stamps it with the field's first observed non-empty value. The stamp is write-once: the bot never overwrites it. If you clear it manually, the bot re-stamps on the next poll.
Supported field types: Date, Select, Status, Number, Text, URL, Email, Phone.
Example: add "Due Date" to get First Due Date stamped automatically. Add "Status" to capture the status a task had when first seen.
For each field listed in update_count_fields, the bot looks for a [Field Name] Update Count number column and increments it whenever the value changes. For Date fields, only a change to the date portion counts; a time-only adjustment on the same day does not increment the counter.
Create columns in your task database following the naming convention:
| Column name | Type | Tracks |
|---|---|---|
First Due Date |
Date | First value of Due Date |
Due Date Update Count |
Number | How many times Due Date changed |
First Status |
Text | Status when task was first seen |
Status Update Count |
Number | How many times Status changed |
Add only the columns you want. Missing columns are silently skipped.
[[databases]]
id = "your-database-id"
first_value_fields = ["Due Date"]
update_count_fields = ["Due Date"]Keeps one open task per series at all times. When a task is marked Done or Cancelled, the bot automatically creates the next one for the appropriate period.
Create a new database in Notion with these fields:
| Field | Type | Notes |
|---|---|---|
| Name | Title | |
| Type | Select | Habit, Bad Habit, Responsibility |
| Status | Status | Bot only creates tasks when Status = Active |
| Cadence Type | Select | Once per period, Exactly N per period, Maximum N per period, Minimum N per period, Unlimited |
| N Cadence | Number | Used by cadence types that reference N; blank for others |
| Period | Select | Day, Week, Month, Year |
| Anchor Day | Number | Mon=1 … Sun=7 for weekly; 1–31 for monthly (overflows to last day of month) |
| Anchor Time | Text | e.g. 13:00; blank = no specific time |
| Grace Period (days) | Number | Responsibilities only — auto-cancelled this many days past due; blank = 1-day default; set 0 to cancel immediately after anchor time |
| Current Period | Date | Optional — bot writes the current period's start and end datetimes each governance pass; enables Notion formulas that filter tasks to the current period |
| Notes | Rich Text | |
| Last Completed | Rollup | Max of Closed Date from related tasks |
Add these fields to your main tasks database:
| Field | Type | Required | Notes |
|---|---|---|---|
| Recurring Series | Relation | Yes | Points to the Definitions database |
| Closed Date | Date | Yes | Required for period logic — see Closed Date Stamping |
| Occurrence # this Period (Recurring Task) | Number | No | Count of completions this period; filled in by the bot |
| Period Key (Recurring Task) | Text | No | Display label for the current period — written by the bot, do not edit |
| Period Target (Recurring Task) | Text | No | e.g. Minimum 3 per Week — set by the bot at creation |
| Ignore Grace Period (Recurring Task) | Checkbox | No | Prevents grace-period auto-cancellation for this specific task instance. The bot sets this automatically when you reopen a Responsibility task, but you can also check it manually — useful when you've customized a task (renamed it, adjusted the due date) and don't want it auto-cancelled. New tasks from the series always start with this unchecked. |
Connect your integration to both databases (... menu → Add connections).
# Global — controls when the logical day begins (affects all period calculations).
# Times between midnight and this hour are attributed to the previous calendar day.
# Also sets when the daily governance cron fires. Default: 3 (3am).
day_start_hour = 3
# Global — first day of the week. Affects weekly recurring task period boundaries.
# Default: "Sunday" (matches Notion). Use "Monday" for ISO/work-week convention.
week_start = "Sunday"
[[databases]]
id = "your-tasks-database-id"
closed_date = true # required for recurring task period logic
[recurring_tasks]
enabled = true
definitions_db_id = "your-definitions-database-id"
tasks_db_id = "your-tasks-database-id"- One open task per series at all times. When a task is marked Done or Cancelled, the bot creates the next one automatically.
- Due dates are calculated from Anchor Day and Anchor Time. Without an anchor, the due date is a range spanning the full period (e.g. April 1–30 for a monthly task).
- Habit Due Dates — Habit tasks receive a Due Date spanning the full period, making them visible in Notion's Today and This Week calendar views. At each period boundary, governance rolls the open Habit task forward to the new period: the Due Date is updated, Occurrence # resets to 1, and any
First Due Date/Due Date Update Counttracking fields are cleared so the new period starts fresh. Habits are never cancelled or archived for being "overdue" — they simply carry forward. - Minimum N carry-over — When a
Minimum N per periodtask is still open at period end and the minimum was already met, governance carries it forward into the new period (updating Due Date, Period Key, and resetting Occurrence # to 1) instead of archiving it. The opportunity to do more than the minimum is preserved. - Occurrence # counts how many tasks have been created for the current period. Resets to 1 at each period boundary for all series types. Continues incrementing within a period for
Minimum N per periodandUnlimitedcadences. - Grace period (Responsibilities only): if a task is still open more than N days past its due date, the bot cancels it and creates the next one.
- Startup governance: on every daemon start, the bot checks that each Active series has exactly one open task for the current period. Zero → creates one. Multiple → logs a warning for manual resolution.
- Live monitoring: the definitions database is polled alongside your task databases. Creating a new definition or toggling one to
Activetriggers governance within one poll cycle — no restart needed. - Deleted tasks are handled by governance — if a recurring task is deleted, the startup check detects the missing open task and creates a replacement.
Open automations.py. Each automation is a plain Python function:
def my_automation(client, page, prev_page) -> dict:
# page — the current state of the Notion page
# prev_page — the state from the previous poll (or same as page on first sight)
# Return a dict of Notion property updates, or {} to skip
if some_condition:
return {"My Field": {"number": 42}}
return {}Then add it to the AUTOMATIONS list at the bottom of the file.
See DESIGN.md for the full automation signature, governance system, and architecture details.
Set poll_interval in config.toml (applies to all databases).
- 60s (default) — good balance, ~1440 API calls/day per database
- 30s — more responsive, ~2880 calls/day
- Notion's free tier rate limit is 3 requests/second — you won't come close.
| Feature | This daemon | Notion built-in |
|---|---|---|
| Trigger on change | ✅ (within poll interval) | ✅ (instant) |
| Custom logic in Python | ✅ | ❌ |
| Recurring tasks | ✅ | ✅ |
| Outbound webhooks | ✅ (add your own code) | ✅ (paid plans) |
| Always-on device required | ✅ (Raspberry Pi works great) | ❌ |
| Subscription required | ❌ | ✅ |
Tips for configurations that are valid but non-obvious. Useful once you're familiar with the features.
day_start_hour — setting your logical day boundary:
By default, the daemon treats 3am as the start of a new day. Any task completed between midnight and 3am is attributed to the previous calendar day's period. This prevents an accidental late-night close from counting toward the next day's quota. If you regularly work past 3am, increase this value (e.g. day_start_hour = 4). If you have an extreme schedule (e.g. sleep at 11am, wake at 6pm), set it to shortly after you wake up — periods will be labeled with the calendar date of your waking day. The same setting controls when the daily governance cron fires.
Minimum N per period with Anchor Day:
If all N repetitions of an activity happen at the same scheduled event (e.g. a weekly meeting, a class, a practice), Minimum N per period is probably not the right cadence. Use Once per period with Anchor Day set to that event's day instead. Completing the task means you showed up; how many times you did something during the event is detail that belongs in the task name or notes, not in N. Minimum N per period is better suited to activities spread across the period with no fixed day.
Two recurring event days per period (e.g. Tuesday and Thursday):
The system supports one Anchor Day per series definition. To track a recurring activity that happens on two specific days per week, create two separate definitions — one anchored to Tuesday, one to Thursday — each with Once per period. Using Minimum 2 per period with no anchor would give you a due date spanning the full period but would not enforce the specific days.
Applying RTD config changes immediately:
Changing an RTD's Period, Cadence Type, or N Cadence doesn't trigger governance right away — the change takes effect at the next daemon startup or daily governance cron. To apply it immediately without waiting: set the RTD's Status to inactive, wait one poll cycle (default 60s), then set it back to Active. The Status → Active transition triggers an immediate governance run that drift-corrects all affected tasks. A one-click Force Governance option is planned as part of the Automation Hub.
Protecting a customized Responsibility task from auto-cancellation:
If you edit a specific recurring task instance — renaming it, changing the due date, scoping it to a particular case — you may not want the grace period cron to cancel it on schedule. Check Ignore Grace Period (Recurring Task) directly on that task. The bot will skip it during every grace period evaluation for as long as it stays open. When you close it, the next task in the series is created fresh with the checkbox unchecked, so normal behavior resumes automatically.
Current Period field — filtering tasks to the current period in Notion:
Add a Current Period Date field to your definitions database. Governance writes the current period's start and end datetimes to it on every governance pass. You can then use a Notion formula on your task pages to check whether a task's Due Date falls within the current period:
prop("Recurring Series").map(current.prop("Current Period")).map(
dateStart(current) <= prop("Due Date") and dateEnd(current) >= prop("Due Date")
).includes(true)
Pair this with a rollup on the definitions database to count tasks completed this period — more accurate than a bot-maintained counter and requires no extra writes.
- Stop the service:
sudo systemctl stop notion-daemon - Back up any files you've edited:
automations.pyandconfig.tomlare the only ones you're expected to modify - Pull the latest changes:
git pull - Install any new dependencies:
venv/bin/pip install -r requirements.txt --force-reinstall- This also updates Notion_API if the pinned version changed. The release notes will call this out explicitly when it applies.
- Apply any Notion-side changes listed in the release notes (field renames, new select options, new columns)
- Update
config.tomlto match any new config format changes - Start the service:
sudo systemctl start notion-daemon - Check logs to confirm governance ran cleanly:
journalctl -u notion-daemon -f
To diff your local automations.py against the new version before overwriting: git diff HEAD automations.py
- Stop the service:
Stop-Service NotionAutomator - Back up any files you've edited:
automations.pyandconfig.tomlare the only ones you're expected to modify - Pull the latest changes:
git pull - Install any new dependencies:
venv\Scripts\pip install -r requirements.txt --force-reinstall- This also updates Notion_API if the pinned version changed. The release notes will call this out explicitly when it applies.
- Apply any Notion-side changes listed in the release notes (field renames, new select options, new columns)
- Update
config.tomlto match any new config format changes - Start the service:
Start-Service NotionAutomator - Check logs to confirm governance ran cleanly: open
notion_daemon.login the project directory, or tail it in PowerShell:Get-Content -Path .\notion_daemon.log -Tail 50 -Wait
To diff your local automations.py against the new version before overwriting: git diff HEAD automations.py
Developed in collaboration with Claude Code by Anthropic. All architectural decisions, requirements definition, design reviews, testing, and production deployment are owned by the human author. Claude assisted with implementation, debugging, and documentation under directed oversight — not vibe-coding, but a design-led workflow where nothing ships without human review and approval.
See PLANNED.md for full details.
- Automation Hub — A single Notion page as the daemon's home base. Surfaces health, errors, and warnings; eventually replaces
config.tomlfor behavioral settings that change frequently. - Extended Cadence — Support for cadences spanning multiple periods (bi-weekly, quarterly, etc.) with a simplified picker and a Custom mode.
- Notifications — Outbound webhook support (Discord, Telegram) for alerts on governance events.
- Change Tracking — Opt-in field change log with old/new values and timestamps, feeding into reporting tools.

