A single-file program (clickup_tasks.py) that calls the ClickUp REST API v2 directly to
create a task -> get its task ID -> change its status, all in one place. No external SDK — just requests.
(ClickUp has no official Python SDK; its own guidance is to call the REST API directly.)
task.py adds a thin workflow layer on top: turn a command into a task in the progress status,
then flip it to review when the work is done.
The intended loop: the assistant (Claude) turns a user command into an English task name + description, registers it in ClickUp as progress, does the work, then moves it to review.
# one-time setup
python task.py lists # find a list_id
python task.py config --list-id 901817138679 # remember it (this repo: "Cycle Task Lists")
# per command
python task.py start --name "Fix login redirect loop" \
--description "Users bounce between /login and /home after SSO; trace the redirect."
# -> creates the task in "progress", PRINTS the task_id, and tracks it by that id
python task.py current # list every tracked task by id + status
python task.py review --task-id <task_id> # when done -> moves THAT task to "review"startresolves the configured start status (progress) against the list's real statuses fuzzily (soprogressalso matchesin progress), and tracks the new task by its id in.task_state.json(shape:{"tasks": {<id>: {...}}, "order": [...]}).- Status is changed per task id, not via a shared "current".
review/set-statusrequire--task-id— printed bystartand listed bycurrent. This is deliberate: severalstarts (e.g. concurrent sessions writing the same state file) each track their own id, so a status change can only ever move the id you name. (The old behavior moved the most-recently started task, which silently moved the wrong one when another session had started a task in between.) startalso assigns the task to you by default (the token owner; auto-detected and cached asassignee_id). Override with--assignee <user_id>, or--no-assignto skip; set a default withconfig --assignee me|<id>|none.reviewre-fetches the task, finds its list's statuses, fuzzy-matchesreview, and updates it.set-status --task-id <id> --status <name>moves any task to any status (also fuzzy-matched).- Token is read from
.env(or theCLICKUP_TOKENenv var);.env,.task_state.json, and.task_config.jsonare git-ignored.
pip install -r requirements.txt- Log in to ClickUp -> avatar (top right) -> Settings -> Apps (left sidebar)
- Under API Token, click Generate and copy the
pk_...token - Set it as an environment variable (a personal token goes into the header RAW, with no
Bearerprefix)
# macOS / Linux
export CLICKUP_TOKEN=pk_xxxxxxxx
# Windows PowerShell
$env:CLICKUP_TOKEN = "pk_xxxxxxxx"Only an OAuth access token uses the
Bearer {token}form. A personal token is used as-is.
# 0) Verify authentication
python clickup_tasks.py whoami
# 1) Find the lists you can access and their list_id
python clickup_tasks.py lists
# 2) See the status names available in a list (statuses differ per list!)
python clickup_tasks.py statuses --list-id 901234567
# 3) Full demo: create -> print ID -> change status -> re-fetch to verify
python clickup_tasks.py demo --list-id 901234567
# Individual actions
python clickup_tasks.py create --list-id 901234567 --name "My task" --status "to do" --priority 3
python clickup_tasks.py set-status --task-id 86abc12 --status "in progress"
python clickup_tasks.py get --task-id 86abc12 --json| Step | Method & endpoint | Notes |
|---|---|---|
| Create | POST /list/{list_id}/task |
body {"name": "..."} (only name is required). The response's top-level id is the new task ID |
| Read valid statuses | GET /list/{list_id} |
the response's statuses[].status are the valid status names for that list |
| Change status | PUT /task/{task_id} |
body {"status": "..."} — PUT (not PATCH). The name is case-insensitive |
- Base URL:
https://api.clickup.com/api/v2 - Auth header:
Authorization: pk_...(raw) - Statuses are not a global enum — they are defined per Space/Folder/List. So before the PUT,
set-statuschecks the name against that list's valid statuses (case-insensitive) and returns a friendly message instead of a 400 if it's wrong. (Disable with--no-validate.)
Free / Unlimited / Business plans allow 100 req/min per token; exceeding it returns HTTP 429.
The client automatically waits until the X-RateLimit-Reset (Unix seconds) header and retries.
(The Retry-After header is not guaranteed by ClickUp's docs, so X-RateLimit-Reset takes priority.)
- Authentication: https://developer.clickup.com/docs/authentication
- Create Task: https://developer.clickup.com/reference/createtask
- Update Task: https://developer.clickup.com/reference/updatetask
- Get List (statuses): https://developer.clickup.com/reference/getlist
- Rate limits: https://developer.clickup.com/docs/rate-limits