Guides

SDK Cookbook

10 min readBuilding

Production patterns for integrating with Taskade.


The @taskade/sdk package 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

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:

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, pass the caller's token to each request. Do not read a shared token 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 that you consume so responses stay type-safe.

The generated client will export TaskadePublicApi plus request and response types. 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.

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:

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: [
      {
        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:

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. As of August 2026, v2 includes createTask, updateTask, deleteTask, and moveTask.

v2 also includes complete and uncomplete operations, assignees, dates, notes, and custom fields.

The v1 example 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({
    // 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

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


Taskade supports two outbound event patterns.

Register a signed programmatic webhook with POST /api/v2/webhooks for 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:

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

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, 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 attaches status to the thrown error. Branch on that value:

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 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.

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 do not access the network:

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

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.

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.


TypeScript SDK

Action API Guide

Webhooks

Autonomous Agents