Add ClickUp task creation via chat tool, skill, and CLI script

New create_task() and find_list_in_folder() methods on ClickUpClient,
clickup_create_task chat tool, create-task skill, and CLI script for
creating tasks in a client's Overall list with Client/Work Category fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix/customer-field-migration
PeninsulaInd 2026-03-11 12:19:54 -05:00
parent 6250918e5e
commit 0d5450fb65
5 changed files with 382 additions and 0 deletions

View File

@ -469,6 +469,70 @@ class ClickUpClient:
log.warning("Failed to read field '%s' from task %s: %s", field_name, task_id, e)
return None
def create_task(
self,
list_id: str,
name: str,
description: str = "",
status: str = "to do",
due_date: int | None = None,
tags: list[str] | None = None,
custom_fields: list[dict] | None = None,
) -> dict:
"""Create a new task in a ClickUp list.
Args:
list_id: The list to create the task in.
name: Task name.
description: Task description (markdown supported).
status: Initial status (default "to do").
due_date: Due date as Unix timestamp in milliseconds.
tags: List of tag names to apply.
custom_fields: List of custom field dicts ({"id": ..., "value": ...}).
Returns:
API response dict containing task id, url, etc.
"""
payload: dict[str, Any] = {"name": name, "status": status}
if description:
payload["description"] = description
if due_date is not None:
payload["due_date"] = due_date
if tags:
payload["tags"] = tags
if custom_fields:
payload["custom_fields"] = custom_fields
def _call():
resp = self._client.post(f"/list/{list_id}/task", json=payload)
resp.raise_for_status()
return resp.json()
result = self._retry(_call)
log.info("Created task '%s' in list %s (id: %s)", name, list_id, result.get("id"))
return result
def find_list_in_folder(
self, space_id: str, folder_name: str, list_name: str = "Overall"
) -> str | None:
"""Find a list within a named folder in a space.
Args:
space_id: ClickUp space ID.
folder_name: Folder name to match (case-insensitive).
list_name: List name within the folder (default "Overall").
Returns:
The list_id if found, or None.
"""
folders = self.get_folders(space_id)
for folder in folders:
if folder["name"].lower() == folder_name.lower():
for lst in folder["lists"]:
if lst["name"].lower() == list_name.lower():
return lst["id"]
return None
def discover_field_filter(self, list_id: str, field_name: str) -> dict[str, Any] | None:
"""Discover a custom field's UUID and dropdown option map.

View File

@ -160,6 +160,71 @@ def clickup_task_status(task_id: str, ctx: dict | None = None) -> str:
return "\n".join(lines)
@tool(
"clickup_create_task",
"Create a new ClickUp task for a client. Requires task name and client name. "
"Optionally set work category, description, and status. "
"The task is created in the 'Overall' list within the client's folder.",
category="clickup",
)
def clickup_create_task(
name: str,
client: str,
work_category: str = "",
description: str = "",
status: str = "to do",
ctx: dict | None = None,
) -> str:
"""Create a new ClickUp task in the client's Overall list."""
client_obj = _get_clickup_client(ctx)
if not client_obj:
return "Error: ClickUp API token not configured."
cfg = ctx["config"].clickup
if not cfg.space_id:
return "Error: ClickUp space_id not configured."
try:
# Find the client's Overall list
list_id = client_obj.find_list_in_folder(cfg.space_id, client)
if not list_id:
return f"Error: Could not find folder '{client}' with an 'Overall' list in space."
# Create the task
result = client_obj.create_task(
list_id=list_id,
name=name,
description=description,
status=status,
)
task_id = result.get("id", "")
task_url = result.get("url", "")
# Set Client dropdown field
client_obj.set_custom_field_by_name(task_id, "Client", client)
# Set Work Category if provided
if work_category:
field_info = client_obj.discover_field_filter(list_id, "Work Category")
if field_info and work_category in field_info["options"]:
option_id = field_info["options"][work_category]
client_obj.set_custom_field_value(task_id, field_info["field_id"], option_id)
else:
log.warning("Work Category '%s' not found in dropdown options", work_category)
return (
f"Task created successfully!\n"
f" Name: {name}\n"
f" Client: {client}\n"
f" ID: {task_id}\n"
f" URL: {task_url}"
)
except Exception as e:
return f"Error creating task: {e}"
finally:
client_obj.close()
@tool(
"clickup_reset_task",
"Reset a ClickUp task to 'to do' status so it can be retried on the next poll. "

View File

@ -0,0 +1,89 @@
"""CLI script to create a ClickUp task in a client's Overall list.
Usage:
uv run python scripts/create_clickup_task.py --name "Task" --client "Client"
uv run python scripts/create_clickup_task.py --name "PR" --client "Acme" \\
--category "Press Release"
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
# Add project root to path so we can import cheddahbot
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from dotenv import load_dotenv
from cheddahbot.clickup import ClickUpClient
def main():
load_dotenv()
parser = argparse.ArgumentParser(description="Create a ClickUp task")
parser.add_argument("--name", required=True, help="Task name")
parser.add_argument("--client", required=True, help="Client folder name in ClickUp")
parser.add_argument("--category", default="", help="Work Category (e.g. 'Press Release')")
parser.add_argument("--description", default="", help="Task description")
parser.add_argument("--status", default="to do", help="Initial status (default: 'to do')")
args = parser.parse_args()
api_token = os.environ.get("CLICKUP_API_TOKEN", "")
space_id = os.environ.get("CLICKUP_SPACE_ID", "")
if not api_token:
print("Error: CLICKUP_API_TOKEN not set", file=sys.stderr)
sys.exit(1)
if not space_id:
print("Error: CLICKUP_SPACE_ID not set", file=sys.stderr)
sys.exit(1)
client = ClickUpClient(api_token=api_token)
try:
# Find the client's Overall list
list_id = client.find_list_in_folder(space_id, args.client)
if not list_id:
msg = f"Error: No folder '{args.client}' with 'Overall' list"
print(msg, file=sys.stderr)
sys.exit(1)
# Create the task
result = client.create_task(
list_id=list_id,
name=args.name,
description=args.description,
status=args.status,
)
task_id = result.get("id", "")
# Set Client dropdown field
client.set_custom_field_by_name(task_id, "Client", args.client)
# Set Work Category if provided
if args.category:
field_info = client.discover_field_filter(list_id, "Work Category")
if field_info and args.category in field_info["options"]:
option_id = field_info["options"][args.category]
client.set_custom_field_value(task_id, field_info["field_id"], option_id)
else:
msg = f"Warning: Work Category '{args.category}' not found"
print(msg, file=sys.stderr)
print(json.dumps({
"id": task_id,
"name": args.name,
"client": args.client,
"url": result.get("url", ""),
"status": args.status,
}, indent=2))
finally:
client.close()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,29 @@
---
name: create-task
description: Create new ClickUp tasks for clients. Use when the user asks to create, add, or make a new task.
tools: [clickup_create_task]
agents: [default]
---
# Create ClickUp Task
Creates a new task in the client's "Overall" list in ClickUp.
## Required Information
- **name**: The task name (e.g., "Write blog post about AI trends")
- **client**: The client/folder name in ClickUp (e.g., "Acme Corp")
## Optional Information
- **work_category**: The work category dropdown value (e.g., "Press Release", "Link Building", "Content Creation", "On Page Optimization")
- **description**: Task description (supports markdown)
- **status**: Initial status (default: "to do")
## Examples
"Create a press release task for Acme Corp about their new product launch"
-> name: "Press Release - New Product Launch", client: "Acme Corp", work_category: "Press Release"
"Add a link building task for Widget Co"
-> name: "Link Building Campaign", client: "Widget Co", work_category: "Link Building"

View File

@ -434,3 +434,138 @@ class TestClickUpClient:
assert result is None
client.close()
@respx.mock
def test_create_task(self):
respx.post(f"{BASE_URL}/list/list_1/task").mock(
return_value=httpx.Response(
200,
json={
"id": "new_task_1",
"name": "Test Task",
"url": "https://app.clickup.com/t/new_task_1",
},
)
)
client = ClickUpClient(api_token="pk_test")
result = client.create_task(
list_id="list_1",
name="Test Task",
description="A test description",
status="to do",
)
assert result["id"] == "new_task_1"
assert result["url"] == "https://app.clickup.com/t/new_task_1"
request = respx.calls.last.request
import json
body = json.loads(request.content)
assert body["name"] == "Test Task"
assert body["description"] == "A test description"
assert body["status"] == "to do"
client.close()
@respx.mock
def test_create_task_with_optional_fields(self):
respx.post(f"{BASE_URL}/list/list_1/task").mock(
return_value=httpx.Response(
200,
json={"id": "new_task_2", "name": "Tagged Task", "url": ""},
)
)
client = ClickUpClient(api_token="pk_test")
result = client.create_task(
list_id="list_1",
name="Tagged Task",
due_date=1740000000000,
tags=["urgent", "mar26"],
custom_fields=[{"id": "cf_1", "value": "opt_1"}],
)
assert result["id"] == "new_task_2"
import json
body = json.loads(respx.calls.last.request.content)
assert body["due_date"] == 1740000000000
assert body["tags"] == ["urgent", "mar26"]
assert body["custom_fields"] == [{"id": "cf_1", "value": "opt_1"}]
client.close()
@respx.mock
def test_find_list_in_folder_found(self):
respx.get(f"{BASE_URL}/space/space_1/folder").mock(
return_value=httpx.Response(
200,
json={
"folders": [
{
"id": "f1",
"name": "Acme Corp",
"lists": [
{"id": "list_overall", "name": "Overall"},
{"id": "list_archive", "name": "Archive"},
],
},
{
"id": "f2",
"name": "Widget Co",
"lists": [
{"id": "list_w_overall", "name": "Overall"},
],
},
]
},
)
)
client = ClickUpClient(api_token="pk_test")
result = client.find_list_in_folder("space_1", "Acme Corp")
assert result == "list_overall"
client.close()
@respx.mock
def test_find_list_in_folder_case_insensitive(self):
respx.get(f"{BASE_URL}/space/space_1/folder").mock(
return_value=httpx.Response(
200,
json={
"folders": [
{
"id": "f1",
"name": "Acme Corp",
"lists": [{"id": "list_overall", "name": "Overall"}],
},
]
},
)
)
client = ClickUpClient(api_token="pk_test")
result = client.find_list_in_folder("space_1", "acme corp")
assert result == "list_overall"
client.close()
@respx.mock
def test_find_list_in_folder_not_found(self):
respx.get(f"{BASE_URL}/space/space_1/folder").mock(
return_value=httpx.Response(
200,
json={
"folders": [
{
"id": "f1",
"name": "Acme Corp",
"lists": [{"id": "list_1", "name": "Overall"}],
},
]
},
)
)
client = ClickUpClient(api_token="pk_test")
result = client.find_list_in_folder("space_1", "NonExistent Client")
assert result is None
client.close()