
Todoist Api
- 52 installs
- 3 repo stars
- Updated June 29, 2026
- tristanmanchester/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
todoist-api is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- todoist-api
- AI & Agent Building
- AI-coding skill
Todoist Api by the numbers
- 52 all-time installs (skills.sh)
- +1 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #7,086 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tristanmanchester/agent-skills --skill todoist-apiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 52 |
|---|---|
| repo stars | ★ 3 |
| Last updated | June 29, 2026 |
| Repository | tristanmanchester/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Todoist API
When to use this skill
Use this skill when work involves Todoist data or automation, especially:
- capture or quick-add new tasks
- inspect, filter, move, complete, reopen, or delete tasks
- manage projects, sections, labels, or comments
- resolve human names to Todoist IDs before writing
- perform safer bulk edits with dry-runs
- review completed work or recent activity
- build Todoist scripts, agents, or integrations around the public API
When not to use this skill
Do not use this skill for:
- editing the user’s local Todoist app UI directly
- calendar-specific workflows that belong in a calendar skill
- attachment upload flows that require multipart handling unless you are prepared to use
curlor therawescape hatch - non-Todoist task systems
Safety defaults
- Start read-only if the user’s intent is ambiguous.
- Resolve names to IDs before any write.
- Prefer close over delete unless the user explicitly wants permanent removal.
- Run
--dry-runfirst for bulk or destructive work. - Use
--confirmfor bulk closes, moves, repeated comments, and deletes. - If a command may return a large payload, set
--output FILEso stdout stays small and predictable.
Pick the smallest capable surface
- One object, one endpoint → use a low-level REST wrapper such as
get-task,update-project, orget-comment. - Natural-language capture → use
quick-add-task. - Resolve names safely → use
resolve-project,resolve-section,resolve-label. - Create if missing → use
ensure-project,ensure-section,ensure-label. - Many matching tasks → use
bulk-close-tasks,bulk-move-tasks,bulk-comment-tasks. - Completed-work review → use
report-completedorget-completed-tasks. - Full or incremental sync / batched writes → use
sync. - Unwrapped or niche endpoint → use
raw.
Output contract
The main script prints structured output to stdout by default.
--format jsonreturns a stable JSON envelope with fields likeaction,ok,count,next_cursor,matched_count,changed_count, andresolved.--format summaryreturns a smaller human-readable summary.--output FILEwrites the full output to a file and prints a small JSON notice to stdout.
This is designed for agent pipelines: stdout stays parseable, stderr carries diagnostics, and retries are built in for transient failures.
Scripts
- `scripts/todoist_api.py` — main non-interactive Todoist CLI
- `scripts/smoke_test.py` — read-only connectivity check
Inspect help first:
python3 scripts/todoist_api.py --help
python3 scripts/todoist_api.py get-tasks-by-filter --help
python3 scripts/todoist_api.py bulk-move-tasks --help
python3 scripts/smoke_test.py --helpQuick start
Set a token:
export TODOIST_API_TOKEN="YOUR_TODOIST_TOKEN"Read-only smoke test:
python3 scripts/smoke_test.pySanity-check access:
python3 scripts/todoist_api.py get-projects --limit 5
python3 scripts/todoist_api.py get-labels --limit 10Resolve names before writes:
python3 scripts/todoist_api.py resolve-project --name "Inbox"
python3 scripts/todoist_api.py resolve-section --project-name "Client Alpha" --name "Next Actions"
python3 scripts/todoist_api.py resolve-label --name "waiting-on"High-value agent workflows
Quick add
python3 scripts/todoist_api.py quick-add-task \
--text "Email Chris tomorrow at 09:00 #Work @follow-up p2"Create-if-missing section
python3 scripts/todoist_api.py ensure-section \
--project-name "Client Alpha" \
--name "Next Actions"Preview a bulk close
python3 scripts/todoist_api.py bulk-close-tasks \
--filter "overdue & @errands" \
--dry-runExecute the same bulk close
python3 scripts/todoist_api.py bulk-close-tasks \
--filter "overdue & @errands" \
--confirmMove matching tasks into a resolved section
python3 scripts/todoist_api.py bulk-move-tasks \
--filter "#Inbox & !recurring" \
--target-project-name "Work" \
--target-section-name "Next Actions" \
--dry-runReport completed work
python3 scripts/todoist_api.py report-completed \
--since "2026-03-01T00:00:00Z" \
--until "2026-03-31T23:59:59Z" \
--by completion \
--output reports/march-completed.jsonRecommended operating pattern
1. Resolve or list the target object. 2. Read current state with a low-level getter. 3. Preview the write with --dry-run. 4. Execute with --confirm when needed. 5. Verify by re-reading or by running a report command.
Feature index
- Command catalogue and endpoint coverage → references/REFERENCE.md
- Task-first recipes → references/RECIPES.md
- Todoist-specific caveats → references/GOTCHAS.md
Escape hatches
Use raw when the public CLI surface does not yet wrap a needed endpoint:
python3 scripts/todoist_api.py raw \
--method GET \
--path /projects/PROJECT_ID/fullUse sync when you need incremental sync or batched commands:
python3 scripts/todoist_api.py sync \
--sync-token '*' \
--resource-types '["all"]'[
{
"type": "note_add",
"temp_id": "temp-note-1",
"uuid": "44444444-4444-4444-4444-444444444444",
"args": {
"item_id": "TASK_ID_HERE",
"content": "Reviewed during weekly planning."
}
}
][
{
"type": "project_add",
"temp_id": "temp-project-1",
"uuid": "11111111-1111-1111-1111-111111111111",
"args": {
"name": "Launch Checklist"
}
},
{
"type": "item_add",
"temp_id": "temp-task-1",
"uuid": "22222222-2222-2222-2222-222222222222",
"args": {
"content": "Draft launch email",
"project_id": "temp-project-1"
}
},
{
"type": "item_add",
"temp_id": "temp-task-2",
"uuid": "33333333-3333-3333-3333-333333333333",
"args": {
"content": "Prepare release notes",
"project_id": "temp-project-1"
}
}
]MIT License
Copyright (c) 2026 OpenAI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Gotchas
Table of contents
- Opaque IDs
- Search first, then write
- Archived objects
- Pagination and bounded output
- Retry behaviour
- Bulk safety
- Legacy naming in sync
- Template and upload gaps
- Task and project deep links
- Plan-gated features
Opaque IDs
Todoist IDs are opaque strings in API v1. Do not assume integers or sortable sequences.
What to do:
- keep IDs as strings
- do not coerce to integers
- use
ids-mapwhen migrating from older stored IDs - prefer
resolve-*commands when starting from names
Search first, then write
Names are not stable IDs.
Safe pattern:
1. resolve or search the object 2. inspect the current object 3. preview the change 4. execute the change 5. verify
This matters most for:
- shared projects
- sections with repeated names across projects
- labels that may exist as personal or shared labels
Archived objects
Archived projects and sections usually need explicit inclusion during resolution.
Examples:
python3 scripts/todoist_api.py resolve-project --name "Archive" --include-archived
python3 scripts/todoist_api.py resolve-section --project-name "Old Client" --name "Done" --include-archivedPagination and bounded output
Many Todoist endpoints are cursor-paginated.
Use:
--limitfor page size--cursorfor the next page--allto drain the cursor chain--max-itemsto stop early--output FILEfor very large payloads
If you only need a quick answer, prefer a small first page before loading everything.
Retry behaviour
The CLI retries transient failures by default.
Built-in defaults:
- retry on
429,500,502,503,504 - honour
Retry-After/error_extra.retry_afterwhen available - exponential backoff from
--retry-backoff
Tune when needed:
python3 scripts/todoist_api.py get-projects --retry 4 --retry-backoff 2Bulk safety
Bulk commands are designed for agents, not manual shell heroics.
Rules:
bulk-close-tasks,bulk-move-tasks, andbulk-comment-tasksrequire--dry-runor--confirm- they return counts and explicit change lists
- they skip tasks already at the destination in
bulk-move-tasks - they do not guess ambiguous names
Legacy naming in sync
Todoist API v1 uses current names in top-level REST endpoints, but /sync still carries some legacy object names.
Examples you will still see in sync payloads:
itemswhere REST usestasksnoteswhere REST usescomments
If a task is small and single-purpose, prefer the modern REST wrapper. Use sync only when batching or incremental sync is the real goal.
Template and upload gaps
The main wrapper intentionally avoids full multipart upload support.
Use raw or curl for:
- file uploads
- attachment workflows
- template import from file
- template export as file download
This keeps the main agent surface deterministic and non-fragile.
Task and project deep links
The script adds todoist://task?id=... links to task objects where practical and todoist://project?id=... links to project objects where practical. These are useful when the next step is “open this object in Todoist”.
Plan-gated features
Some Todoist API capabilities depend on the authenticated user’s plan or token scopes.
Watch for this especially around:
- reminders and other premium features
- backups
- uploads and upload limits
- template import/export availability in the user plan
Recipes
Table of contents
- Inbox triage
- Quick capture from natural language
- Project setup
- Weekly review
- Overdue clean-up
- Audit trail comments
- Shared project assignment prep
- Legacy ID migration
- Template export
Inbox triage
Preview what is currently in a project:
python3 scripts/todoist_api.py resolve-project --name "Inbox"
python3 scripts/todoist_api.py get-tasks --project-id PROJECT_ID --allMove non-recurring inbox work into a target section:
python3 scripts/todoist_api.py bulk-move-tasks \
--filter "#Inbox & !recurring" \
--target-project-name "Work" \
--target-section-name "Next Actions" \
--dry-runThen execute:
python3 scripts/todoist_api.py bulk-move-tasks \
--filter "#Inbox & !recurring" \
--target-project-name "Work" \
--target-section-name "Next Actions" \
--confirmQuick capture from natural language
For user requests that already sound like Todoist input, prefer quick add:
python3 scripts/todoist_api.py quick-add-task \
--text "Pay electricity bill next Tuesday 18:00 #Admin @finance p2"This is usually better than manually constructing create-task payloads when the user already phrased dates, labels, and priorities naturally.
Project setup
Create a project if needed:
python3 scripts/todoist_api.py ensure-project \
--name "Client Alpha" \
--description "Delivery work"Create standard sections:
python3 scripts/todoist_api.py ensure-section --project-name "Client Alpha" --name "Next Actions"
python3 scripts/todoist_api.py ensure-section --project-name "Client Alpha" --name "Waiting"
python3 scripts/todoist_api.py ensure-section --project-name "Client Alpha" --name "Done"Optionally seed tasks via /sync:
python3 scripts/todoist_api.py sync \
--commands-file assets/sync/seed-project.json \
--dry-runWeekly review
Get completed work for the last week:
python3 scripts/todoist_api.py report-completed \
--since "2026-03-01T00:00:00Z" \
--until "2026-03-07T23:59:59Z" \
--by completion \
--output reports/weekly-review.jsonGet recent activity for one project:
python3 scripts/todoist_api.py get-activities \
--parent-project-id PROJECT_ID \
--all \
--output reports/weekly-activity.jsonOverdue clean-up
Preview overdue work:
python3 scripts/todoist_api.py get-tasks-by-filter \
--query "overdue" \
--allPreview closing all overdue errands:
python3 scripts/todoist_api.py bulk-close-tasks \
--filter "overdue & @errands" \
--dry-runIf the user confirms that completed/irrelevant errands should be closed, execute with --confirm.
Audit trail comments
Add a comment to every matching urgent task:
python3 scripts/todoist_api.py bulk-comment-tasks \
--filter "today & p1" \
--content "Reviewed during daily planning." \
--dry-runThis is useful when an agent needs to leave a trace explaining a bulk review, escalation, or triage pass.
Shared project assignment prep
Resolve collaborators before assigning:
python3 scripts/todoist_api.py get-project-collaborators --project-id PROJECT_IDThen update a task with the selected collaborator ID:
python3 scripts/todoist_api.py update-task \
--task-id TASK_ID \
--assignee-id USER_ID \
--dry-runLegacy ID migration
When the user has old cached IDs or data from an older integration:
python3 scripts/todoist_api.py ids-map \
--object-name tasks \
--ids 918273645,918273646Repeat for projects, sections, or comments as needed.
Template export
Get a shareable template URL from an existing project:
python3 scripts/todoist_api.py template-export-url \
--project-id PROJECT_IDIf the user wants a full downloadable template file or a file-based import, switch to raw or curl, because those flows are not fully wrapped here.
Reference
Table of contents
- Command families
- Global flags
- Result envelope
- Name-resolution commands
- Ensure commands
- Bulk commands
- Low-level wrappers
- Escape hatches
- Coverage notes
- Quick grep targets
Command families
Read/list/search
get-projectsget-archived-projectssearch-projectsget-projectget-project-collaboratorsget-project-fullget-sectionsget-archived-sectionssearch-sectionsget-sectionget-labelsget-shared-labelssearch-labelsget-labelget-tasksget-tasks-by-filterget-taskget-completed-tasksget-completed-statsget-commentsget-commentget-activitiesids-mapget-backupstemplate-export-url
Write one object
create-projectupdate-projectdelete-projectarchive-projectunarchive-projectcreate-sectionupdate-sectiondelete-sectionarchive-sectionunarchive-sectioncreate-labelupdate-labeldelete-labelquick-add-taskcreate-taskupdate-taskmove-taskclose-taskreopen-taskdelete-taskcreate-commentupdate-commentdelete-commentget-or-create-emaildisable-email
Agent-friendly helpers
resolve-projectresolve-sectionresolve-labelensure-projectensure-sectionensure-labelbulk-close-tasksbulk-move-tasksbulk-comment-tasksreport-completed
Escape hatches
syncraw
Global flags
Available on the main script:
--token— explicit Todoist token--base-url— override API base URL--timeout— request timeout--retry— retries for 429/5xx/network failures--retry-backoff— initial backoff in seconds--format json|summary--output FILE|---verbose
For paginated commands:
--limit--cursor--all--max-items
For write commands:
--dry-run--confirmfor bulk and destructive operations
Result envelope
Default output is a stable JSON envelope.
Typical read:
{
"ok": true,
"action": "get-projects",
"dry_run": false,
"count": 3,
"next_cursor": null,
"data": {
"results": [
{"id": "abc", "name": "Inbox"},
{"id": "def", "name": "Work"}
],
"next_cursor": null
}
}Typical resolver:
{
"ok": true,
"action": "resolve-section",
"dry_run": false,
"data": {
"id": "6fFPHV272WWh3gpW",
"project_id": "6XGgm6PHrGgMpCFX",
"name": "Next Actions"
}
}Typical bulk preview:
{
"ok": true,
"action": "bulk-move-tasks",
"dry_run": true,
"matched_count": 5,
"changed_count": 4,
"skipped_count": 1,
"resolved": {
"filter": "#Inbox & !recurring",
"project_id": "6XGgm6PHrGgMpCFX",
"section_id": "6fFPHV272WWh3gpW"
},
"data": [
{
"task_id": "6XGgmFVcrG5RRjVr",
"from_project_id": "old-project",
"to_project_id": "6XGgm6PHrGgMpCFX",
"to_section_id": "6fFPHV272WWh3gpW"
}
]
}Name-resolution commands
These commands are the safest way to bridge from user language to Todoist IDs.
Resolve a project
python3 scripts/todoist_api.py resolve-project --name "Inbox"
python3 scripts/todoist_api.py resolve-project --name "Archive" --include-archived
python3 scripts/todoist_api.py resolve-project --name "Client Alpha" --strictResolve a section within a project scope
python3 scripts/todoist_api.py resolve-section \
--project-name "Client Alpha" \
--name "Next Actions"Resolve a shared label too
python3 scripts/todoist_api.py resolve-label \
--name "blocked" \
--include-sharedResolution rules:
1. exact case-insensitive name 2. if --strict is absent, unique prefix match 3. if still unresolved, unique substring match 4. otherwise fail loudly with candidate details
Ensure commands
These commands are useful for idempotent agents.
Ensure a project exists
python3 scripts/todoist_api.py ensure-project --name "Client Alpha"Ensure a project and patch a few fields if it already exists
python3 scripts/todoist_api.py ensure-project \
--name "Client Alpha" \
--description "Delivery work" \
--color blue \
--update-existing \
--dry-runEnsure a section within a resolved project
python3 scripts/todoist_api.py ensure-section \
--project-name "Client Alpha" \
--name "Waiting" \
--update-existing \
--dry-runEnsure a label exists
python3 scripts/todoist_api.py ensure-label \
--name "waiting-on" \
--color berry_redBulk commands
Close all matching tasks
python3 scripts/todoist_api.py bulk-close-tasks \
--filter "overdue & @errands" \
--dry-runMove all matching tasks
python3 scripts/todoist_api.py bulk-move-tasks \
--filter "#Inbox & !recurring" \
--target-project-name "Work" \
--target-section-name "Next Actions" \
--dry-runAdd the same comment to all matching tasks
python3 scripts/todoist_api.py bulk-comment-tasks \
--filter "today & p1" \
--content "Reviewed during morning triage." \
--dry-runBulk commands are intentionally conservative:
- they always require
--dry-runor--confirm - they respect
--max-items - they return
matched_count,changed_count,skipped_count - they fail name resolution instead of guessing
Low-level wrappers
Projects
python3 scripts/todoist_api.py get-projects --all
python3 scripts/todoist_api.py get-project --project-id PROJECT_ID
python3 scripts/todoist_api.py get-project-full --project-id PROJECT_ID --output project-full.json
python3 scripts/todoist_api.py archive-project --project-id PROJECT_ID --dry-runSections
python3 scripts/todoist_api.py get-sections --project-id PROJECT_ID --all
python3 scripts/todoist_api.py update-section --section-id SECTION_ID --name "Later" --dry-runLabels
python3 scripts/todoist_api.py get-labels --all
python3 scripts/todoist_api.py get-shared-labels
python3 scripts/todoist_api.py update-label --label-id LABEL_ID --color blue --dry-runTasks
python3 scripts/todoist_api.py get-tasks --project-id PROJECT_ID --all
python3 scripts/todoist_api.py get-tasks-by-filter --query "today & !recurring" --lang en --all
python3 scripts/todoist_api.py quick-add-task --text "Email Jess tomorrow 09:00 #Personal"
python3 scripts/todoist_api.py update-task --task-id TASK_ID --priority 1 --dry-run
python3 scripts/todoist_api.py move-task --task-id TASK_ID --section-id SECTION_ID --dry-runComments
python3 scripts/todoist_api.py get-comments --task-id TASK_ID --all
python3 scripts/todoist_api.py create-comment --task-id TASK_ID --content "Waiting on review" --dry-runReporting and utilities
python3 scripts/todoist_api.py report-completed \
--since "2026-03-01T00:00:00Z" \
--until "2026-03-31T23:59:59Z"
python3 scripts/todoist_api.py get-activities --parent-project-id PROJECT_ID --all
python3 scripts/todoist_api.py ids-map --object-name tasks --ids 918273645,918273646
python3 scripts/todoist_api.py get-or-create-email --obj-type task --obj-id TASK_ID --dry-run
python3 scripts/todoist_api.py template-export-url --project-id PROJECT_IDEscape hatches
sync
Use this when the work is inherently batched or when you need incremental sync:
python3 scripts/todoist_api.py sync \
--sync-token '*' \
--resource-types '["all"]'Preview sync commands:
python3 scripts/todoist_api.py sync \
--commands-file assets/sync/seed-project.json \
--dry-runraw
Use this for niche endpoints or temporary gaps in the wrapper:
python3 scripts/todoist_api.py raw \
--method GET \
--path /projects/PROJECT_ID/fullpython3 scripts/todoist_api.py raw \
--method GET \
--path /activities \
--query parent_project_id=PROJECT_ID \
--allCoverage notes
Wrapped directly in V2:
- projects, archived projects, collaborators, project full
- sections, archived sections
- labels and shared labels
- tasks, task filter, quick add, move, close, reopen, completed tasks, completed stats
- comments
- activities
- ID mapping
- backups list
- task/project email creation and disable
- template export URL
- sync/raw escape hatches
- name resolution, ensure, bulk close/move/comment, completed report
Still best handled with raw or curl:
- multipart uploads
- template import from file
- template export as file download
- attachment upload and comment attachment wiring
- any very new endpoint not yet exposed by the CLI
Quick grep targets
Useful search terms for this reference set:
grep -n "bulk-" references/REFERENCE.md
grep -n "ensure-" references/REFERENCE.md
grep -n "completed" references/REFERENCE.md
grep -n "ids-map" references/REFERENCE.md
grep -n "template" references/REFERENCE.md#!/usr/bin/env python3
"""
Read-only smoke test for the Todoist API skill.
This verifies:
- token discovery
- basic authenticated access
- one paginated read endpoint
- helper exit codes
Usage:
python3 scripts/smoke_test.py
python3 scripts/smoke_test.py --token "$TODOIST_API_TOKEN"
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
DEFAULT_BASE_URL = "https://api.todoist.com/api/v1"
USER_AGENT = "todoist-api-skill-smoke/2.0.0"
def emit(data: dict) -> None:
json.dump(data, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True)
sys.stdout.write("\n")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Read-only smoke test for the Todoist API skill.")
parser.add_argument("--token", help="Todoist API token. Defaults to TODOIST_API_TOKEN.")
parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help=f"API base URL (default: {DEFAULT_BASE_URL})")
parser.add_argument("--timeout", type=int, default=20, help="HTTP timeout in seconds (default: 20).")
args = parser.parse_args(argv)
token = args.token or os.getenv("TODOIST_API_TOKEN") or os.getenv("TODOIST_TOKEN")
if not token:
emit({"ok": False, "error": "Missing Todoist token. Pass --token or set TODOIST_API_TOKEN."})
return 2
url = args.base_url.rstrip("/") + "/projects?" + urllib.parse.urlencode({"limit": 1})
request = urllib.request.Request(
url=url,
method="GET",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"User-Agent": USER_AGENT,
},
)
try:
with urllib.request.urlopen(request, timeout=args.timeout) as response:
body = response.read().decode("utf-8", errors="replace")
payload = json.loads(body) if body else {}
except urllib.error.HTTPError as exc:
try:
payload = json.loads(exc.read().decode("utf-8", errors="replace"))
except Exception:
payload = {"error": f"HTTP {exc.code}"}
emit({"ok": False, "status": exc.code, "details": payload})
return 3 if exc.code in {401, 403} else 5
except urllib.error.URLError as exc:
emit({"ok": False, "error": f"Network error: {exc.reason}"})
return 6
results = payload.get("results", []) if isinstance(payload, dict) else []
emit(
{
"ok": True,
"checked": {
"auth": True,
"projects_endpoint": True,
"paginated_shape": isinstance(payload, dict) and "results" in payload and "next_cursor" in payload,
},
"sample_project_count": len(results),
"next_cursor": payload.get("next_cursor") if isinstance(payload, dict) else None,
}
)
return 0
if __name__ == "__main__":
raise SystemExit(main())