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
parent
6250918e5e
commit
0d5450fb65
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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. "
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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"
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue