5.5 KiB
Live Artifacts
A live artifact is one that fetches its ClickUp data at view time instead of shipping a copy. Use it for anything that should reflect the workspace as it is now: task boards, dashboards, sprint trackers, inboxes.
Hardcode what the artifact is about, never what it shows. The ids of the list, space or tasks the user pointed at go in the code; everything about them (names, statuses, assignees, counts) is fetched at view time. Baked-in data goes stale and leaks: every viewer would see what you had access to. Reads and writes run as the viewer, so what they see depends on who they are and can be empty. The artifact-creator skill's safety rules still apply.
Read live data
The kit is in every project as kits/clickup; use one hook per kind:
import { PriorityFlag, StatusBadge, useLists, useTasks } from "@kits/clickup";
const SPRINT_LIST_ID = "901234567"; // the list the user asked about
export default function App() {
const list = useLists([SPRINT_LIST_ID]);
const tasks = useTasks({
parent: SPRINT_LIST_ID,
assignees: ["me"],
dueAt: "overdue",
});
return (
<main>
<h1>{list.data?.[0]?.name ?? "Sprint"}</h1>
<Overdue tasks={tasks} />
</main>
);
}
function Overdue({ tasks }) {
if (tasks.error) {
return <p role="alert">Live data could not load: {tasks.error.message}</p>;
}
if (tasks.isPending) {
return <p aria-busy="true">Loading…</p>;
}
if (tasks.data.length === 0) {
return <p>Nothing of yours is overdue.</p>;
}
return (
<ul>
{tasks.data.map((task) => (
<li key={task.id} className="flex items-center gap-2">
<PriorityFlag priority={task.priority} />
{task.name}
<StatusBadge className="text-xs" status={task.status} />
</li>
))}
</ul>
);
}
Kinds
The kit covers spaces, folders, lists, custom fields, tasks with their statuses and types, comments, users, teams, inbox, reminders and search. Each kind has a use<Kinds> hook; clickup.<kinds> has the same reads as plain promises, plus the writes. Before using a kind, read its reference file in kits/clickup/docs/: fields, selector, filters and writes. Spaces, folders and lists are in hierarchy.d.ts, users and teams in people.d.ts. Avatar, AvatarGroup, StatusBadge, PriorityFlag, Tag and CustomFieldValue render assignees, statuses, priorities, tags and custom fields; RichText renders a task or comment body (docs/components.d.ts). kits/clickup/src/ is the kit's source, there when a question outlives the docs; read docs/ first.
clickup.viewer is the user looking at the artifact; clickup.navigate(url) opens a ClickUp url; clickup.askBrain(prompt) sends a prompt to Brain.
How the kit works
- Every asset is a
Ref,{ kind, id, name, url }, plus the fields of its kind; full assets also carryancestors, from the space down. - Hooks return TanStack Query v5's
useQueryresult (data,error,isPending,isFetching, and the rest) and take itsenabled,placeholderDataandrefetchIntervaloptions. The same selector shares one request across components. A hook first shows what the viewer saw last time, then loads fresh data, sodatais often set whileisFetchingis true: keep showing it rather than a spinner. - Every failure is a
LiveErrorwith acode.invalid: your selector or input is wrong; fix the code.forbidden: the viewer is not allowed to; hide the feature.failed: something went wrong; retry whenretryable.denied: the viewer declined a write. - Dates are ISO 8601 strings; inputs also take
YYYY-MM-DD.
Writes
There are no write hooks. Create and update with the clickup methods (clickup.tasks.update(task, patch), clickup.tasks.create(input), and so on), wrapped in useMutation({ mutationFn }) and called from an event handler. Every changed field needs the viewer's approval in ClickUp, so a write stops at the first denial and can stay pending for a while. The kit does the rest: lists on screen update optimistically, roll back if the viewer declines, and refetch once ClickUp confirms. Don't refetch() after a write or mirror it in local state.
import { clickup, useMutation, useStatuses } from "@kits/clickup";
export default function TaskRow({ task }) {
const list = task.ancestors.find((ref) => ref.kind === "list");
const statuses = useStatuses({ parent: list }, { enabled: !!list });
const done = statuses.data?.find((status) => status.type === "done");
const complete = useMutation({
mutationFn: (status) => clickup.tasks.update(task, { status }),
});
return (
<li>
{task.name}
<button
disabled={!done || complete.isPending}
onClick={() => complete.mutate(done)}
>
{complete.isPending ? "Waiting for approval…" : "Mark done"}
</button>
{complete.isError && <p role="alert">{complete.error.message}</p>}
</li>
);
}
Design for data you have not seen
What you see while authoring is a sample: for another viewer any optional field can be missing, text can be far longer or shorter, and a query can return nothing or hit the limit. Design the loading, empty and error states: keep the layout with a skeleton while loading, say what came up empty, and when live data could not load, say so and keep the rest usable. Never invent replacement data, and render live values as text, never as HTML.
The sandbox preview does not restrict reads and only pretends to write, so you will never see a forbidden state there and nothing changes after a write. Design both anyway.