Production patterns for integrating with Taskade.
The@taskade/sdkpackage is in preview and not yet on public npm. These recipes use plain HTTP.They use a small
taskade(operation, body)helper for Action API v2. Some recipes also use REST API v1.The SDK Preview quickstart shows the planned generated client. The retry, pagination, idempotency, and testing patterns remain applicable.
Table of Contents
- Setup & Authentication
- TypeScript Types
- Agents
- Automations
- Projects & Tasks
- Webhooks
- Bundles (Import/Export)
- Error Handling Taxonomy
- Pagination
- Testing & Mocking
- Related
Setup & Authentication
A tiny HTTP helper
Until the SDK ships, use a small typed wrapper around Action API v2.
Each operation sends a JSON body to POST /{operation}. All recipes use this helper:
async function taskade(operation: string, body: unknown = {}) {
const res = await fetch(`https://www.taskade.com/api/v2/${operation}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TASKADE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await res.json();
// v2 wraps success as { ok: true, ... } and errors as { ok: false, message, code }
if (!res.ok || data.ok === false) {
throw Object.assign(new Error(data.message ?? operation), { status: res.status, data });
}
return data;
}
Never hardcode your token. Useprocess.env.TASKADE_TOKEN, a.envfile (gitignored), or a secret manager.
Per-request token override
For multi-tenant apps, pass the caller's token to each request. Do not read a shared token from the environment:
async function taskadeAs(token: string, operation: string, body: unknown = {}) {
const res = await fetch(`https://www.taskade.com/api/v2/${operation}`, {
method: "POST",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return res.json();
}
const { items } = await taskadeAs(userAccessToken, "listSpaces");
TypeScript Types
Declare the shapes that you consume so responses stay type-safe.
The generated client will export TaskadePublicApi plus request and response types. See the SDK Preview quickstart.
type Project = { id: string; name: string };
async function findProject(spaceId: string, name: string): Promise<Project | undefined> {
const { items } = await taskade("listProjects", { spaceId });
return (items as Project[]).find(p => p.name === name);
}
Use type guards on the { ok } union that v2 returns:
function isError(resp: unknown): resp is { ok: false; message: string; code: string } {
return typeof resp === "object" && resp !== null && (resp as any).ok === false;
}
Agents
Prompt an agent
promptAgent sends one prompt and returns a synchronous text response in summary:
const { summary } = await taskade("promptAgent", {
spaceId: SPACE_ID,
agentId: AGENT_ID,
prompt: "Draft a weekly standup summary from these notes",
});
console.log(summary);
Review past conversations
// List an agent's conversations, then fetch one
const { items } = await taskade("listConversations", { agentId: AGENT_ID });
const convo = await taskade("getConversation", {
agentId: AGENT_ID,
convoId: items[0].id,
});
Attach knowledge to an agent
// Ground the agent in a project
await taskade("addKnowledgeProject", {
agentId: AGENT_ID,
projectId: "PROJECT_ID",
});
// Or attach uploaded media
await taskade("addKnowledgeMedia", {
agentId: AGENT_ID,
mediaId: "MEDIA_ID",
});
Handle rate limits with retry
The helper throws with a status property, so back off on 429:
async function promptWithRetry(
spaceId: string,
agentId: string,
prompt: string,
retries = 3,
) {
for (let i = 0; i < retries; i++) {
try {
return await taskade("promptAgent", { spaceId, agentId, prompt });
} catch (err: any) {
if (err.status === 429 && i < retries - 1) {
await new Promise(r => setTimeout(r, 2 ** i * 1000));
continue;
}
throw err;
}
}
throw new Error("Rate limit retries exhausted");
}
Automations
There is no "run automation" API operation. Automations run inside Taskade.A Taskade trigger starts the run, such as task added or task completed.
The run then acts through built-in steps, including an outbound HTTP Request action.
Kick off an automation from code
To start a flow, create the object that its trigger watches. The automation then runs.
For example, creating a task starts a "task added → notify" automation:
// Creating the task is enough — the automation's trigger handles the rest
await fetch("https://www.taskade.com/api/v1/projects/PROJECT_ID/tasks/", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TASKADE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
tasks: [
{
contentType: "text/markdown",
content: "Onboard Acme Corp (Pro)",
placement: "beforeend",
},
],
}),
});
To push external data into an automation, point it at an inbound Webhook trigger and POST your payload to the URL Taskade gives you.
Projects & Tasks
Create a project from Markdown
v2 createProject seeds a whole project (outline and all) from a Markdown string:
const { item: project } = await taskade("createProject", {
spaceId: SPACE_ID,
contentType: "text/markdown",
content: "# Q2 Roadmap\n\n- Ship v2 API docs\n- Write SDK cookbook",
});
Add tasks
Task writes ship in both versions. As of August 2026, v2 includescreateTask,updateTask,deleteTask, andmoveTask.v2 also includes complete and uncomplete operations, assignees, dates, notes, and custom fields.
The v1 example still works. The
createTaskaction is the v2 equivalent.
// v1: create tasks in a project
await fetch(`https://www.taskade.com/api/v1/projects/${project.id}/tasks/`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TASKADE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
// contentType, content and placement are all required on each entry.
// Send at most 20 entries per call.
tasks: [
{ contentType: "text/markdown", content: "Ship v2 API docs", placement: "beforeend" },
{ contentType: "text/markdown", content: "Write SDK cookbook", placement: "beforeend" },
],
}),
});
Mark a task complete
// v1: complete a task
await fetch(
`https://www.taskade.com/api/v1/projects/${project.id}/tasks/${taskId}/complete`,
{
method: "POST",
headers: { Authorization: `Bearer ${process.env.TASKADE_TOKEN}` },
},
);
Webhooks
Taskade supports two outbound event patterns.Register a signed programmatic webhook with
POST /api/v2/webhooksfor the six events that API exposes.Or build a no-code automation with any Taskade trigger and an outbound HTTP action.
See the Webhooks guide for the event list, signature verification, and the automation pattern.
Register a signed webhook
The resource-style webhook API returns an HMAC signing secret once. Store it securely.
For every delivery, make sure that the X-Taskade-Signature header is valid:
const { webhook, secret } = await fetch("https://www.taskade.com/api/v2/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TASKADE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
targetUrl: "https://your-app.example.com/hooks/taskade",
events: ["task.due", "comment.created"],
spaceIds: [],
}),
}).then(response => response.json());
Use GET /api/v2/webhooks to list registrations and GET or DELETE /api/v2/webhooks/{id} to inspect or remove one. The {id} is the URL-encoded target URL.
Receive an event in your app
For a no-code automation, your endpoint receives the JSON body from its HTTP action.
Make the handler idempotent because automations can retry failed requests:
const processed = new Set<string>();
app.post("/hooks/taskade", (req, res) => {
const eventId = `${req.body.projectId}:${req.body.nodeId}`;
if (processed.has(eventId)) return res.status(200).end();
processed.add(eventId);
// ... handle the task event
res.status(200).end();
});
To send data into Taskade, use an inbound webhook trigger. See Inbound Webhooks.
Bundles (Import/Export)
Export a Taskade Genesis app as a portable bundle, then import elsewhere. See Bundles & App Kits for the full schema.
// Export a space's bundle (exportBundle returns the bundle under `item`)
const { item: bundleData } = await taskade("exportBundle", { spaceId: SOURCE_SPACE_ID });
await fs.writeFile("my-app.bundle.json", JSON.stringify(bundleData, null, 2));
// Import into another workspace (note: importBundle takes workspaceId + bundleData)
const imported = await taskade("importBundle", {
workspaceId: TARGET_WORKSPACE_ID,
bundleData: JSON.parse(await fs.readFile("my-app.bundle.json", "utf8")),
});
Error Handling Taxonomy
v2 reports failures with an HTTP error status and an { ok: false, message, code } body.
The taskade helper attaches status to the thrown error. Branch on that value:
try {
await taskade("promptAgent", { spaceId: SPACE_ID, agentId: AGENT_ID, prompt: "..." });
} catch (err: any) {
switch (err.status) {
case 401: /* Invalid token — refresh or regenerate */ break;
case 403: /* Account lacks access or the role can't write — not a token scope */ break;
case 404: /* Agent not found */ break;
case 429: /* Rate limited — retry with backoff */ break;
case 402: /* Out of credits — top up or switch model */ break;
case 500:
case 502:
case 503: /* Retry with backoff */ break;
default: throw err; // includes network/parse errors with no status
}
}
| err.status | Retry? | Typical Fix |
|---|---|---|
| 400 | No | Fix request body |
| 401 | No | Refresh or regenerate token |
| 402 | No | Top up credits or change model |
| 403 | No | The token account lacks access, or its role cannot write. Personal access tokens have no scopes. Regenerating a token does not help. Change the membership or role |
| 404 | No | Make sure that the ID and workspace access are correct |
| 429 | Yes | Exponential backoff |
| 5xx | Yes | Retry up to 3 times |
Pagination
Tasks and blocks use cursor pagination. Set after to the last item's id to get the next page.
Stop when a page contains fewer items than limit. Members, conversations, and projects use page and limit.
// Manual loop over a project's tasks
let after: string | undefined;
const limit = 100;
do {
const { items } = await taskade("listTasks", { projectId, limit, after });
for (const task of items) console.log(task.text);
after = items.length === limit ? items[items.length - 1].id : undefined;
} while (after);
// Async iterator helper
async function* iterateTasks(projectId: string, limit = 100) {
let after: string | undefined;
let count: number;
do {
const { items } = await taskade("listTasks", { projectId, limit, after });
count = items.length;
for (const t of items) yield t;
after = count === limit ? items[count - 1].id : undefined;
} while (after);
}
for await (const task of iterateTasks(projectId)) {
console.log(task.text);
}
Testing & Mocking
Environment-based client
Wrap the helper so tests do not access the network:
// client.ts
export const callTaskade =
process.env.NODE_ENV === "test" ? createMockTaskade() : taskade;
Mock with vitest / jest
Every call uses one fetch. Stub fetch or the taskade helper, and return a { ok: true, ... } body:
import { vi } from "vitest";
const callTaskade = vi.fn().mockResolvedValue({
ok: true,
summary: "mocked response",
});
// test code using callTaskade("promptAgent", { ... })
Integration tests with a sandbox workspace
Create a dedicated "SDK Test" workspace. Then issue the CI token from a separate Taskade account.
Make that account a member of only the test workspace. Personal access tokens have no workspace scope.
Your pipeline can then use the live API without access to production workspaces.
Related
→ Webhooks