workctl
v0.2.1
Published
Unified CLI for task management across providers (ClickUp, Jira, etc.)
Maintainers
Readme
workctl
Unified CLI for task management across providers. One interface, multiple backends.
Currently supports ClickUp, with Jira and others on the roadmap. Designed for two audiences: developers working from the terminal, and AI agents / CI pipelines consuming structured JSON.
Table of Contents
- Installation
- Configuration
- Command Reference
- Working with Tasks
- Agent and Programmatic Use
- Architecture
- Development
- License
Installation
npm install -g workctlRequires Node.js >= 20.
Configuration
Interactive setup
workctl config initThe prompts guide you through:
- Generating an API token at https://app.clickup.com/settings/integrations/api
- Finding your Workspace ID at https://app.clickup.com/settings/workspace (the last number in the URL)
Configuration is stored in ~/.config/workctl/config.json (mode 0600).
Non-interactive setup
For CI or agent environments:
workctl config init --api-key pk_xxx --team-id 12345 --format jsonAlternatively, skip the config file entirely and run with environment variables only.
Environment variables
Env vars always take precedence over file values:
| Variable | Purpose |
|----------|---------|
| WORKCTL_CLICKUP_API_KEY | API token |
| WORKCTL_CLICKUP_TEAM_ID | Workspace ID |
| WORKCTL_PROVIDER | Provider name (default: clickup) |
| WORKCTL_PROFILE | Profile from the global config to apply (overrides .workctl.json's profile) |
With WORKCTL_CLICKUP_API_KEY and WORKCTL_CLICKUP_TEAM_ID set, no config file is needed.
Profiles
The global config supports a profiles map for working across multiple workspaces or clients:
{
"provider": "clickup",
"clickup": { "apiKey": "pk_xxx", "teamId": "111" },
"profiles": {
"acme": {
"clickup": { "teamId": "222" },
"defaults": { "listId": "list-acme" },
"statusShortcuts": { "done": "shipped" }
}
}
}A profile overlays the base config: any field it defines wins over the global value. Select one via .workctl.json ("profile": "acme") or the WORKCTL_PROFILE env var (which beats the repo file).
Per-repo config (.workctl.json)
Drop a .workctl.json at your repo root. It is discovered walking up from the current directory, stopping at the .git boundary:
{
"profile": "acme",
"defaults": { "listId": "list456", "branchPrefix": "feat" },
"statusShortcuts": { "done": "shipped" }
}Rules:
- Credentials are rejected in
.workctl.json. API keys live only in the global config. - Precedence (highest first): env vars >
.workctl.json> referenced profile > global config. - With
defaults.listIdset,task listandtask createno longer need--list.
Command Reference
All commands accept --format json for structured output.
| Command | Description |
|---------|-------------|
| task search | Search tasks across the entire workspace |
| task mine | List tasks assigned to you |
| task list --list <ID> | List tasks from a specific list |
| task view [ID] | View task details; --tree includes subtasks |
| task create <NAME> --list <ID> | Create a task; --if-not-exists for idempotency, --parent for subtasks |
| task update <ID> | Update status, priority, name, description, tags, due date, custom fields |
| task start [ID] | Move a task to the in-progress status of its list |
| task done [ID] | Move a task to the done status of its list |
| task branch <ID> | Create and switch to a git branch named after the task |
| task pr-link [ID] | Comment the current branch's PR URL on the task |
| task comments <ID> | List comments of a task |
| task comment <ID> <TEXT> | Add a comment to a task |
| task move <ID> --to-list <ID> | Move a task to a different list |
| task delete <ID> | Delete a task |
| task bulk | Bulk update tasks matching search filters; --dry-run to preview |
| workspace spaces | List spaces in the workspace |
| workspace lists <SPACE_ID> | List task lists in a space |
| list statuses <LIST_ID> | Show the valid statuses of a list |
| config init | Configure API keys and provider |
| introspect | Show the full API contract (for agents/tooling) |
Commands with [ID] accept an optional task ID: when omitted, the ID is derived from the current git branch (see Git bridge).
Working with Tasks
Task IDs
All commands accept both internal IDs (abc123) and custom IDs (DEV-123). Custom IDs are auto-detected.
The literal me resolves to your user ID wherever an assignee is accepted:
workctl task mine --status "in progress"
workctl task search --assignee me --format jsonGit bridge
task branch creates a branch named <prefix>/<id>-<slug> from the task name and switches to it. On such a branch, view, start, done and pr-link no longer need an explicit task ID:
workctl task branch DEV-123 # -> feat/DEV-123-fix-login-flow
workctl task start # moves DEV-123 to in-progress
gh pr create && workctl task pr-link # comments the PR URL on DEV-123
workctl task done # moves DEV-123 to donePrefix resolution: --prefix flag > defaults.branchPrefix in config or .workctl.json > feat.
task pr-link detects the PR of the current branch via the gh CLI; pass --url <URL> to skip detection (useful in CI or without GitHub).
Status shortcuts
task start / task done resolve the right status per list: done picks the first done-type status, start the first custom-type status by order. Lists with several custom statuses can make start ambiguous — pin it explicitly with statusShortcuts in the config or .workctl.json:
{ "statusShortcuts": { "start": "in development", "done": "shipped" } }Inspect a list's statuses with workctl list statuses <LIST_ID>.
Custom fields
task view shows custom fields with values; JSON output includes them as customFields. Dropdown and label fields are resolved to their option names on read.
Set fields with --field "Name=value" (repeatable). Field names are case-insensitive; dropdown/labels accept option names (labels take comma-separated values); numbers, dates (ISO) and checkboxes (true/false) are coerced automatically:
workctl task update DEV-123 --field "Severity=high" --field "Env=staging"
workctl task update DEV-123 --field "Areas=backend,infra"Filter search results by field value (applied client-side on the fetched pages — combine with --all for workspace-wide filtering):
workctl task search --status open --field "Sprint=12" --all --format jsonUnknown field or option names fail with the list of available ones.
Subtasks
Create a subtask with --parent; inspect a task's direct subtasks with --tree:
workctl task create "Write tests" --list abc123 --parent DEV-123
workctl task view DEV-123 --treeIn JSON output, --tree adds a subtasks array to the task object.
Idempotent creation
--if-not-exists skips creation when an open task with the same name (case-insensitive, exact match) already exists in the list, returning the existing task instead. Useful for agent loops and CI jobs that may retry:
workctl task create "Nightly report" --list abc123 --if-not-exists --format jsonIn JSON output the existing task carries "created": false.
Tags and due dates
workctl task create "Fix bug" --list abc123 --tags "bug,frontend" --due 2025-02-15
workctl task update DEV-123 --tags "bug" # replaces the full tag set
workctl task update DEV-123 --clear-due
workctl task search --tag bug --due-before 2025-03-01Note (ClickUp): the update endpoint ignores tags, so --tags diffs against the current tags via the tag endpoints — this costs one extra GET per update.
Markdown descriptions
Descriptions support full Markdown, inline or from a file:
workctl task create "Feature spec" --list abc123 --description-file ./spec.md
workctl task update DEV-123 --description-file ./updated-spec.mdIn task view (human mode), Markdown is rendered for the terminal.
Search and pagination
task search supports repeatable filters (--status, --assignee, --tag), date ranges and closed-task inclusion:
workctl task search --created-after "2025-01-01" --updated-before "2025-03-31" --format json
workctl task search --assignee me --include-closed --format jsontask list, task search and task mine accept --page <N> or --all (fetches every page, capped at 50 with a warning on stderr).
Bulk updates
task bulk combines search filters with a set of updates. Always preview first:
workctl task bulk --filter-status "in review" --set-status "done" --dry-run --format json
workctl task bulk --filter-status "in review" --set-status "done" --format jsonUpdates run with bounded concurrency. Failures do not abort the run: the result reports updated, taskIds and a failed array with per-task errors. There is no rollback — the underlying APIs are not transactional.
Moving tasks (ClickUp limitation)
ClickUp API v2 has no native "move task" endpoint. task move emulates it by adding the task to the destination list and removing it from the current one. This requires the "Tasks in Multiple Lists" ClickApp enabled in your Workspace, and ClickUp does not allow removing a task from its home list — in that case the task ends up in both lists and the command reports it.
workctl task move DEV-123 --to-list list456 --format jsonAgent and Programmatic Use
Every command supports --format json:
- stdout: JSON result data
- stderr: JSON error objects (
{ "error", "code", "message" })
Exit codes are stable and mapped to error codes:
| Exit code | Error code | Meaning |
|-----------|------------|---------|
| 0 | — | Success |
| 1 | VALIDATION_ERROR / unknown | Invalid input or unexpected failure |
| 2 | CONFIG_ERROR | Configuration missing or invalid |
| 3 | AUTH_ERROR | Authentication failed |
| 4 | PROVIDER_ERROR | Provider API error |
| 5 | RATE_LIMIT | Rate limit exceeded (after automatic retries) |
The HTTP client retries on 429 (honoring Retry-After) and 5xx with exponential backoff, and preemptively throttles using the provider's rate-limit headers.
Get the full machine-readable API contract (commands, flags, output examples, error specs, workflows):
workctl introspect --format jsonGenerate a .cursor/rules/workctl.mdc file for LLM workspace context:
workctl introspect --emit-rulesGenerate a Claude Code skill (auto-loaded when task-management context appears):
# User-global: ~/.claude/skills/workctl/SKILL.md
workctl introspect --emit-rules --target claude
# Custom directory (e.g. project-level or ctk skill distribution)
workctl introspect --emit-rules --target claude --out .claude/skillsTo let Claude Code run workctl without permission prompts, add to .claude/settings.json:
{ "permissions": { "allow": ["Bash(workctl:*)"] } }When installed via claude-master-toolkit, ctk install claude can invoke the --out form to place the skill alongside the rest of the ctk skill set.
Architecture
src/
commands/ CLI commands (oclif)
domain/ Models and ports (TaskProvider interface)
providers/ Provider implementations (ClickUp, ...)
shared/ Base command, errors, contract, formatters
lib/ HTTP client, config service, git service, loggerHexagonal architecture: commands depend on ports (interfaces), not implementations. Adding a new provider means implementing TaskProvider and registering it in the factory.
Development
git clone https://github.com/itboxful/workctl.git
cd workctl
npm install
npm run build
./bin/dev.js task searchRun tests:
npm testLicense
MIT
