diff --git a/packages/backend/src/apps/todoist/actions/create-task/index.ts b/packages/backend/src/apps/todoist/actions/create-task/index.ts
new file mode 100644
index 00000000..257d847c
--- /dev/null
+++ b/packages/backend/src/apps/todoist/actions/create-task/index.ts
@@ -0,0 +1,100 @@
+import defineAction from '../../../../helpers/define-action';
+
+export default defineAction({
+ name: 'Create Task',
+ key: 'createTask',
+ description: 'Creates a Task in Todoist',
+ arguments: [
+ {
+ label: 'Project ID',
+ key: 'projectId',
+ type: 'dropdown' as const,
+ required: false,
+ variables: false,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listProjects',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Section ID',
+ key: 'sectionId',
+ type: 'dropdown' as const,
+ required: false,
+ variables: false,
+ dependsOn: ['parameters.projectId'],
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listSections',
+ },
+ {
+ name: 'parameters.projectId',
+ value: '{parameters.projectId}',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Labels',
+ key: 'labels',
+ type: 'string' as const,
+ required: false,
+ variables: true,
+ description:
+ 'Labels to add to task (comma separated). Examples: "work" "work,imported"',
+ },
+ {
+ label: 'Content',
+ key: 'content',
+ type: 'string' as const,
+ required: true,
+ variables: true,
+ description:
+ 'Task content, may be markdown. Example: "Foo"',
+ },
+ {
+ label: 'Description',
+ key: 'description',
+ type: 'string' as const,
+ required: false,
+ variables: true,
+ description:
+ 'Task description, may be markdown. Example: "Foo"',
+ },
+ ],
+
+ async run($) {
+ const requestPath = `/tasks`;
+ const {
+ projectId,
+ sectionId,
+ labels,
+ content,
+ description
+ } = $.step.parameters;
+
+ const labelsArray = (labels as string).split(',')
+
+ const payload = {
+ content,
+ description: description || null,
+ project_id: projectId || null,
+ labels: labelsArray || null,
+ section_id: sectionId || null,
+ }
+
+ const response = await $.http.post(requestPath, payload);
+
+ $.setActionItem({ raw: response.data });
+ },
+});
diff --git a/packages/backend/src/apps/todoist/actions/index.ts b/packages/backend/src/apps/todoist/actions/index.ts
new file mode 100644
index 00000000..14f24a09
--- /dev/null
+++ b/packages/backend/src/apps/todoist/actions/index.ts
@@ -0,0 +1,3 @@
+import createTask from './create-task';
+
+export default [createTask];
diff --git a/packages/backend/src/apps/todoist/assets/favicon.svg b/packages/backend/src/apps/todoist/assets/favicon.svg
new file mode 100644
index 00000000..679cdc63
--- /dev/null
+++ b/packages/backend/src/apps/todoist/assets/favicon.svg
@@ -0,0 +1,14 @@
+
+
+
\ No newline at end of file
diff --git a/packages/backend/src/apps/todoist/auth/index.ts b/packages/backend/src/apps/todoist/auth/index.ts
new file mode 100644
index 00000000..fdf3d552
--- /dev/null
+++ b/packages/backend/src/apps/todoist/auth/index.ts
@@ -0,0 +1,34 @@
+import verifyCredentials from './verify-credentials';
+import isStillVerified from './is-still-verified';
+
+export default {
+ fields: [
+ {
+ key: 'screenName',
+ label: 'Screen Name',
+ type: 'string' as const,
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description:
+ 'Name your connection (only used for Automatisch UI).',
+ clickToCopy: false,
+ },
+ {
+ key: 'apiToken',
+ label: 'API Token',
+ type: 'string' as const,
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description:
+ 'Your Todoist API token. See https://todoist.com/app/settings/integrations/developer',
+ clickToCopy: false,
+ },
+ ],
+
+ verifyCredentials,
+ isStillVerified,
+};
diff --git a/packages/backend/src/apps/todoist/auth/is-still-verified.ts b/packages/backend/src/apps/todoist/auth/is-still-verified.ts
new file mode 100644
index 00000000..66bb963e
--- /dev/null
+++ b/packages/backend/src/apps/todoist/auth/is-still-verified.ts
@@ -0,0 +1,9 @@
+import { IGlobalVariable } from '@automatisch/types';
+import verifyCredentials from './verify-credentials';
+
+const isStillVerified = async ($: IGlobalVariable) => {
+ await verifyCredentials($);
+ return true;
+};
+
+export default isStillVerified;
diff --git a/packages/backend/src/apps/todoist/auth/verify-credentials.ts b/packages/backend/src/apps/todoist/auth/verify-credentials.ts
new file mode 100644
index 00000000..7b8c386a
--- /dev/null
+++ b/packages/backend/src/apps/todoist/auth/verify-credentials.ts
@@ -0,0 +1,7 @@
+import { IGlobalVariable } from '@automatisch/types';
+
+const verifyCredentials = async ($: IGlobalVariable) => {
+ await $.http.get('/projects');
+};
+
+export default verifyCredentials;
diff --git a/packages/backend/src/apps/todoist/common/add-auth-header.ts b/packages/backend/src/apps/todoist/common/add-auth-header.ts
new file mode 100644
index 00000000..dfb6a786
--- /dev/null
+++ b/packages/backend/src/apps/todoist/common/add-auth-header.ts
@@ -0,0 +1,12 @@
+import { TBeforeRequest } from '@automatisch/types';
+
+const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
+ if ($.auth.data?.apiToken) {
+ const authorizationHeader = `Bearer ${$.auth.data.apiToken}`;
+ requestConfig.headers.Authorization = authorizationHeader;
+ }
+
+ return requestConfig;
+};
+
+export default addAuthHeader;
diff --git a/packages/backend/src/apps/todoist/dynamic-data/index.ts b/packages/backend/src/apps/todoist/dynamic-data/index.ts
new file mode 100644
index 00000000..cc6c6000
--- /dev/null
+++ b/packages/backend/src/apps/todoist/dynamic-data/index.ts
@@ -0,0 +1,5 @@
+import listProjects from './list-projects';
+import listSections from './list-sections';
+import listLabels from './list-labels';
+
+export default [listProjects, listSections, listLabels];
diff --git a/packages/backend/src/apps/todoist/dynamic-data/list-labels/index.ts b/packages/backend/src/apps/todoist/dynamic-data/list-labels/index.ts
new file mode 100644
index 00000000..46fb5257
--- /dev/null
+++ b/packages/backend/src/apps/todoist/dynamic-data/list-labels/index.ts
@@ -0,0 +1,19 @@
+import { IGlobalVariable } from '@automatisch/types';
+
+export default {
+ name: 'List labels',
+ key: 'listLabels',
+
+ async run($: IGlobalVariable) {
+ const response = await $.http.get('/labels');
+
+ response.data = response.data.map((label: { name: string }) => {
+ return {
+ value: label.name,
+ name: label.name,
+ };
+ });
+
+ return response;
+ },
+};
diff --git a/packages/backend/src/apps/todoist/dynamic-data/list-projects/index.ts b/packages/backend/src/apps/todoist/dynamic-data/list-projects/index.ts
new file mode 100644
index 00000000..ca10bf72
--- /dev/null
+++ b/packages/backend/src/apps/todoist/dynamic-data/list-projects/index.ts
@@ -0,0 +1,19 @@
+import { IGlobalVariable } from '@automatisch/types';
+
+export default {
+ name: 'List projects',
+ key: 'listProjects',
+
+ async run($: IGlobalVariable) {
+ const response = await $.http.get('/projects');
+
+ response.data = response.data.map((project: { id: string, name: string }) => {
+ return {
+ value: project.id,
+ name: project.name,
+ };
+ });
+
+ return response;
+ },
+};
diff --git a/packages/backend/src/apps/todoist/dynamic-data/list-sections/index.ts b/packages/backend/src/apps/todoist/dynamic-data/list-sections/index.ts
new file mode 100644
index 00000000..cb4c23d9
--- /dev/null
+++ b/packages/backend/src/apps/todoist/dynamic-data/list-sections/index.ts
@@ -0,0 +1,23 @@
+import { IGlobalVariable } from '@automatisch/types';
+
+export default {
+ name: 'List sections',
+ key: 'listSections',
+
+ async run($: IGlobalVariable) {
+ const params = {
+ project_id: ($.step.parameters.projectId as string),
+ };
+
+ const response = await $.http.get('/sections', {params});
+
+ response.data = response.data.map((section: { id: string, name: string }) => {
+ return {
+ value: section.id,
+ name: section.name,
+ };
+ });
+
+ return response;
+ },
+};
diff --git a/packages/backend/src/apps/todoist/index.d.ts b/packages/backend/src/apps/todoist/index.d.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/backend/src/apps/todoist/index.ts b/packages/backend/src/apps/todoist/index.ts
new file mode 100644
index 00000000..a1850597
--- /dev/null
+++ b/packages/backend/src/apps/todoist/index.ts
@@ -0,0 +1,22 @@
+import defineApp from '../../helpers/define-app';
+import addAuthHeader from './common/add-auth-header';
+import auth from './auth';
+import triggers from './triggers';
+import actions from './actions';
+import dynamicData from './dynamic-data';
+
+export default defineApp({
+ name: 'Todoist',
+ key: 'todoist',
+ iconUrl: '{BASE_URL}/apps/todoist/assets/favicon.svg',
+ authDocUrl: 'https://automatisch.io/docs/apps/todoist/connection',
+ supportsConnections: true,
+ baseUrl: 'https://todoist.com',
+ apiBaseUrl: 'https://api.todoist.com/rest/v2',
+ primaryColor: 'e44332',
+ beforeRequest: [addAuthHeader],
+ auth,
+ triggers,
+ actions,
+ dynamicData,
+});
diff --git a/packages/backend/src/apps/todoist/triggers/get-tasks/get-tasks.ts b/packages/backend/src/apps/todoist/triggers/get-tasks/get-tasks.ts
new file mode 100644
index 00000000..089d02fe
--- /dev/null
+++ b/packages/backend/src/apps/todoist/triggers/get-tasks/get-tasks.ts
@@ -0,0 +1,30 @@
+import { IGlobalVariable } from '@automatisch/types';
+
+const getActiveTasks = async ($: IGlobalVariable) => {
+
+ const params = {
+ project_id: ($.step.parameters.projectId as string)?.trim(),
+ section_id: ($.step.parameters.sectionId as string)?.trim(),
+ label: ($.step.parameters.label as string)?.trim(),
+ filter: ($.step.parameters.filter as string)?.trim(),
+ };
+
+ const response = await $.http.get('/tasks', { params });
+
+ // todoist api doesn't offer sorting, so we inverse sort on id here
+ response.data.sort((a: { id: number; }, b: { id: number; }) => {
+ return b.id - a.id;
+ })
+
+ for (const task of response.data) {
+ $.pushTriggerItem({
+ raw: task,
+ meta:{
+ internalId: task.id as string,
+ }
+ });
+ }
+};
+
+
+export default getActiveTasks;
diff --git a/packages/backend/src/apps/todoist/triggers/get-tasks/index.ts b/packages/backend/src/apps/todoist/triggers/get-tasks/index.ts
new file mode 100644
index 00000000..29ea1ac3
--- /dev/null
+++ b/packages/backend/src/apps/todoist/triggers/get-tasks/index.ts
@@ -0,0 +1,80 @@
+import defineTrigger from '../../../../helpers/define-trigger';
+import getActiveTasks from './get-tasks';
+
+export default defineTrigger({
+ name: 'Get Active Tasks',
+ key: 'getActiveTasks',
+ pollInterval: 15,
+ description: 'Triggers when new Task(s) are found',
+ arguments: [
+ {
+ label: 'Project ID',
+ key: 'projectId',
+ type: 'dropdown' as const,
+ required: false,
+ variables: false,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listProjects',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Section ID',
+ key: 'sectionId',
+ type: 'dropdown' as const,
+ required: false,
+ variables: false,
+ dependsOn: ['parameters.projectId'],
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listSections',
+ },
+ {
+ name: 'parameters.projectId',
+ value: '{parameters.projectId}',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Label',
+ key: 'label',
+ type: 'dropdown' as const,
+ required: false,
+ variables: false,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listLabels',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Filter',
+ key: 'filter',
+ type: 'string' as const,
+ required: false,
+ variables: false,
+ description:
+ 'Limit queried tasks to this filter. Example: "Meeting & today"',
+ },
+ ],
+
+ async run($) {
+ await getActiveTasks($);
+ },
+});
diff --git a/packages/backend/src/apps/todoist/triggers/index.ts b/packages/backend/src/apps/todoist/triggers/index.ts
new file mode 100644
index 00000000..a52a15d7
--- /dev/null
+++ b/packages/backend/src/apps/todoist/triggers/index.ts
@@ -0,0 +1,3 @@
+import getTasks from './get-tasks';
+
+export default [getTasks];
diff --git a/packages/docs/pages/apps/todoist/actions.md b/packages/docs/pages/apps/todoist/actions.md
new file mode 100644
index 00000000..6f433845
--- /dev/null
+++ b/packages/docs/pages/apps/todoist/actions.md
@@ -0,0 +1,14 @@
+---
+favicon: /favicons/todoist.svg
+items:
+ - name: Get Tasks
+ desc: Finds tasks in Todoist, optionally matching specified parameters.
+ - name: Create Task
+ desc: Creates a task in Todoist.
+---
+
+
+
+
diff --git a/packages/docs/pages/apps/todoist/connection.md b/packages/docs/pages/apps/todoist/connection.md
new file mode 100644
index 00000000..6e6366ed
--- /dev/null
+++ b/packages/docs/pages/apps/todoist/connection.md
@@ -0,0 +1,10 @@
+# Todoist
+
+:::info
+This page explains the steps you need to follow to set up the Todoist connection in Automatisch. If any of the steps are outdated, please let us know!
+:::
+
+1. Go to the account [Integrations page](https://todoist.com/app/settings/integrations/developer) to copy your **API token**.
+1. Paste the **API token** value into Automatisch.
+1. Enter a memorable name for your connection in the **Screen Name** field.
+1. Click the **Submit** button on Automatisch.
diff --git a/packages/docs/pages/public/favicons/todoist.svg b/packages/docs/pages/public/favicons/todoist.svg
new file mode 100644
index 00000000..679cdc63
--- /dev/null
+++ b/packages/docs/pages/public/favicons/todoist.svg
@@ -0,0 +1,14 @@
+
+
+
\ No newline at end of file