Guides

SDK Cookbook

9 min readBuilding

Production patterns for integrating with Taskade.


The @taskade/sdk package is in preview and not yet on public npm. The recipes below run today with plain HTTP — a small taskade(operation, body) helper over the Action API v2, with the REST API v1 alongside it where a recipe reaches for a v1 route. See the SDK Preview quickstart for what the generated client will look like when it ships. The concepts (retry, pagination, idempotency, testing) carry over either way.

Table of Contents


Setup & Authentication

A tiny HTTP helper

Until the SDK ships, the cleanest pattern is a small typed wrapper around the Action API v2 — every operation is a POST /{operation} with a JSON body. The recipes below all build on this one helper:

Typescript
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. Use process.env.TASKADE_TOKEN, a .env file (gitignored), or a secret manager.

Per-request token override

For multi-tenant apps where each request acts on behalf of a different user, pass the caller's token in instead of reading it from the environment:

Typescript
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 you consume so responses stay type-safe. (When the generated client ships, it exports a TaskadePublicApi plus request/response types for each v2 operation — see the SDK Preview quickstart.)

Typescript
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:

Typescript
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:

Typescript
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

Typescript
// 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

Typescript
// 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:

Typescript
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, fired by a Taskade trigger (such as task added or task completed) and acting through built-in steps including an outbound HTTP Request action.

Kick off an automation from code

To start a flow from your integration, create the object its trigger watches — the automation fires on its own. For example, a "task added → notify" automation runs when you create a task via the REST API v1:

Typescript
// 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: [{ content: "Onboard Acme Corp (Pro)" }] }),
});

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:

Typescript
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 — v2 gained them in August 2026 (createTask, updateTask, deleteTask, moveTask, assignees, dates, notes, fields). The v1 example below still works; the createTask action is the v2 equivalent.

Typescript
// 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({
    tasks: [
      { content: "Ship v2 API docs", contentType: "text/markdown" },
      { content: "Write SDK cookbook", contentType: "text/markdown" },
    ],
  }),
});

Mark a task complete

Typescript
// 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


There is no event-subscription API (no POST /api/v2/webhooks). To react to Taskade events, build an automation with a Taskade trigger (e.g. task completed) and an outbound HTTP action that calls your endpoint. See the Webhooks guide for the full model and the task.added / task.completed payloads.

Receive an event in your app

Your endpoint receives whatever JSON body the automation's HTTP action sends. Make your handler idempotent — automations may retry on failure:

Typescript
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 instead, 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.

Typescript
// 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 above attaches status to the thrown error, so you can branch on it:

Typescript
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's account lacks access, or its role can't write. Personal access tokens carry no scopes, so regenerating one changes nothing — fix membership or role instead
404 No Verify ID and workspace access
429 Yes Exponential backoff
5xx Yes Retry up to 3 times

Pagination

Tasks and blocks use cursor pagination: pass after set to the last item's id to get the next page; stop when a page comes back smaller than limit. (Members, conversations, and projects use page / limit instead — see the Action API v2 Reference.)

Typescript
// 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 don't hit the network:

Typescript
// client.ts
export const callTaskade =
  process.env.NODE_ENV === "test" ? createMockTaskade() : taskade;

Mock with vitest / jest

Since every call goes through one fetch, stub fetch (or the taskade helper) and return a { ok: true, ... } body:

Typescript
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 that is only a member of that workspace — a personal access token has no workspace scope, so a token from your own account would reach production data too. Your pipeline then runs against the live API on an isolated workspace, verifying real behavior without production pollution.


TypeScript SDK

Action API Guide

Webhooks

Autonomous Agents