Skip to content

Todoist sample

This Pack provides an integration with the task tracking app Todoist. It uses a variety of building blocks to allow users to work with their projects and tasks, including:

  • Formulas that provide rich data about an item given its URL.
  • Column formats that automatically apply those formulas to matching URLs.
  • Action formulas that create and update items, for use in button and automations.
  • Sync tables for pulling in all of the user's items.

The Pack uses OAuth2 to connect to a user's Todoist account, which you can create for free.

import * as coda from "@codahq/packs-sdk";


// Constants.

const ProjectUrlPatterns: RegExp[] = [
  new RegExp("^https://todoist.com/app/project/([0-9]+)$"),
  new RegExp("^https://todoist.com/showProject\\?id=([0-9]+)"),
];

const TaskUrlPatterns: RegExp[] = [
  new RegExp("^https://todoist.com/app/project/[0-9]+/task/([0-9]+)$"),
  new RegExp("^https://todoist.com/showTask\\?id=([0-9]+)"),
];


// Pack setup.

export const pack = coda.newPack();

pack.addNetworkDomain("todoist.com");

pack.setUserAuthentication({
  type: coda.AuthenticationType.OAuth2,
  // OAuth2 URLs and scopes are found in the the Todoist OAuth guide:
  // https://developer.todoist.com/guides/#oauth
  authorizationUrl: "https://todoist.com/oauth/authorize",
  tokenUrl: "https://todoist.com/oauth/access_token",
  scopes: ["data:read_write"],

  // Determines the display name of the connected account.
  getConnectionName: async function (context) {
    let url = coda.withQueryParams("https://api.todoist.com/sync/v8/sync", {
      resource_types: JSON.stringify(["user"]),
    });
    let response = await context.fetcher.fetch({
      method: "GET",
      url: url,
    });
    return response.body.user?.full_name;
  },
});

// Schemas

// A reference to a synced Project. Usually you can use
// `coda.makeReferenceSchemaFromObjectSchema` to generate these from the primary
// schema, but that doesn't work in this case since a Project itself can contain
// a reference to a parent project.
const ProjectReferenceSchema = coda.makeObjectSchema({
  codaType: coda.ValueHintType.Reference,
  properties: {
    name: {type: coda.ValueType.String, required: true},
    projectId: {type: coda.ValueType.Number, required: true},
  },
  primary: "name",
  id: "projectId",
  identity: {
    name: "Project",
  },
});

const ProjectSchema = coda.makeObjectSchema({
  properties: {
    name: {
      description: "The name of the project.",
      type: coda.ValueType.String,
      required: true,
    },
    url: {
      description: "A link to the project in the Todoist app.",
      type: coda.ValueType.String,
      codaType: coda.ValueHintType.Url,
    },
    shared: {
      description: "Is the project is shared.",
      type: coda.ValueType.Boolean,
    },
    favorite: {
      description: "Is the project a favorite.",
      type: coda.ValueType.Boolean,
    },
    projectId: {
      description: "The ID of the project.",
      type: coda.ValueType.Number,
      required: true,
    },
    parentProjectId: {
      description: "For sub-projects, the ID of the parent project.",
      type: coda.ValueType.Number,
    },
    // Add a reference to the sync'ed row of the parent project.
    // References only work in sync tables.
    parentProject: ProjectReferenceSchema,
  },
  primary: "name",
  id: "projectId",
  featured: ["url"],
  identity: {
    name: "Project",
  },
});

// A reference to a synced Task. Usually you can use
// `coda.makeReferenceSchemaFromObjectSchema` to generate these from the primary
// schema, but that doesn't work in this case since a task itself can contain
// a reference to a parent task.
const TaskReferenceSchema = coda.makeObjectSchema({
  codaType: coda.ValueHintType.Reference,
  properties: {
    name: {type: coda.ValueType.String, required: true},
    taskId: {type: coda.ValueType.Number, required: true},
  },
  primary: "name",
  id: "taskId",
  identity: {
    name: "Task",
  },
});

const TaskSchema = coda.makeObjectSchema({
  properties: {
    name: {
      description: "The name of the task.",
      type: coda.ValueType.String,
      required: true,
    },
    description: {
      description: "A detailed description of the task.",
      type: coda.ValueType.String,
    },
    url: {
      description: "A link to the task in the Todoist app.",
      type: coda.ValueType.String,
      codaType: coda.ValueHintType.Url
    },
    order: {
      description: "The position of the task in the project or parent task.",
      type: coda.ValueType.Number,
    },
    priority: {
      description: "The priority of the task.",
      type: coda.ValueType.String,
    },
    taskId: {
      description: "The ID of the task.",
      type: coda.ValueType.Number,
      required: true,
    },
    projectId: {
      description: "The ID of the project that the task belongs to.",
      type: coda.ValueType.Number,
    },
    parentTaskId: {
      description: "For sub-tasks, the ID of the parent task it belongs to.",
      type: coda.ValueType.Number,
    },
    // A reference to the sync'ed row of the project.
    // References only work in sync tables.
    project: ProjectReferenceSchema,
    // Add a reference to the sync'ed row of the parent task.
    // References only work in sync tables.
    parentTask: TaskReferenceSchema,
  },
  primary: "name",
  id: "taskId",
  featured: ["project", "url"],
  identity: {
    name: "Task",
  },
});

/**
 * Convert a Project API response to a Project schema.
 */
function toProject(project: any, withReferences=false) {
  let result: any = {
    name: project.name,
    projectId: project.id,
    url: project.url,
    shared: project.shared,
    favorite: project.favorite,
    parentProjectId: project.parent_id,
  };
  if (withReferences && project.parent_id) {
    result.parentProject = {
      projectId: project.parent_id,
      name: "Not found",  // If sync'ed, the real name will be shown instead.
    };
  }
  return result;
}

/**
 * Convert a Task API response to a Task schema.
 */
function toTask(task: any, withReferences=false) {
  let result: any = {
    name: task.content,
    description: task.description,
    url: task.url,
    order: task.order,
    priority: task.priority,
    taskId: task.id,
    projectId: task.project_id,
    parentTaskId: task.parent_id,
  };
  if (withReferences) {
    // Add a reference to the corresponding row in the Projects sync table.
    result.project = {
      projectId: task.project_id,
      name: "Not found",  // If sync'ed, the real name will be shown instead.
    };
    if (task.parent_id) {
      // Add a reference to the corresponding row in the Tasks sync table.
      result.parentTask = {
        taskId: task.parent_id,
        name: "Not found",  // If sync'ed, the real name will be shown instead.
      };
    }
  }
  return result;
}


// Formulas (read-only).

pack.addFormula({
  name: "GetProject",
  description: "Gets a Todoist project by URL",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "url",
      description: "The URL of the project",
    }),
  ],
  resultType: coda.ValueType.Object,
  schema: ProjectSchema,

  execute: async function ([url], context) {
    let projectId = extractProjectId(url);
    let response = await context.fetcher.fetch({
      url: "https://api.todoist.com/rest/v1/projects/" + projectId,
      method: "GET",
    });
    return toProject(response.body);
  },
});

pack.addFormula({
  name: "GetTask",
  description: "Gets a Todoist task by URL",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "url",
      description: "The URL of the task",
    }),
  ],
  resultType: coda.ValueType.Object,
  schema: TaskSchema,

  execute: async function ([url], context) {
    let taskId = extractTaskId(url);
    let response = await context.fetcher.fetch({
      url: "https://api.todoist.com/rest/v1/tasks/" + taskId,
      method: "GET",
    });
    return toTask(response.body);
  },
});


// Column Formats.

pack.addColumnFormat({
  name: "Project",
  formulaName: "GetProject",
  formulaNamespace: "Deprecated",
  matchers: ProjectUrlPatterns,
});

pack.addColumnFormat({
  name: "Task",
  formulaName: "GetTask",
  formulaNamespace: "Deprecated",
  matchers: TaskUrlPatterns,
});


// Action formulas (buttons/automations).

pack.addFormula({
  name: "AddProject",
  description: "Add a new Todoist project",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "name",
      description: "The name of the new project",
    }),
  ],
  resultType: coda.ValueType.String,
  isAction: true,

  execute: async function ([name], context) {
    let response = await context.fetcher.fetch({
      url: "https://api.todoist.com/rest/v1/projects",
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        name: name,
      }),
    });
    return response.body.url;
  },
});

pack.addFormula({
  name: "AddTask",
  description: "Add a new task.",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "name",
      description: "The name of the task.",
    }),
    coda.makeParameter({
      type: coda.ParameterType.Number,
      name: "projectId",
      description: "The ID of the project to add it to. If blank, " +
          "it will be added to the user's Inbox.",
      optional: true,
    }),
  ],
  resultType: coda.ValueType.String,
  isAction: true,

  execute: async function ([name, projectId], context) {
    let response = await context.fetcher.fetch({
      url: "https://api.todoist.com/rest/v1/tasks",
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        content: name,
        project_id: projectId,
      }),
    });
    return response.body.url;
  },
});

pack.addFormula({
  name: "UpdateTask",
  description: "Updates the name of a task.",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "taskId",
      description: "The ID of the task to update.",
    }),
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "name",
      description: "The new name of the task.",
    }),
  ],
  resultType: coda.ValueType.Object,
  schema: TaskSchema,
  isAction: true,

  execute: async function ([taskId, name], context) {
    let url = "https://api.todoist.com/rest/v1/tasks/" + taskId;
    await context.fetcher.fetch({
      url: url,
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        content: name,
      }),
    });
    // Get the updated Task and return it, which will update the row in the sync
    // table.
    let response = await context.fetcher.fetch({
      url: url,
      method: "GET",
      cacheTtlSecs: 0,  // Ensure we are getting the latest data.
    });
    return toTask(response.body);
  },
});

pack.addFormula({
  name: "MarkAsComplete",
  description: "Mark a task as completed.",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "taskId",
      description: "The ID of the task to be marked as complete.",
    }),
  ],
  resultType: coda.ValueType.String,
  isAction: true,

  execute: async function ([taskId], context) {
    let url = "https://api.todoist.com/rest/v1/tasks/" + taskId + "/close";
    await context.fetcher.fetch({
      method: "POST",
      url: url,
      headers: {
        "Content-Type": "application/json",
      },
    });
    return "OK";
  },
});


// Sync tables.

pack.addSyncTable({
  name: "Projects",
  schema: ProjectSchema,
  identityName: "Project",
  formula: {
    name: "SyncProjects",
    description: "Sync projects",
    parameters: [],

    execute: async function ([], context) {
      let url = "https://api.todoist.com/rest/v1/projects";
      let response = await context.fetcher.fetch({
        method: "GET",
        url: url,
      });

      let results = [];
      for (let project of response.body) {
        results.push(toProject(project, true));
      }
      return {
        result: results,
      };
    },
  },
});

pack.addSyncTable({
  name: "Tasks",
  schema: TaskSchema,
  identityName: "Task",
  formula: {
    name: "SyncTasks",
    description: "Sync tasks",
    parameters: [],
    execute: async function ([], context) {
      let url = "https://api.todoist.com/rest/v1/tasks";
      let response = await context.fetcher.fetch({
        method: "GET",
        url: url,
      });

      let results = [];
      for (let task of response.body) {
        results.push(toTask(task, true));
      }
      return {
        result: results,
      };
    },
  },
});


// Helper functions.

function extractProjectId(projectUrl: string) {
  for (let pattern of ProjectUrlPatterns) {
    let matches = projectUrl.match(pattern);
    if (matches && matches[1]) {
      return matches[1];
    }
  }
  throw new coda.UserVisibleError("Invalid project URL: " + projectUrl);
}

function extractTaskId(taskUrl: string) {
  for (let pattern of TaskUrlPatterns) {
    let matches = taskUrl.match(pattern);
    if (matches && matches[1]) {
      return matches[1];
    }
  }
  throw new coda.UserVisibleError("Invalid task URL: " + taskUrl);
}