No description
  • Emacs Lisp 99.8%
  • Makefile 0.2%
Find a file
natsukium 1bd81db738
refactor(custom-fields): prefix properties CLICKUP_FIELD_ not CF_
`CF_' is not a generally recognized abbreviation for "custom field"
(it reads as CompactFlash, Cloudflare, cash flow, ...), and it was the
only property outside the `CLICKUP_' namespace every other managed
property uses. Switch the prefix to `CLICKUP_FIELD_', which is
self-explanatory, consistent with the rest of the drawer, and mirrors
the API's /list/{id}/field endpoint. The prefix now lives in one
constant, `org-clickup-custom-field-property-prefix'. A re-pull
rewrites existing CF_ drawers.

Assisted-by: Claude Code Opus 4.8
2026-06-15 17:34:50 +09:00
docs refactor(custom-fields): prefix properties CLICKUP_FIELD_ not CF_ 2026-06-15 17:34:50 +09:00
lisp refactor(custom-fields): prefix properties CLICKUP_FIELD_ not CF_ 2026-06-15 17:34:50 +09:00
test refactor(custom-fields): prefix properties CLICKUP_FIELD_ not CF_ 2026-06-15 17:34:50 +09:00
.gitignore build: scaffold project with Makefile, minimal entry point, and PBT test helper 2026-05-17 14:44:26 +09:00
Makefile feat(api): HTTP execution, rate-limit governor, and tasks pagination 2026-05-17 14:53:19 +09:00
README.org refactor(custom-fields): prefix properties CLICKUP_FIELD_ not CF_ 2026-06-15 17:34:50 +09:00

org-clickup

Sync ClickUp tasks with org-mode TODO entries from inside Emacs.

Status: pre-release (0.1.0-pre). The package is usable end-to-end for personal task tracking but the API surface may still shift. See the roadmap for what's done and what's coming.

What it does

  • Pulls tasks from ClickUp into one org file per List, with watermark-based incremental sync.
  • Pushes edits back to ClickUp: name, description, status, priority, due/start dates, time estimate, assignees, tags.
  • Renders display names (alice,bob, Engineering, ACME) alongside the raw IDs so the org buffer reads like a project view rather than a database dump.
  • Nests subtasks under their parent on pull, and creates subtasks on ClickUp when you write a heading under another task in org.
  • Detects server-side conflicts before pushing, with a configurable resolution policy.
  • Skips redundant pushes via a content hash, so a sync after push is a no-op.
  • Lets you read and post comments on demand (no per-task GET on every sync).

Requirements

  • Emacs 29.1 or newer (uses built-in json-parse-string, json-serialize, secure-hash, auth-source, modern org).
  • A ClickUp Personal API Token (pk_...). Generate one at https://app.clickup.com/settings/apps.

Installation

Until the package is on a package archive, clone the repo and add it to your load-path:

(add-to-list 'load-path "/path/to/org-clickup/lisp")
(require 'org-clickup)
(require 'org-clickup-setup)
(require 'org-clickup-sync)

Setup

Run the first-run wizard once:

M-x org-clickup-setup

It will:

  1. Prompt you for the pk_... token (read via read-passwd).
  2. Validate it against GET /team.
  3. Let you pick a workspace if you have more than one.
  4. Persist the token via auth-source (so ~/.authinfo.gpg if you use GPG, otherwise wherever your auth-source chain points).
  5. Save the chosen workspace id as org-clickup-team-id via Customize.

Per-file configuration

Each org file that mirrors a ClickUp List gets a small header. Use the discovery wizard to drill down through Workspace → Space → Folder → List and write the header for you:

M-x org-clickup-insert-list-header

Or write it by hand:

#+TITLE: Engineering tasks
#+CLICKUP_TEAM_ID: 1234567
#+CLICKUP_LIST_ID: 901234567890

#+CLICKUP_LIST_ID: is optional — if you omit it, sync hits the workspace-wide Get Filtered Team Tasks endpoint instead of the per-List one.

To pull across multiple Lists, or to filter by assignee, use the optional CSV keywords:

#+CLICKUP_LIST_IDS:  901,902,903   ; multi-List filter (workspace-wide)
#+CLICKUP_ASSIGNEES: 12,34         ; filter to those user IDs

#+CLICKUP_LIST_IDS: takes precedence over #+CLICKUP_LIST_ID: and routes through Get Filtered Team Tasks with list_ids[], since the per-List endpoint can only scope to one List. #+CLICKUP_ASSIGNEES: applies to whichever endpoint is selected.

The discovery wizard (org-clickup-insert-list-header) also asks whether to add an assignee filter and, if yes, lets you multi-select workspace members; the resulting #+CLICKUP_ASSIGNEES: line is written alongside the team/list keywords. At the Space picker, the wizard surfaces a (all Lists in workspace) entry — pick it to skip the rest of the drill-down and write a workspace-wide header (no #+CLICKUP_LIST_ID:), useful for a personal "everything assigned to me" file when combined with #+CLICKUP_ASSIGNEES:.

Workflow

Pull from ClickUp

M-x org-clickup-sync-buffer

Incrementally fetches tasks updated since the last successful sync (per-file watermark). On the first run, fetches everything.

Pull every configured file at once

M-x org-clickup-sync

Sweeps every file in org-clickup-sync-files and pulls each one — without needing to visit the buffer first. Files org-clickup opens itself are saved and closed afterwards; a buffer you already had open is left modified for you to review. One file failing (missing file, no team id, network error) does not abort the rest; failures are reported at the end.

org-clickup-sync-files is populated automatically the first time org-clickup-insert-list-header writes a header into a file. You can also edit the list via M-x customize-variable RET org-clickup-sync-files, or drive a periodic background pull from a timer:

(run-at-time t 900 #'org-clickup-sync)   ; every 15 minutes

Edit a task in org

Just edit the headline, description (inside the :CLICKUP: drawer), DEADLINE, SCHEDULED, EFFORT, etc. — or change the values inside the property drawer (:CLICKUP_STATUS:, :CLICKUP_PRIORITY:, :CLICKUP_ASSIGNEE_IDS:, etc.).

Custom fields

Custom field values render into the property drawer on pull, each under a property named after the field (so Story Points becomes :CLICKUP_FIELD_STORY_POINTS:). The field's ClickUp id rides alongside in a :CLICKUP_FIELD_..._ID: companion — that is what identifies the field on push, so you read and edit by name and never have to know the raw id:

:PROPERTIES:
,...
:CLICKUP_FIELD_STORY_POINTS:    5
:CLICKUP_FIELD_STORY_POINTS_ID: a1b2c3d4-...
:CLICKUP_FIELD_PRIORITY:        High
:CLICKUP_FIELD_PRIORITY_ID:     e5f6...
:END:

Edit the value like any property — C-c C-x p on a drop-down or label field completes its configured options — and push it back with org-clickup-push-at-point. Each changed field is sent to ClickUp's POST /task/{id}/field/{id} endpoint (one call per field), encoded for its type, and only when it actually differs from the server. Custom-field push needs the pre-push conflict GET, so it is skipped under org-clickup-trust-local. Removing a property line does not delete the field on ClickUp.

To override the auto-generated property name on a given List, set org-clickup-custom-field-map (optional):

(setq org-clickup-custom-field-map
      '(("901234567890"            ; List ID
         ("Story Points" . "SP"))))

Push your edits

M-x org-clickup-push-at-point

The pipeline:

  1. Compares the entry's :CLICKUP_LOCAL_HASH: to a freshly computed one — if unchanged, skips silently.
  2. Pre-push GET to detect server-side conflicts (unless org-clickup-trust-local is t).
  3. PUTs scalar fields plus an assignees add/remove diff.
  4. Issues one POST/DELETE per added/removed tag.
  5. Writes the new :CLICKUP_UPDATED: and :CLICKUP_LOCAL_HASH: back to the entry.

Create a new task

Write a regular org TODO heading anywhere in a configured file, then:

M-x org-clickup-create-from-heading

If the heading sits under an existing ClickUp task (with a :CLICKUP_TASK_ID:), the new task is created as a subtask of that parent in ClickUp too.

Comments

M-x org-clickup-fetch-comments-at-point   ; pull all comments
M-x org-clickup-add-comment-at-point      ; post a new one

Comments render as level+1 sub-headings under the task, with :CLICKUP_COMMENT_ID: for in-place refresh on re-fetch.

Configuration

Common =defcustom=s:

Variable Default Effect
org-clickup-team-id nil Workspace ID. Set by the wizard.
org-clickup-conflict-strategy prompt prompt / server-wins / org-wins / abort on push-time conflict.
org-clickup-trust-local nil Skip the pre-push GET. Saves an API call; loses conflict detection and the assignees/tags diff.
org-clickup-include-closed t Include closed tasks on pull.
org-clickup-use-status-as-todo nil When non-nil, use ClickUp status verbatim as the TODO keyword (e.g. IN-PROGRESS); else collapse.
org-clickup-priority-map =((1 . ?A) (2 . ?A) …) = ClickUp priority int → org priority char.
org-clickup-drawer-name "CLICKUP" Name of the drawer holding the task description.
org-clickup-custom-field-map nil Optional per-List override of custom-field property names; by default the key is derived from the field name.

See the org-clickup Customize group for the full list.

Roadmap

Implemented (Phase 04):

  • Pure HTTP/SDK layer with rate-limit governor and pagination
  • Read-only sync with watermark-based incremental fetch
  • Onboarding wizard (org-clickup-setup) and List discovery
  • ID-to-name resolution in the property drawer
  • Scalar-field push with conflict detection and hash-skip
  • Assignees and tags diff push
  • Comments (on-demand fetch and add)
  • Subtask nesting on pull, parent-aware task creation
  • Registry-wide pull (org-clickup-sync over org-clickup-sync-files, no buffer needed)

Coming (Phase 5+, see docs/design.md):

  • Phase 5 — Custom fields (per-List name → property map): rendered on pull, pushed via POST /task/{id}/field/{id} with per-type encoding and diff-vs-server, plus C-c C-x p completion of drop-down/label options
  • Phase 6 — UX polish: transient menu, status cycle command, [[clickup:abc12]] link type
  • Phase 7 — Verbatim status mapping, property overrides
  • Phase 8 — Deletion reconciliation, backup retention, 3-way merge for conflict resolution
  • Filter keywords — #+CLICKUP_ASSIGNEES: (numeric IDs) and #+CLICKUP_LIST_IDS: (multi-List) shipped; me shorthand and #+CLICKUP_STATUSES: still pending
  • Org-capture and refile hooks
  • Buffer-wide push (org-clickup-push-buffer)

Development

make test         # run the full ert suite
make test-quick   # skip :slow tagged tests
make compile      # byte-compile lisp/*.el with -Werror

No external test dependencies — a tiny property-based testing helper is bundled in test/test-helper.el.

Architecture and rationale: docs/design.md. Prior-art studies (org-trello, org-jira, org-gcal) live alongside it in docs/prior-art/.