Agentic Tasks — Notion Provider
This file contains all Notion-specific implementation details for agentic-tasks. Load this file when the active provider is notion.
Config Retrieval
When detecting-provider requests config retrieval for the Notion provider, follow these steps to populate headless_config:
- Search for the "Agentic Tasks Config" page using
notion-search - Retrieve the page body using
notion-fetch - Parse the JSON code block and set the following as the
headless_configsession variable:tasksDatabaseId(required)teamsDatabaseId(optional)maxConcurrentAgents(optional — default: 3)intakeLogDatabaseId(optional — exists after first ingesting-messages run)
If the Config page is not found, instruct the user to run the setting-up-tasks skill, then stop.
Schema Validation
After loading config, verify Core fields by calling notion-fetch with tasksDatabaseId and inspecting the returned schema's properties object.
Required Core fields (15): Title, Description, Acceptance Criteria, Status, Blocked By, Priority, Executor, Requires Review, Execution Plan, Working Directory, Session Reference, Dispatched At, Agent Output, Error Message, Issuer.
Auto-Repair (Missing Fields)
If any Core field is missing, automatically repair using notion-update-data-source.
First obtain the data source ID via notion-fetch on the database URL.
Then run the appropriate DDL (one ADD COLUMN per call):
| Missing Field | Repair DDL |
|---|---|
| Status | ADD COLUMN "Status" SELECT('Backlog':gray, 'Ready':blue, 'In Progress':yellow, 'In Review':orange, 'Done':green, 'Blocked':red) |
| Priority | ADD COLUMN "Priority" SELECT('Urgent':red, 'High':orange, 'Medium':yellow, 'Low':blue) |
| Executor | ADD COLUMN "Executor" SELECT('claude-code':purple, 'cowork':green, 'human':gray) |
| Dispatched At / Due Date | ADD COLUMN "<field>" DATE |
| Issuer | ADD COLUMN "Issuer" PERSON |
| (other text fields) | ADD COLUMN "<field>" RICH_TEXT |
After repair, re-verify and continue. Never ask the user to manually fix the schema.
MCP Tool Reference
notion-create-pages— Create a task (parent:{ "data_source_id": TASKS_DS_ID })notion-update-page— Update task propertiesnotion-fetch— Get a database, data source, or single task by URL/IDnotion-search— Full-text search across tasks; use for filtering by field valuenotion-get-comments/notion-create-comment— Read/write task comments
Schema: Notion Property → Canonical Role
Core Fields (15 required — verify existence at session start)
| Property | Notion Type | Canonical Role | Notes |
|---|---|---|---|
| Title | title | task_title |
Task name |
| Description | rich_text | task_description |
Orchestrator-written detail |
| Acceptance Criteria | rich_text | task_acceptance_criteria |
Verifiable completion conditions |
| Status | select | task_status |
Backlog / Ready / In Progress / In Review / Done / Blocked |
| Blocked By | relation | task_blocked_by |
Self-relation (dependency). Empty or all blockers Done = actionable |
| Priority | select | task_priority |
Urgent / High / Medium / Low |
| Executor | select | task_executor |
claude-code / cowork / human |
| Requires Review | checkbox | task_requires_review |
On → must pass In Review. Off → can go directly to Done |
| Execution Plan | rich_text | task_execution_plan |
Orchestrator's plan written before dispatch. write-once |
| Working Directory | rich_text | task_working_directory |
claude-code: absolute path. cowork: workspace-relative path |
| Session Reference | rich_text | task_session_ref |
Written after dispatch: tmux session name / Cowork task ID |
| Dispatched At | date | task_dispatched_at |
Dispatch timestamp. Used for timeout detection |
| Agent Output | rich_text | task_agent_output |
Execution result |
| Error Message | rich_text | task_error_message |
Written on failure only. Query with "Error Message is not empty" |
| Issuer | people | task_issuer |
Who created/initiated this task. Auto-populated with current_user. Write-once. |
Extended Fields (optional — graceful degradation if absent)
| Property | Notion Type | Canonical Role | Notes |
|---|---|---|---|
| Context | rich_text | task_context |
Background info, constraints |
| Artifacts | rich_text | task_artifacts |
PR URLs, file paths (newline-separated) |
| Repository | url | task_repository |
GitHub repository URL |
| Due Date | date | task_due_date |
ISO format |
| Tags | multi_select | task_tags |
Free tags |
| Parent Task | relation | task_parent |
Self-relation (hierarchy) |
| Assignees | people | task_assignees |
Human executor assignment |
| Branch | rich_text | task_branch |
Git branch name (e.g. feature/task-slug). Leave blank to work on the current branch |
| Source Message ID | rich_text | task_source_message_id |
Messaging tool message unique ID (e.g. Slack channel_id:ts). Used for cross-member dedup |
Auto-Repair DDL for Extended Fields
If Source Message ID is missing and needed, repair with:
ADD COLUMN "Source Message ID" RICH_TEXT
Intake Log Database
The Intake Log DB tracks processed message IDs to avoid reprocessing. It is created automatically by the ingesting-messages skill on first run.
| Property | Notion Type | Description |
|---|---|---|
| Message ID | title | Message unique ID (e.g. Slack: channel_id:ts) |
| Tool Name | select | slack / teams / discord |
| Processed At | date | Processing timestamp |
The database ID is stored in the config page as intakeLogDatabaseId.
Querying Tasks
Use the first available query path (checked in order):
Query Path Detection
execution_environment = "cowork"ANDmcp__Notion_Query_for_Agentic_Tasks__notion-querytool available → Path 2 (Extension)NOTION_TOKENenv var set (check: run[ -n "$NOTION_TOKEN" ] && echo "SET" || echo "NOT SET"via Bash) → Path 1 (API script)- Otherwise → Path 3 (MCP fallback)
Path 1: Notion API Script (requires NOTION_TOKEN)
Call the query script for server-side filtering:
bash ${CLAUDE_PLUGIN_ROOT}/skills/providers/notion/scripts/query-tasks.sh \
"<tasksDatabaseId>" '<filter_json>' '<sort_json>'
The script returns {"results": [...]} with full page objects including all properties.
Filter Recipes
Tasks assigned to a user:
{"property":"Assignees","people":{"contains":"<user_id>"}}
Ready tasks assigned to a user:
{"and":[{"property":"Status","select":{"equals":"Ready"}},{"property":"Assignees","people":{"contains":"<user_id>"}}]}
In Progress tasks (for concurrency check):
{"and":[{"property":"Status","select":{"equals":"In Progress"}},{"property":"Assignees","people":{"contains":"<user_id>"}}]}
Ready tasks by executor and assignee:
{"and":[{"property":"Status","select":{"equals":"Ready"}},{"property":"Executor","select":{"equals":"claude-code"}},{"property":"Assignees","people":{"contains":"<user_id>"}}]}
Sort by Priority then Due Date:
[{"property":"Priority","direction":"ascending"},{"property":"Due Date","direction":"ascending"}]
Blocked tasks owned by user (via Assignees OR Issuer fallback):
{"and":[{"property":"Status","select":{"equals":"Blocked"}},{"or":[{"property":"Assignees","people":{"contains":"<user_id>"}},{"and":[{"property":"Issuer","people":{"contains":"<user_id>"}},{"property":"Assignees","people":{"is_empty":true}}]}]}]}
Ready human tasks owned by user (via Assignees OR Issuer fallback):
{"and":[{"property":"Status","select":{"equals":"Ready"}},{"property":"Executor","select":{"equals":"human"}},{"or":[{"property":"Assignees","people":{"contains":"<user_id>"}},{"and":[{"property":"Issuer","people":{"contains":"<user_id>"}},{"property":"Assignees","people":{"is_empty":true}}]}]}]}
Path 2: notion-query Extension (Cowork)
When the notion-query MCP tool is available (installed via Desktop Extension), call it directly:
mcp__Notion_Query_for_Agentic_Tasks__notion-query({ database_id: "<tasksDatabaseId>", filter: <filter_object>, sorts: <sort_array> })
The tool accepts the same filter/sort objects as Path 1's filter recipes. It returns {"results": [...]} with full page objects.
Build & install: See skills/providers/notion/extension/ for source and build instructions.
Path 3: MCP Fallback (no token, no extension)
Use notion-search with data_source_url to find task pages, then notion-fetch each page individually to get properties. Filter client-side by checking property values.
This is the slowest path — use only when Path 1 and Path 2 are unavailable.
Post-Processing (all paths)
- Blocked By resolved: Check that the
Blocked Byrelation array is empty OR fetch each referenced task's Status and confirm all are "Done". This cannot be filtered server-side. - Sort (if not done server-side): Priority — Urgent > High > Medium > Low; then by Due Date (earliest first).
Displaying Task Lists
When displaying queried tasks to the user in list or table format, extract only display-relevant fields to prevent output truncation:
bash ${CLAUDE_PLUGIN_ROOT}/skills/providers/notion/scripts/query-tasks.sh \
"<tasksDatabaseId>" '<filter_json>' '<sort_json>' | \
jq '[.results[] | {
id: .id,
title: (.properties.Title.title[0].plain_text // ""),
status: (.properties.Status.select.name // ""),
priority: (.properties.Priority.select.name // ""),
executor: (.properties.Executor.select.name // ""),
assignees: ([.properties.Assignees.people[]?.name] | join(", ")),
due_date: (.properties["Due Date"].date.start // ""),
blocked_by: (([.properties["Blocked By"].relation[]?.id] | length | tostring) + " deps")
}]'
For single-task detail views (update, status change), use the full page object.
Fetch All Tasks
To retrieve all tasks (e.g. for view server data push), use the detected query path with no filter:
- Path 1:
bash ${CLAUDE_PLUGIN_ROOT}/skills/providers/notion/scripts/query-tasks.sh "<tasksDatabaseId>"(no filter/sort args) - Path 2:
mcp__Notion_Query_for_Agentic_Tasks__notion-query({ database_id: "<tasksDatabaseId>" })(no filter/sorts) - Path 3:
notion-searchwithdata_source_url+notion-fetchper page
No post-processing needed (no Blocked By filter, no sort required).
Task Record Reference
When referring to a task in dispatch prompts and completion instructions, use:
- Task ID: the Notion page ID (from the
idfield when the task was created) - Update instruction: "Use
notion-update-pagewith page ID<Page ID>to write results to Agent Output and update Status."
In the Cowork environment, the dispatch prompt is set as the Scheduled Task's prompt. Notion MCP tools (notion-update-page) are available in both environments.
Pushing Data to View Server
After any task operation (create, update, delete), push fresh data to the local view server:
- Use Fetch All Tasks (above) to retrieve all tasks from the tasks database
- Format the response as a
TasksResponseJSON object:{ "tasks": [...], "updatedAt": "<ISO timestamp>" } - POST to
http://localhost:3456/api/datawithContent-Type: application/json
# Silently skip if server is not running
curl -s http://localhost:3456/api/health -o /dev/null 2>/dev/null && \
curl -s -X POST http://localhost:3456/api/data \
-H "Content-Type: application/json" -d '<json>' -o /dev/null 2>/dev/null || true
Identity: Resolve Current User
Called by resolving-identity shared skill when active_provider = notion.
- Call
notion-get-userswithuser_id: "self". - Map the response:
id←response.idname←response.nameemail←response.person.email(null if Bot user)
- Save to session variable
current_user: { id, name, email }. - Fallback: If
notion-get-usersis unavailable or fails:id←"unknown"name←$USERenvironment variable or "local"email← null
Identity: Resolve Team Membership
Called by resolving-identity shared skill when teamsDatabaseId is present in config.
- Call
notion-fetchonteamsDatabaseIdto retrieve all team pages. - For each team, inspect the
Memberspeople field. Check ifcurrent_user.idis present in the array. - Set
current_user.teamsto the list of matching teams:[{ id, name, members: [{ id, name }] }]. - Determine
current_team:- 1 matching team → automatically set
current_teamto that team. - 2+ matching teams → use AskUserQuestion: "You belong to multiple teams: [list]. Which team are you working with now?"
- 0 matching teams → set
current_team: null.
- 1 matching team → automatically set
- If
current_teamis set, populatecurrent_team.memberswith all members from that team'sMembersfield (array of{ id, name }). This is used by downstream skills for team-scoped filtering.
Identity: List Org Members
Called by resolving-identity shared skill when org_members lookup is needed.
- Call
notion-get-userswith no arguments to list all workspace members. - Map each user to
OrgMember { id, name, email }:id←user.idname←user.nameemail←user.person.email(null for Bot users)
- Save to session variable
org_members: OrgMember[]. - Fallback: If
notion-get-usersis unavailable, setorg_members: []and return. Thelooking-up-membersskill will then fall back to TeamsDB Members field.
Identity: Self-Task Detection
To determine whether a task is assigned to the current user:
- Fetch the task's
Assigneesproperty (people type — returns an array of person objects). - Check if any element in the array has
id === current_user.id. - Use this check when filtering tasks in
managing-tasksandexecuting-tasks.